mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-11 13:28:57 +00:00
b1357799a9
Add username support to registration with validation, reserved words, and real-time availability checking (TASK-409). Backend: - ValidateUsername() with format/length/reserved word checks - 35+ reserved usernames (route conflicts, system terms) - GET /auth/check-username endpoint for real-time validation - handleBootstrap auto-generates username from name (D1) - handleRegister accepts optional username, auto-generates if omitted Frontend: - Register page: username field with auto-generation from name - Join/invite page: same username field in register mode - Debounced availability checking (400ms) via /auth/check-username - Inline status indicators (checking/available/taken) - API client: register() accepts username, new checkUsername() method
95 lines
3.1 KiB
Go
95 lines
3.1 KiB
Go
package server
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
var usernameRe = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*[a-z0-9]$`)
|
|
var usernameShortRe = regexp.MustCompile(`^[a-z0-9]$`)
|
|
|
|
// reservedUsernames contains words that cannot be used as usernames because they
|
|
// conflict with existing or planned URL routes, system paths, or common terms.
|
|
// Decision D11: "be generous — better to reserve too many than too few."
|
|
var reservedUsernames = map[string]bool{
|
|
// Auth routes
|
|
"login": true, "register": true, "join": true, "signup": true, "signin": true,
|
|
"forgot-password": true, "reset-password": true,
|
|
"logout": true, "auth": true, "oauth": true, "sso": true,
|
|
|
|
// App routes
|
|
"settings": true, "admin": true, "console": true,
|
|
"dashboard": true, "api": true, "health": true,
|
|
"new": true, "create": true, "edit": true, "delete": true,
|
|
|
|
// Share/special routes
|
|
"s": true, "share": true, "shared": true,
|
|
"invite": true, "invitations": true,
|
|
|
|
// System
|
|
"app": true, "www": true, "mail": true, "ftp": true, "smtp": true,
|
|
"static": true, "assets": true, "public": true, "cdn": true,
|
|
"pad": true, "system": true, "root": true, "server": true,
|
|
"null": true, "undefined": true, "nan": true, "true": true, "false": true,
|
|
|
|
// Common reserved
|
|
"about": true, "help": true, "support": true, "contact": true,
|
|
"docs": true, "blog": true, "pricing": true, "plans": true,
|
|
"terms": true, "privacy": true, "security": true, "legal": true,
|
|
"status": true, "changelog": true, "updates": true,
|
|
"home": true, "index": true, "search": true, "explore": true,
|
|
"notifications": true, "messages": true, "feed": true,
|
|
"account": true, "profile": true, "user": true, "users": true,
|
|
"org": true, "orgs": true, "team": true, "teams": true,
|
|
"workspace": true, "workspaces": true,
|
|
"project": true, "projects": true,
|
|
}
|
|
|
|
// ValidateUsername checks a username for format, length, and reserved word violations.
|
|
// It does NOT check uniqueness (that requires a store call).
|
|
func ValidateUsername(username string) error {
|
|
if username == "" {
|
|
return fmt.Errorf("username is required")
|
|
}
|
|
|
|
// Must be lowercase
|
|
if username != strings.ToLower(username) {
|
|
return fmt.Errorf("username must be lowercase")
|
|
}
|
|
|
|
// Length check
|
|
if len(username) < 3 {
|
|
return fmt.Errorf("username must be at least 3 characters")
|
|
}
|
|
if len(username) > 39 {
|
|
return fmt.Errorf("username must be at most 39 characters")
|
|
}
|
|
|
|
// Format check
|
|
if len(username) == 1 {
|
|
if !usernameShortRe.MatchString(username) {
|
|
return fmt.Errorf("username must contain only lowercase letters, numbers, and hyphens")
|
|
}
|
|
} else if !usernameRe.MatchString(username) {
|
|
return fmt.Errorf("username must contain only lowercase letters, numbers, and hyphens, and cannot start or end with a hyphen")
|
|
}
|
|
|
|
// No consecutive hyphens
|
|
if strings.Contains(username, "--") {
|
|
return fmt.Errorf("username cannot contain consecutive hyphens")
|
|
}
|
|
|
|
// Reserved words
|
|
if reservedUsernames[username] {
|
|
return fmt.Errorf("username %q is reserved", username)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// IsReservedUsername checks if a username is in the reserved list.
|
|
func IsReservedUsername(username string) bool {
|
|
return reservedUsernames[strings.ToLower(username)]
|
|
}
|