| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- 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 <token>
- 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 ""
- }
|