summaryrefslogtreecommitdiff
path: root/internal/api/handlers_auth.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-01 21:10:43 +0300
committerPaul Buetow <paul@buetow.org>2026-05-01 21:10:43 +0300
commitaf29deb33ee25800976b7122236bf7895a5ff39e (patch)
treeb138a6dfe2bb9636ff1c58ff8d255223cd145901 /internal/api/handlers_auth.go
parent74c2e0982f336ab135d8b63d26fa49bcd58e57a9 (diff)
refactor(api): split monolithic handlers.go into domain-specific files (task 3)
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.
Diffstat (limited to 'internal/api/handlers_auth.go')
-rw-r--r--internal/api/handlers_auth.go148
1 files changed, 148 insertions, 0 deletions
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),
+ })
+}