fix: seed dev user from DEV_USER_EMAIL env var so auth survives restarts
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

In standalone mode (no DATABASE_URL), the in-memory user store only had
hardcoded demo accounts. Any real email the developer used was lost on every
server restart, causing OTP requests to silently fail with "unknown email".

NewUserRepository now accepts devEmail + devPassword. If DEV_USER_EMAIL is
set, that account is seeded on every startup alongside the demo users. The
developer's email is always registered, OTPs route to notify (or log to
console), and re-renders/restarts no longer break the auth flow.

New config fields: DevUserEmail (DEV_USER_EMAIL) / DevUserPassword
(DEV_USER_PASSWORD, default: "DevPassword1").

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jordan 2026-02-21 23:46:12 -07:00
parent 27e6cfd42b
commit 5f66eb0e7b
3 changed files with 19 additions and 2 deletions

View File

@ -135,7 +135,7 @@ func main() {
jobQueue, jobReader = setupDBQueue(ctx, cfg, dbPool, sseHub, logger) jobQueue, jobReader = setupDBQueue(ctx, cfg, dbPool, sseHub, logger)
} else { } else {
logger.Info("DATABASE_URL not set — running in standalone mode (in-memory queue + in-process AI)") logger.Info("DATABASE_URL not set — running in standalone mode (in-memory queue + in-process AI)")
userRepo = memory.NewUserRepository() userRepo = memory.NewUserRepository(cfg.DevUserEmail, cfg.DevUserPassword)
sessionRepo = memory.NewSessionRepository() sessionRepo = memory.NewSessionRepository()
authCodeRepo = memory.NewAuthCodeRepository() authCodeRepo = memory.NewAuthCodeRepository()
mediaRepo = memory.NewMediaRepository() mediaRepo = memory.NewMediaRepository()

View File

@ -24,7 +24,9 @@ type UserRepository struct {
} }
// NewUserRepository creates a new in-memory user repository seeded with demo users. // NewUserRepository creates a new in-memory user repository seeded with demo users.
func NewUserRepository() *UserRepository { // If devEmail is non-empty, an additional user is seeded with that email and devPassword
// so the developer's account survives server restarts without re-registering.
func NewUserRepository(devEmail, devPassword string) *UserRepository {
repo := &UserRepository{ repo := &UserRepository{
users: make(map[domain.UserID]*domain.User), users: make(map[domain.UserID]*domain.User),
passwords: make(map[domain.UserID]string), passwords: make(map[domain.UserID]string),
@ -37,6 +39,12 @@ func NewUserRepository() *UserRepository {
repo.seedUser("usr_test_001", "test@example.com", "Test User", "Password123", []string{"user"}) repo.seedUser("usr_test_001", "test@example.com", "Test User", "Password123", []string{"user"})
repo.seedUser("usr_admin_001", "admin@example.com", "Admin User", "Admin1234", []string{"admin", "user"}) repo.seedUser("usr_admin_001", "admin@example.com", "Admin User", "Admin1234", []string{"admin", "user"})
// Seed the developer's own account if DEV_USER_EMAIL is configured.
// This ensures the email is always registered after restarts without manual re-registration.
if devEmail != "" {
repo.seedUser("usr_dev_001", devEmail, "Dev User", devPassword, []string{"admin", "user"})
}
return repo return repo
} }

View File

@ -36,6 +36,12 @@ type Config struct {
SupportEmail string // SUPPORT_EMAIL, default: NOTIFY_FROM value SupportEmail string // SUPPORT_EMAIL, default: NOTIFY_FROM value
LogoURL string // LOGO_URL, default: "" (hides logo area) LogoURL string // LOGO_URL, default: "" (hides logo area)
BrandColor string // BRAND_COLOR, default: "#6366f1" BrandColor string // BRAND_COLOR, default: "#6366f1"
// Dev mode seed user — seeded into the in-memory user store on startup so the
// developer's email is always available without re-registering after each restart.
// No effect when DATABASE_URL is set (production uses real persistence).
DevUserEmail string // DEV_USER_EMAIL, e.g. "you@example.com"
DevUserPassword string // DEV_USER_PASSWORD, default: "DevPassword1"
} }
// Load reads configuration from environment variables. // Load reads configuration from environment variables.
@ -68,6 +74,9 @@ func Load() *Config {
SupportEmail: getEnvDefault("SUPPORT_EMAIL", notifyFrom), SupportEmail: getEnvDefault("SUPPORT_EMAIL", notifyFrom),
LogoURL: os.Getenv("LOGO_URL"), LogoURL: os.Getenv("LOGO_URL"),
BrandColor: getEnvDefault("BRAND_COLOR", "#6366f1"), BrandColor: getEnvDefault("BRAND_COLOR", "#6366f1"),
DevUserEmail: os.Getenv("DEV_USER_EMAIL"),
DevUserPassword: getEnvDefault("DEV_USER_PASSWORD", "DevPassword1"),
} }
return cfg return cfg