@@ -0,0 +1,102 @@ |
| 1 | +// SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | + |
| 3 | +// Package auth contains shared auth primitives used across the auth |
| 4 | +// surface (handlers, middleware, services). Reserved-name validation |
| 5 | +// lives here because it's read by signup *and* by username-change in S10. |
| 6 | +package auth |
| 7 | + |
| 8 | +import "strings" |
| 9 | + |
| 10 | +// reservedNames is the set of usernames that may NOT be claimed by users. |
| 11 | +// It covers: |
| 12 | +// - Every static top-level route shithub registers (so /login can never |
| 13 | +// resolve to a user named "login"). |
| 14 | +// - GitHub-known reservations we choose to mirror for parity. |
| 15 | +// - A buffer of likely-future routes we'd rather lock in now than fight |
| 16 | +// to reclaim from a squatter later. |
| 17 | +// |
| 18 | +// MAINTENANCE: when adding a new top-level route in any sprint, add the |
| 19 | +// leading path segment here. The audit test in |
| 20 | +// internal/web/handlers/handlers_test.go enforces this by walking the |
| 21 | +// registered chi router and checking every static segment. |
| 22 | +var reservedNames = map[string]struct{}{ |
| 23 | + // shithub-registered routes (S02–S05+). |
| 24 | + "static": {}, |
| 25 | + "healthz": {}, |
| 26 | + "readyz": {}, |
| 27 | + "metrics": {}, |
| 28 | + "signup": {}, |
| 29 | + "login": {}, |
| 30 | + "logout": {}, |
| 31 | + "password": {}, |
| 32 | + "verify-email": {}, |
| 33 | + "internal": {}, |
| 34 | + "settings": {}, |
| 35 | + "api": {}, |
| 36 | + "admin": {}, |
| 37 | + "new": {}, |
| 38 | + "explore": {}, |
| 39 | + "trending": {}, |
| 40 | + "marketplace": {}, |
| 41 | + "issues": {}, |
| 42 | + "pulls": {}, |
| 43 | + "notifications": {}, |
| 44 | + "watching": {}, |
| 45 | + "stars": {}, |
| 46 | + "orgs": {}, |
| 47 | + "organizations": {}, |
| 48 | + "search": {}, |
| 49 | + "about": {}, |
| 50 | + "contact": {}, |
| 51 | + "pricing": {}, |
| 52 | + "site": {}, |
| 53 | + "help": {}, |
| 54 | + "docs": {}, |
| 55 | + "blog": {}, |
| 56 | + "security": {}, |
| 57 | + "status": {}, |
| 58 | + "terms": {}, |
| 59 | + "privacy": {}, |
| 60 | + "cookies": {}, |
| 61 | + "oauth": {}, |
| 62 | + "sessions": {}, |
| 63 | + "saml": {}, |
| 64 | + "sso": {}, |
| 65 | + "webauthn": {}, |
| 66 | + "two-factor": {}, |
| 67 | + "recovery": {}, |
| 68 | + "avatar": {}, |
| 69 | + "avatars": {}, |
| 70 | + "raw": {}, |
| 71 | + "media": {}, |
| 72 | + "assets": {}, |
| 73 | + "favicon.ico": {}, |
| 74 | + "robots.txt": {}, |
| 75 | + "sitemap.xml": {}, |
| 76 | + "git": {}, |
| 77 | + "ssh": {}, |
| 78 | + "public": {}, |
| 79 | + "private": {}, |
| 80 | + "shithub": {}, |
| 81 | + "shithubd": {}, |
| 82 | + "shithubbot": {}, |
| 83 | +} |
| 84 | + |
| 85 | +// IsReserved reports whether name is on the reserved list. The check is |
| 86 | +// case-insensitive — usernames are stored as citext, so we normalize here |
| 87 | +// to match. |
| 88 | +func IsReserved(name string) bool { |
| 89 | + _, ok := reservedNames[strings.ToLower(name)] |
| 90 | + return ok |
| 91 | +} |
| 92 | + |
| 93 | +// ReservedNames returns a copy of the reserved set as a slice. Used by |
| 94 | +// the route-audit test in handlers_test.go to confirm every registered |
| 95 | +// top-level path segment is present here. |
| 96 | +func ReservedNames() []string { |
| 97 | + out := make([]string, 0, len(reservedNames)) |
| 98 | + for n := range reservedNames { |
| 99 | + out = append(out, n) |
| 100 | + } |
| 101 | + return out |
| 102 | +} |