| 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 | "keys": {}, |
| 81 | "tokens": {}, |
| 82 | "shithub": {}, |
| 83 | "shithubd": {}, |
| 84 | "shithubbot": {}, |
| 85 | } |
| 86 | |
| 87 | // IsReserved reports whether name is on the reserved list. The check is |
| 88 | // case-insensitive — usernames are stored as citext, so we normalize here |
| 89 | // to match. |
| 90 | func IsReserved(name string) bool { |
| 91 | _, ok := reservedNames[strings.ToLower(name)] |
| 92 | return ok |
| 93 | } |
| 94 | |
| 95 | // ReservedNames returns a copy of the reserved set as a slice. Used by |
| 96 | // the route-audit test in handlers_test.go to confirm every registered |
| 97 | // top-level path segment is present here. |
| 98 | func ReservedNames() []string { |
| 99 | out := make([]string, 0, len(reservedNames)) |
| 100 | for n := range reservedNames { |
| 101 | out = append(out, n) |
| 102 | } |
| 103 | return out |
| 104 | } |
| 105 |