// SPDX-License-Identifier: AGPL-3.0-or-later // Package handler contains HTTP handlers for all web and API routes. package handler import ( "context" "io/fs" "net/http" "time" "kode.naiv.no/olemd/favoritter/internal/config" "kode.naiv.no/olemd/favoritter/internal/middleware" "kode.naiv.no/olemd/favoritter/internal/render" "kode.naiv.no/olemd/favoritter/internal/store" "kode.naiv.no/olemd/favoritter/web" ) // Deps bundles all dependencies injected into handlers. type Deps struct { Config *config.Config Users *store.UserStore Sessions *store.SessionStore Settings *store.SettingsStore Faves *store.FaveStore Tags *store.TagStore SignupRequests *store.SignupRequestStore Renderer *render.Renderer } // Handler holds all HTTP handler methods and their dependencies. type Handler struct { deps Deps rateLimiter *middleware.RateLimiter } // New creates a new Handler with the given dependencies. func New(deps Deps) *Handler { return &Handler{ deps: deps, rateLimiter: middleware.NewRateLimiter(deps.Config.RateLimit), } } // RateLimiterCleanupLoop periodically evicts stale rate limiter entries. func (h *Handler) RateLimiterCleanupLoop(ctx context.Context, interval time.Duration) { ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: h.rateLimiter.Cleanup() } } } // Routes registers all routes on a new ServeMux and returns it. func (h *Handler) Routes() *http.ServeMux { mux := http.NewServeMux() // Static files (served from embedded filesystem). staticFS, err := fs.Sub(web.StaticFS, "static") if err != nil { panic("embedded static filesystem missing: " + err.Error()) } mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS)))) // Uploaded images (served from the filesystem upload directory). mux.Handle("GET /uploads/", http.StripPrefix("/uploads/", http.FileServer(http.Dir(h.deps.Config.UploadDir)))) // Health check. mux.HandleFunc("GET /health", h.handleHealth) // Auth routes (rate-limited). mux.Handle("POST /login", h.rateLimiter.Limit(http.HandlerFunc(h.handleLoginPost))) mux.Handle("POST /signup", h.rateLimiter.Limit(http.HandlerFunc(h.handleSignupPost))) mux.HandleFunc("GET /login", h.handleLoginGet) mux.HandleFunc("GET /signup", h.handleSignupGet) mux.HandleFunc("POST /logout", h.handleLogout) // Password reset (for must-reset-password flow). mux.HandleFunc("GET /reset-password", h.handleResetPasswordGet) mux.HandleFunc("POST /reset-password", h.handleResetPasswordPost) // Home page. mux.HandleFunc("GET /{$}", h.handleHome) // Faves — authenticated routes use requireLogin wrapper. requireLogin := middleware.RequireLogin(h.deps.Config.BasePath) mux.Handle("GET /faves", requireLogin(http.HandlerFunc(h.handleFaveList))) mux.Handle("GET /faves/new", requireLogin(http.HandlerFunc(h.handleFaveNew))) mux.Handle("POST /faves", requireLogin(http.HandlerFunc(h.handleFaveCreate))) mux.HandleFunc("GET /faves/{id}", h.handleFaveDetail) mux.Handle("GET /faves/{id}/edit", requireLogin(http.HandlerFunc(h.handleFaveEdit))) mux.Handle("POST /faves/{id}", requireLogin(http.HandlerFunc(h.handleFaveUpdate))) mux.Handle("DELETE /faves/{id}", requireLogin(http.HandlerFunc(h.handleFaveDelete))) // Tags. mux.HandleFunc("GET /tags/search", h.handleTagSearch) mux.HandleFunc("GET /tags/{name}", h.handleTagBrowse) // Profiles. mux.HandleFunc("GET /u/{username}", h.handlePublicProfile) // User settings (authenticated). mux.Handle("GET /settings", requireLogin(http.HandlerFunc(h.handleSettingsGet))) mux.Handle("POST /settings", requireLogin(http.HandlerFunc(h.handleSettingsPost))) mux.Handle("POST /settings/avatar", requireLogin(http.HandlerFunc(h.handleAvatarPost))) mux.Handle("POST /settings/password", requireLogin(http.HandlerFunc(h.handleSettingsPasswordPost))) return mux }