package api import ( "context" "encoding/json" "net/http" "strings" "time" "debtik/internal/auth" ) type contextKey string const ( sessionContextKey contextKey = "auth_session" CookieSessionName = "debtik_session" ) func getSessionFromContext(ctx context.Context) *auth.Session { if val := ctx.Value(sessionContextKey); val != nil { if s, ok := val.(*auth.Session); ok { return s } } return nil } func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { var req auth.LoginRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "Invalid request payload") return } req.Username = strings.TrimSpace(req.Username) if req.Username == "" || req.Password == "" { writeError(w, http.StatusBadRequest, "Username and password are required") return } sess, err := s.authMgr.Authenticate(req.Username, req.Password) if err != nil { if err == auth.ErrUserDisabled { writeError(w, http.StatusForbidden, "User account is disabled") return } writeError(w, http.StatusUnauthorized, "Invalid username or password") return } // Set HttpOnly session cookie http.SetCookie(w, &http.Cookie{ Name: CookieSessionName, Value: sess.Token, Path: "/", Expires: sess.ExpiresAt, HttpOnly: true, SameSite: http.SameSiteLaxMode, }) pubUser, _ := s.authMgr.GetPublicUser(sess.Username) var userInfo auth.PublicUser if pubUser != nil { userInfo = *pubUser } else { userInfo = auth.PublicUser{Username: sess.Username, Role: sess.Role} } writeJSON(w, http.StatusOK, auth.LoginResponse{ Success: true, Token: sess.Token, User: userInfo, Message: "Authentication successful", }) } func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) { sess := getSessionFromContext(r.Context()) if sess != nil { s.authMgr.RevokeSession(sess.Token) } else { // Also check cookie/header if not in context token := extractToken(r) if token != "" { s.authMgr.RevokeSession(token) } } // Expire cookie http.SetCookie(w, &http.Cookie{ Name: CookieSessionName, Value: "", Path: "/", Expires: time.Unix(0, 0), HttpOnly: true, SameSite: http.SameSiteLaxMode, }) writeJSON(w, http.StatusOK, map[string]any{ "success": true, "message": "Logged out successfully", }) } func (s *Server) handleGetMe(w http.ResponseWriter, r *http.Request) { sess := getSessionFromContext(r.Context()) if sess == nil { writeError(w, http.StatusUnauthorized, "Not authenticated") return } pubUser, err := s.authMgr.GetPublicUser(sess.Username) if err != nil { writeError(w, http.StatusNotFound, "User not found") return } writeJSON(w, http.StatusOK, pubUser) } func (s *Server) handleChangePassword(w http.ResponseWriter, r *http.Request) { sess := getSessionFromContext(r.Context()) if sess == nil { writeError(w, http.StatusUnauthorized, "Not authenticated") return } var req auth.ChangePasswordRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "Invalid request payload") return } if req.NewPassword == "" { writeError(w, http.StatusBadRequest, "New password is required") return } if err := s.authMgr.ChangePassword(sess.Username, req.OldPassword, req.NewPassword, false); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]any{ "success": true, "message": "Password changed successfully", }) } // ================= User Management Endpoints (Admin Only) ================= func (s *Server) handleListUsers(w http.ResponseWriter, r *http.Request) { users := s.authMgr.ListUsers() writeJSON(w, http.StatusOK, users) } func (s *Server) handleCreateUser(w http.ResponseWriter, r *http.Request) { var req auth.CreateUserRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "Invalid request payload") return } req.Username = strings.TrimSpace(req.Username) if err := s.authMgr.CreateUser(req); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } pubUser, _ := s.authMgr.GetPublicUser(req.Username) writeJSON(w, http.StatusCreated, pubUser) } func (s *Server) handleUpdateUser(w http.ResponseWriter, r *http.Request) { username := r.PathValue("username") if username == "" { writeError(w, http.StatusBadRequest, "Username is required") return } var req auth.UpdateUserRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "Invalid request payload") return } if err := s.authMgr.UpdateUser(username, req); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } pubUser, _ := s.authMgr.GetPublicUser(username) writeJSON(w, http.StatusOK, pubUser) } func (s *Server) handleDeleteUser(w http.ResponseWriter, r *http.Request) { username := r.PathValue("username") if username == "" { writeError(w, http.StatusBadRequest, "Username is required") return } sess := getSessionFromContext(r.Context()) actor := "" if sess != nil { actor = sess.Username } if err := s.authMgr.DeleteUser(username, actor); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]any{ "success": true, "message": "User " + username + " deleted successfully", }) } func (s *Server) handleAdminResetPassword(w http.ResponseWriter, r *http.Request) { username := r.PathValue("username") if username == "" { writeError(w, http.StatusBadRequest, "Username is required") return } var req auth.AdminResetPasswordRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "Invalid request payload") return } if req.NewPassword == "" { writeError(w, http.StatusBadRequest, "New password cannot be empty") return } if err := s.authMgr.ChangePassword(username, "", req.NewPassword, true); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]any{ "success": true, "message": "Password for user " + username + " reset successfully", }) } func extractToken(r *http.Request) string { // 1. Authorization header: Bearer authHeader := r.Header.Get("Authorization") if strings.HasPrefix(strings.ToLower(authHeader), "bearer ") { return strings.TrimSpace(authHeader[7:]) } // 2. Cookie: debtik_session if cookie, err := r.Cookie(CookieSessionName); err == nil { return strings.TrimSpace(cookie.Value) } // 3. Fallback query param for SSE or download links if q := r.URL.Query().Get("token"); q != "" { return strings.TrimSpace(q) } return "" }