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) timeoffStore := timeoff.NewStore(db) scheduleStore := schedule.NewStore(db) scheduleHandler := schedule.NewHandler(scheduleStore, notificationStore, timeoffStore) timeoffHandler := timeoff.NewHandler(timeoffStore, notificationStore, volunteerStore) 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.Put("/timeoff/{id}", timeoffHandler.Update) r.Delete("/timeoff/{id}", timeoffHandler.Delete) r.With(middleware.RequireAdmin).Put("/timeoff/{id}/review", timeoffHandler.Review) r.With(middleware.RequireAdmin).Get("/timeoff/{id}/shifts", timeoffHandler.RemovedShifts) // 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, with SPA fallback r.Handle("/*", spaHandler(staticDir)) return r } // spaHandler serves static files from dir, falling back to index.html for // paths that don't match a file on disk (so client-side routing works). func spaHandler(dir string) http.HandlerFunc { fs := http.Dir(dir) fileServer := http.FileServer(fs) return func(w http.ResponseWriter, r *http.Request) { // Try to open the requested path as a static file. if f, err := fs.Open(r.URL.Path); err == nil { f.Close() fileServer.ServeHTTP(w, r) return } // Not a real file — serve index.html and let React Router handle it. http.ServeFile(w, r, dir+"/index.html") } }