this repo has no description

appview/signup: username blacklist

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.sh>

authored by Anirudh Oppiliappan and committed by Tangled 1ebd3b6b 0b922f7b

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 { ··· 37 41 l.Warn("failed to create cloudflare client, signup will be disabled", "error", err) 38 42 } 39 43 } 44 + 45 + disallowedNicknames := loadDisallowedNicknames(cfg.Core.DisallowedNicknamesFile, l) 40 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, 49 56 } 50 57 } 51 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 + } 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)] 103 + } 104 + 52 105 func (s *Signup) Router() http.Handler { 53 106 r := chi.NewRouter() 54 107 r.Post("/", s.signup) ··· 128 181 129 182 if !userutil.IsValidSubdomain(username) { 130 183 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.") 184 + return 185 + } 186 + 187 + if !s.isNicknameAllowed(username) { 188 + s.pages.Notice(w, "signup-error", "This username is not available. Please choose a different one.") 131 189 return 132 190 } 133 191