From af29deb33ee25800976b7122236bf7895a5ff39e Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Fri, 1 May 2026 21:10:43 +0300 Subject: refactor(api): split monolithic handlers.go into domain-specific files (task 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split internal/api/handlers.go (1045 lines) to improve KISS/SRP: - handlers_auth.go – bootstrap, login, logout, health, session cookies - handlers_media.go – sets, media CRUD, tags, favorites, notes, progress - handlers_share.go – create/list/revoke shares, share page, share stream - handlers_admin.go – trash, rescan, users, permissions - handlers_file.go – stream, download, thumbnail, regenerate thumbnail Shared helpers (writeJSON, readJSON, pathID, serveFileResult, mimeTypeForFilename, etc.) remain in handlers.go. All tests pass: go test ./... -race -cover. --- internal/api/handlers_auth.go | 148 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 internal/api/handlers_auth.go (limited to 'internal/api/handlers_auth.go') diff --git a/internal/api/handlers_auth.go b/internal/api/handlers_auth.go new file mode 100644 index 0000000..96991e6 --- /dev/null +++ b/internal/api/handlers_auth.go @@ -0,0 +1,148 @@ +package api + +import ( + "net/http" + "time" + + "codeberg.org/snonux/player/internal/model" +) + +type bootstrapRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type loginRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +// ------------------------------------------------------------------ +// Bootstrap & Auth +// ------------------------------------------------------------------ + +func (s *Server) handleBootstrap(w http.ResponseWriter, r *http.Request) { + var req bootstrapRequest + if err := readJSON(r, &req); err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"}) + return + } + if req.Username == "" || req.Password == "" { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "username and password required"}) + return + } + + ctx := r.Context() + count, err := s.store.CountUsers(ctx) + if err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal server error"}) + return + } + if count > 0 { + writeJSON(w, http.StatusForbidden, map[string]string{"error": "bootstrap already complete"}) + return + } + + hash, err := s.hasher.Hash(req.Password) + if err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal server error"}) + return + } + + user := &model.User{Username: req.Username, PasswordHash: hash, IsAdmin: true, CreatedAt: time.Now()} + id, err := s.store.CreateUser(ctx, user) + if err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal server error"}) + return + } + user.ID = id + + sessID, err := s.sm.CreateSession(ctx, id) + if err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal server error"}) + return + } + s.setSessionCookie(w, sessID) + writeJSON(w, http.StatusOK, map[string]interface{}{"id": user.ID, "username": user.Username, "is_admin": user.IsAdmin}) +} + +func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { + var req loginRequest + if err := readJSON(r, &req); err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"}) + return + } + if req.Username == "" || req.Password == "" { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "username and password required"}) + return + } + + ctx := r.Context() + user, err := s.store.GetUserByUsername(ctx, req.Username) + if err != nil { + writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid credentials"}) + return + } + if user == nil { + writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid credentials"}) + return + } + if err := s.hasher.Compare(user.PasswordHash, req.Password); err != nil { + writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid credentials"}) + return + } + + sessID, err := s.sm.CreateSession(ctx, user.ID) + if err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal server error"}) + return + } + s.setSessionCookie(w, sessID) + writeJSON(w, http.StatusOK, map[string]interface{}{"id": user.ID, "username": user.Username, "is_admin": user.IsAdmin}) +} + +func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie("session") + if err == nil && cookie.Value != "" { + _ = s.sm.DeleteSession(r.Context(), cookie.Value) + } + s.clearSessionCookie(w) + w.WriteHeader(http.StatusNoContent) +} + +func (s *Server) handleHealthz(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) +} + +func (s *Server) handleReadyz(w http.ResponseWriter, r *http.Request) { + if err := s.pingStore(r.Context()); err != nil { + http.Error(w, "not ready", http.StatusServiceUnavailable) + return + } + w.WriteHeader(http.StatusOK) +} + +func (s *Server) setSessionCookie(w http.ResponseWriter, value string) { + http.SetCookie(w, &http.Cookie{ + Name: "session", + Value: value, + Path: "/", + HttpOnly: true, + Secure: s.cfg.SecureCookies, + SameSite: http.SameSiteStrictMode, + Expires: time.Now().Add(time.Duration(s.cfg.SessionTimeoutHours) * time.Hour), + }) +} + +func (s *Server) clearSessionCookie(w http.ResponseWriter) { + http.SetCookie(w, &http.Cookie{ + Name: "session", + Value: "", + Path: "/", + HttpOnly: true, + Secure: s.cfg.SecureCookies, + SameSite: http.SameSiteStrictMode, + MaxAge: -1, + Expires: time.Unix(0, 0), + }) +} -- cgit v1.2.3