appview/signup: username blacklist #408

merged
opened by anirudh.fi targeting master from push-qlzpkvltqlzm
Changed files
+80 -21
appview
config
pages
templates
signup
+6 -5
appview/config/config.go
··· 10 10 ) 11 11 12 12 type CoreConfig struct { 13 - CookieSecret string `env:"COOKIE_SECRET, default=00000000000000000000000000000000"` 14 - DbPath string `env:"DB_PATH, default=appview.db"` 15 - ListenAddr string `env:"LISTEN_ADDR, default=0.0.0.0:3000"` 16 - AppviewHost string `env:"APPVIEW_HOST, default=https://tangled.sh"` 17 - Dev bool `env:"DEV, default=false"` 13 + CookieSecret string `env:"COOKIE_SECRET, default=00000000000000000000000000000000"` 14 + DbPath string `env:"DB_PATH, default=appview.db"` 15 + ListenAddr string `env:"LISTEN_ADDR, default=0.0.0.0:3000"` 16 + AppviewHost string `env:"APPVIEW_HOST, default=https://tangled.sh"` 17 + Dev bool `env:"DEV, default=false"` 18 + DisallowedNicknamesFile string `env:"DISALLOWED_NICKNAMES_FILE"` 18 19 } 19 20 20 21 type OAuthConfig struct {
+1 -1
appview/pages/templates/user/completeSignup.html
··· 51 51 name="code" 52 52 tabindex="1" 53 53 required 54 - placeholder="pds-tngl-sh-foo-bar" 54 + placeholder="tngl-sh-foo-bar" 55 55 /> 56 56 <span class="text-sm text-gray-500 mt-1"> 57 57 Enter the code sent to your email.
+73 -15
appview/signup/signup.go
··· 1 1 package signup 2 2 3 3 import ( 4 + "bufio" 4 5 "fmt" 5 6 "log/slog" 6 7 "net/http" 8 + "os" 9 + "strings" 7 10 8 11 "github.com/go-chi/chi/v5" 9 12 "github.com/posthog/posthog-go" ··· 18 21 ) 19 22 20 23 type Signup struct { 21 - config *config.Config 22 - db *db.DB 23 - cf *dns.Cloudflare 24 - posthog posthog.Client 25 - xrpc *xrpcclient.Client 26 - idResolver *idresolver.Resolver 27 - pages *pages.Pages 28 - l *slog.Logger 24 + config *config.Config 25 + db *db.DB 26 + cf *dns.Cloudflare 27 + posthog posthog.Client 28 + xrpc *xrpcclient.Client 29 + idResolver *idresolver.Resolver 30 + pages *pages.Pages 31 + l *slog.Logger 32 + disallowedNicknames map[string]bool 29 33 } 30 34 31 35 func New(cfg *config.Config, database *db.DB, pc posthog.Client, idResolver *idresolver.Resolver, pages *pages.Pages, l *slog.Logger) *Signup { ··· 38 42 } 39 43 } 40 44 45 + disallowedNicknames := loadDisallowedNicknames(cfg.Core.DisallowedNicknamesFile, l) 46 + 41 47 return &Signup{ 42 - config: cfg, 43 - db: database, 44 - posthog: pc, 45 - idResolver: idResolver, 46 - cf: cf, 47 - pages: pages, 48 - l: l, 48 + config: cfg, 49 + db: database, 50 + posthog: pc, 51 + idResolver: idResolver, 52 + cf: cf, 53 + pages: pages, 54 + l: l, 55 + disallowedNicknames: disallowedNicknames, 56 + } 57 + } 58 + 59 + func loadDisallowedNicknames(filepath string, logger *slog.Logger) map[string]bool { 60 + disallowed := make(map[string]bool) 61 + 62 + if filepath == "" { 63 + logger.Debug("no disallowed nicknames file configured") 64 + return disallowed 65 + } 66 + 67 + file, err := os.Open(filepath) 68 + if err != nil { 69 + logger.Warn("failed to open disallowed nicknames file", "file", filepath, "error", err) 70 + return disallowed 71 + } 72 + defer file.Close() 73 + 74 + scanner := bufio.NewScanner(file) 75 + lineNum := 0 76 + for scanner.Scan() { 77 + lineNum++ 78 + line := strings.TrimSpace(scanner.Text()) 79 + if line == "" || strings.HasPrefix(line, "#") { 80 + continue // skip empty lines and comments 81 + } 82 + 83 + nickname := strings.ToLower(line) 84 + if userutil.IsValidSubdomain(nickname) { 85 + disallowed[nickname] = true 86 + } else { 87 + logger.Warn("invalid nickname format in disallowed nicknames file", 88 + "file", filepath, "line", lineNum, "nickname", nickname) 89 + } 49 90 } 91 + 92 + if err := scanner.Err(); err != nil { 93 + logger.Error("error reading disallowed nicknames file", "file", filepath, "error", err) 94 + } 95 + 96 + logger.Info("loaded disallowed nicknames", "count", len(disallowed), "file", filepath) 97 + return disallowed 98 + } 99 + 100 + // isNicknameAllowed checks if a nickname is allowed (not in the disallowed list) 101 + func (s *Signup) isNicknameAllowed(nickname string) bool { 102 + return !s.disallowedNicknames[strings.ToLower(nickname)] 50 103 } 51 104 52 105 func (s *Signup) Router() http.Handler { ··· 131 184 return 132 185 } 133 186 187 + if !s.isNicknameAllowed(username) { 188 + s.pages.Notice(w, "signup-error", "This username is not available. Please choose a different one.") 189 + return 190 + } 191 + 134 192 email, err := db.GetEmailForCode(s.db, code) 135 193 if err != nil { 136 194 s.l.Error("failed to get email for code", "error", err)