@@ -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 | +} |