Files
walkies/internal/server/server.go
James Griffin 3900dff5a1 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>
2026-04-08 19:02:16 -03:00

102 lines
3.6 KiB
Go

package server
import (
"database/sql"
"net/http"
"github.com/go-chi/chi/v5"
chimiddleware "github.com/go-chi/chi/v5/middleware"
"git.unsupervised.ca/walkies/internal/auth"
"git.unsupervised.ca/walkies/internal/checkin"
"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"
)
func New(db *sql.DB, jwtSecret string, staticDir string) http.Handler {
authSvc := auth.NewService(db, jwtSecret)
volunteerStore := volunteer.NewStore(db)
volunteerHandler := volunteer.NewHandler(volunteerStore, authSvc)
notificationStore := notification.NewStore(db)
notificationHandler := notification.NewHandler(notificationStore)
scheduleStore := schedule.NewStore(db)
scheduleHandler := schedule.NewHandler(scheduleStore, notificationStore)
timeoffStore := timeoff.NewStore(db)
timeoffHandler := timeoff.NewHandler(timeoffStore)
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)
r.Use(chimiddleware.RealIP)
// API routes
r.Route("/api/v1", func(r chi.Router) {
// Public auth endpoints
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))
// Volunteers
r.With(middleware.RequireAdmin).Post("/volunteers", volunteerHandler.Create)
r.Get("/volunteers", volunteerHandler.List)
r.Get("/volunteers/{id}", volunteerHandler.Get)
r.Put("/volunteers/{id}", volunteerHandler.Update)
r.With(middleware.RequireAdmin).Post("/volunteers/{id}/invite", volunteerHandler.ResendInvite)
// Shift templates (admin only)
r.Get("/shift-templates", scheduleHandler.ListTemplates)
r.With(middleware.RequireAdmin).Post("/shift-templates", scheduleHandler.CreateTemplate)
r.With(middleware.RequireAdmin).Put("/shift-templates/{id}", scheduleHandler.UpdateTemplate)
r.With(middleware.RequireAdmin).Delete("/shift-templates/{id}", scheduleHandler.DeleteTemplate)
// Shift instances
r.Get("/shifts", scheduleHandler.ListInstances)
r.With(middleware.RequireAdmin).Post("/shifts/generate", scheduleHandler.GenerateInstances)
r.With(middleware.RequireAdmin).Post("/shifts/publish", scheduleHandler.PublishMonth)
r.With(middleware.RequireAdmin).Post("/shifts/unpublish", scheduleHandler.UnpublishMonth)
r.With(middleware.RequireAdmin).Put("/shifts/{id}", scheduleHandler.UpdateInstance)
r.Post("/shifts/{id}/confirm", scheduleHandler.ConfirmShift)
// Time off
r.Get("/timeoff", timeoffHandler.List)
r.Post("/timeoff", timeoffHandler.Create)
r.With(middleware.RequireAdmin).Put("/timeoff/{id}/review", timeoffHandler.Review)
// Check-in / check-out
r.Post("/checkin", checkinHandler.CheckIn)
r.Post("/checkout", checkinHandler.CheckOut)
r.Get("/checkin/history", checkinHandler.History)
// Notifications
r.Get("/notifications", notificationHandler.List)
r.Put("/notifications/{id}/read", notificationHandler.MarkRead)
})
})
// Serve static React app for all other routes
r.Handle("/*", http.FileServer(http.Dir(staticDir)))
return r
}