// 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))) // Feeds (public, no auth required). mux.HandleFunc("GET /feed.xml", h.handleFeedGlobal) mux.HandleFunc("GET /u/{username}/feed.xml", h.handleFeedUser) mux.HandleFunc("GET /tags/{name}/feed.xml", h.handleFeedTag) // Import/Export (authenticated). mux.Handle("GET /export", requireLogin(http.HandlerFunc(h.handleExportPage))) mux.Handle("GET /export/json", requireLogin(http.HandlerFunc(h.handleExportJSON))) mux.Handle("GET /export/csv", requireLogin(http.HandlerFunc(h.handleExportCSV))) mux.Handle("GET /import", requireLogin(http.HandlerFunc(h.handleImportPage))) mux.Handle("POST /import", requireLogin(http.HandlerFunc(h.handleImportPost))) // Admin panel (requires admin role). admin := func(hf http.HandlerFunc) http.Handler { return requireLogin(middleware.RequireAdmin(http.HandlerFunc(hf))) } mux.Handle("GET /admin", admin(h.handleAdminDashboard)) mux.Handle("GET /admin/users", admin(h.handleAdminUsers)) mux.Handle("POST /admin/users", admin(h.handleAdminCreateUser)) mux.Handle("POST /admin/users/{id}/reset-password", admin(h.handleAdminResetPassword)) mux.Handle("POST /admin/users/{id}/toggle-disabled", admin(h.handleAdminToggleDisabled)) mux.Handle("GET /admin/tags", admin(h.handleAdminTags)) mux.Handle("POST /admin/tags/{id}/rename", admin(h.handleAdminRenameTag)) mux.Handle("POST /admin/tags/{id}/delete", admin(h.handleAdminDeleteTag)) mux.Handle("GET /admin/signup-requests", admin(h.handleAdminSignupRequests)) mux.Handle("POST /admin/signup-requests/{id}", admin(h.handleAdminSignupRequestAction)) mux.Handle("GET /admin/settings", admin(h.handleAdminSettingsGet)) mux.Handle("POST /admin/settings", admin(h.handleAdminSettingsPost)) return mux }