···5151 name="code"
5252 tabindex="1"
5353 required
5454- placeholder="pds-tngl-sh-foo-bar"
5454+ placeholder="tngl-sh-foo-bar"
5555 />
5656 <span class="text-sm text-gray-500 mt-1">
5757 Enter the code sent to your email.
+73-15
appview/signup/signup.go
···11package signup
2233import (
44+ "bufio"
45 "fmt"
56 "log/slog"
67 "net/http"
88+ "os"
99+ "strings"
710811 "github.com/go-chi/chi/v5"
912 "github.com/posthog/posthog-go"
···1821)
19222023type Signup struct {
2121- config *config.Config
2222- db *db.DB
2323- cf *dns.Cloudflare
2424- posthog posthog.Client
2525- xrpc *xrpcclient.Client
2626- idResolver *idresolver.Resolver
2727- pages *pages.Pages
2828- l *slog.Logger
2424+ config *config.Config
2525+ db *db.DB
2626+ cf *dns.Cloudflare
2727+ posthog posthog.Client
2828+ xrpc *xrpcclient.Client
2929+ idResolver *idresolver.Resolver
3030+ pages *pages.Pages
3131+ l *slog.Logger
3232+ disallowedNicknames map[string]bool
2933}
30343135func New(cfg *config.Config, database *db.DB, pc posthog.Client, idResolver *idresolver.Resolver, pages *pages.Pages, l *slog.Logger) *Signup {
···3741 l.Warn("failed to create cloudflare client, signup will be disabled", "error", err)
3842 }
3943 }
4444+4545+ disallowedNicknames := loadDisallowedNicknames(cfg.Core.DisallowedNicknamesFile, l)
40464147 return &Signup{
4242- config: cfg,
4343- db: database,
4444- posthog: pc,
4545- idResolver: idResolver,
4646- cf: cf,
4747- pages: pages,
4848- l: l,
4848+ config: cfg,
4949+ db: database,
5050+ posthog: pc,
5151+ idResolver: idResolver,
5252+ cf: cf,
5353+ pages: pages,
5454+ l: l,
5555+ disallowedNicknames: disallowedNicknames,
4956 }
5057}
51585959+func loadDisallowedNicknames(filepath string, logger *slog.Logger) map[string]bool {
6060+ disallowed := make(map[string]bool)
6161+6262+ if filepath == "" {
6363+ logger.Debug("no disallowed nicknames file configured")
6464+ return disallowed
6565+ }
6666+6767+ file, err := os.Open(filepath)
6868+ if err != nil {
6969+ logger.Warn("failed to open disallowed nicknames file", "file", filepath, "error", err)
7070+ return disallowed
7171+ }
7272+ defer file.Close()
7373+7474+ scanner := bufio.NewScanner(file)
7575+ lineNum := 0
7676+ for scanner.Scan() {
7777+ lineNum++
7878+ line := strings.TrimSpace(scanner.Text())
7979+ if line == "" || strings.HasPrefix(line, "#") {
8080+ continue // skip empty lines and comments
8181+ }
8282+8383+ nickname := strings.ToLower(line)
8484+ if userutil.IsValidSubdomain(nickname) {
8585+ disallowed[nickname] = true
8686+ } else {
8787+ logger.Warn("invalid nickname format in disallowed nicknames file",
8888+ "file", filepath, "line", lineNum, "nickname", nickname)
8989+ }
9090+ }
9191+9292+ if err := scanner.Err(); err != nil {
9393+ logger.Error("error reading disallowed nicknames file", "file", filepath, "error", err)
9494+ }
9595+9696+ logger.Info("loaded disallowed nicknames", "count", len(disallowed), "file", filepath)
9797+ return disallowed
9898+}
9999+100100+// isNicknameAllowed checks if a nickname is allowed (not in the disallowed list)
101101+func (s *Signup) isNicknameAllowed(nickname string) bool {
102102+ return !s.disallowedNicknames[strings.ToLower(nickname)]
103103+}
104104+52105func (s *Signup) Router() http.Handler {
53106 r := chi.NewRouter()
54107 r.Post("/", s.signup)
···128181129182 if !userutil.IsValidSubdomain(username) {
130183 s.pages.Notice(w, "signup-error", "Invalid username. Username must be 4–63 characters, lowercase letters, digits, or hyphens, and can't start or end with a hyphen.")
184184+ return
185185+ }
186186+187187+ if !s.isNicknameAllowed(username) {
188188+ s.pages.Notice(w, "signup-error", "This username is not available. Please choose a different one.")
131189 return
132190 }
133191