Implement Issue #11: Initial admin account setup on first deploy

When the database has no users, the UI redirects to /setup and prompts
for creation of the first admin account. The setup endpoints are
self-disabling — once any user exists, POST /setup/admin returns 403
and the frontend redirects /setup back to /login. The backend uses an
atomic transaction to prevent race conditions on concurrent requests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-08 19:02:16 -03:00
parent f29d9669f8
commit 3900dff5a1
9 changed files with 585 additions and 12 deletions

View File

@@ -12,6 +12,7 @@ import (
"git.unsupervised.ca/walkies/internal/notification"
"git.unsupervised.ca/walkies/internal/schedule"
"git.unsupervised.ca/walkies/internal/server/middleware"
"git.unsupervised.ca/walkies/internal/setup"
"git.unsupervised.ca/walkies/internal/timeoff"
"git.unsupervised.ca/walkies/internal/volunteer"
)
@@ -34,6 +35,9 @@ func New(db *sql.DB, jwtSecret string, staticDir string) http.Handler {
checkinStore := checkin.NewStore(db)
checkinHandler := checkin.NewHandler(checkinStore)
setupStore := setup.NewStore(db)
setupHandler := setup.NewHandler(setupStore, authSvc)
r := chi.NewRouter()
r.Use(chimiddleware.Logger)
r.Use(chimiddleware.Recoverer)
@@ -45,6 +49,10 @@ func New(db *sql.DB, jwtSecret string, staticDir string) http.Handler {
r.Post("/auth/login", volunteerHandler.Login)
r.Post("/auth/activate", volunteerHandler.Activate)
// Public setup endpoints (self-disabling once first user exists)
r.Get("/setup/status", setupHandler.Status)
r.Post("/setup/admin", setupHandler.CreateAdmin)
// Protected routes
r.Group(func(r chi.Router) {
r.Use(middleware.Authenticate(authSvc))