package handlers import ( "context" "crypto/rand" "encoding/base64" "encoding/json" "net/http" "time" "golang.org/x/crypto/bcrypt" "lookbook/internal/data/admin" "lookbook/internal/data/session" ) const sessionDuration = 30 * 24 * time.Hour // 30 days type loginRequest struct { Password string `json:"password"` } type loginResponse struct { FirstTime bool `json:"firstTime,omitempty"` Error string `json:"error,omitempty"` } // HandleLogin handles POST /api/auth/login // If no password is set, it sets the password. Otherwise, it verifies the password. func HandleLogin(rc *RequestContext, w http.ResponseWriter, r *http.Request) error { var req loginRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { return writeJSON(w, http.StatusBadRequest, loginResponse{Error: "invalid request"}) } if req.Password == "" { return writeJSON(w, http.StatusBadRequest, loginResponse{Error: "password required"}) } ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel() adm, err := admin.QGet(ctx, rc.DB) if err != nil { return err } firstTime := adm.PasswordHash == nil if firstTime { // First login: set the password hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { return err } if err := admin.QSetPassword(ctx, rc.DB, hash); err != nil { return err } } else { // Verify password if err := bcrypt.CompareHashAndPassword(adm.PasswordHash, []byte(req.Password)); err != nil { return writeJSON(w, http.StatusUnauthorized, loginResponse{Error: "invalid password"}) } } // Create session sessionID, err := generateSessionID() if err != nil { return err } expiresAt := time.Now().Add(sessionDuration) if _, err := session.QCreate(ctx, rc.DB, sessionID, expiresAt); err != nil { return err } // Set cookie http.SetCookie(w, &http.Cookie{ Name: "session_id", Value: sessionID, Path: "/", Expires: expiresAt, HttpOnly: true, SameSite: http.SameSiteLaxMode, Secure: r.TLS != nil, }) return writeJSON(w, http.StatusOK, loginResponse{FirstTime: firstTime}) } // HandleLogout handles POST /api/auth/logout func HandleLogout(rc *RequestContext, w http.ResponseWriter, r *http.Request) error { cookie, err := r.Cookie("session_id") if err == nil { ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel() session.QDelete(ctx, rc.DB, cookie.Value) } // Clear cookie http.SetCookie(w, &http.Cookie{ Name: "session_id", Value: "", Path: "/", MaxAge: -1, HttpOnly: true, SameSite: http.SameSiteLaxMode, }) return writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) } // HandleAuthStatus handles GET /api/auth/status func HandleAuthStatus(rc *RequestContext, w http.ResponseWriter, r *http.Request) error { ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel() hasPassword, err := admin.QHasPassword(ctx, rc.DB) if err != nil { return err } return writeJSON(w, http.StatusOK, map[string]any{ "isAdmin": rc.IsAdmin, "passwordSet": hasPassword, }) } func generateSessionID() (string, error) { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { return "", err } return base64.URLEncoding.EncodeToString(b), nil } func writeJSON(w http.ResponseWriter, status int, v any) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) return json.NewEncoder(w).Encode(v) }