auth_handlers.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. package api
  2. import (
  3. "context"
  4. "encoding/json"
  5. "net/http"
  6. "strings"
  7. "time"
  8. "proxmox-router/internal/auth"
  9. )
  10. type contextKey string
  11. const (
  12. sessionContextKey contextKey = "auth_session"
  13. CookieSessionName = "debtik_session"
  14. )
  15. func getSessionFromContext(ctx context.Context) *auth.Session {
  16. if val := ctx.Value(sessionContextKey); val != nil {
  17. if s, ok := val.(*auth.Session); ok {
  18. return s
  19. }
  20. }
  21. return nil
  22. }
  23. func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
  24. var req auth.LoginRequest
  25. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  26. writeError(w, http.StatusBadRequest, "Invalid request payload")
  27. return
  28. }
  29. req.Username = strings.TrimSpace(req.Username)
  30. if req.Username == "" || req.Password == "" {
  31. writeError(w, http.StatusBadRequest, "Username and password are required")
  32. return
  33. }
  34. sess, err := s.authMgr.Authenticate(req.Username, req.Password)
  35. if err != nil {
  36. if err == auth.ErrUserDisabled {
  37. writeError(w, http.StatusForbidden, "User account is disabled")
  38. return
  39. }
  40. writeError(w, http.StatusUnauthorized, "Invalid username or password")
  41. return
  42. }
  43. // Set HttpOnly session cookie
  44. http.SetCookie(w, &http.Cookie{
  45. Name: CookieSessionName,
  46. Value: sess.Token,
  47. Path: "/",
  48. Expires: sess.ExpiresAt,
  49. HttpOnly: true,
  50. SameSite: http.SameSiteLaxMode,
  51. })
  52. pubUser, _ := s.authMgr.GetPublicUser(sess.Username)
  53. var userInfo auth.PublicUser
  54. if pubUser != nil {
  55. userInfo = *pubUser
  56. } else {
  57. userInfo = auth.PublicUser{Username: sess.Username, Role: sess.Role}
  58. }
  59. writeJSON(w, http.StatusOK, auth.LoginResponse{
  60. Success: true,
  61. Token: sess.Token,
  62. User: userInfo,
  63. Message: "Authentication successful",
  64. })
  65. }
  66. func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
  67. sess := getSessionFromContext(r.Context())
  68. if sess != nil {
  69. s.authMgr.RevokeSession(sess.Token)
  70. } else {
  71. // Also check cookie/header if not in context
  72. token := extractToken(r)
  73. if token != "" {
  74. s.authMgr.RevokeSession(token)
  75. }
  76. }
  77. // Expire cookie
  78. http.SetCookie(w, &http.Cookie{
  79. Name: CookieSessionName,
  80. Value: "",
  81. Path: "/",
  82. Expires: time.Unix(0, 0),
  83. HttpOnly: true,
  84. SameSite: http.SameSiteLaxMode,
  85. })
  86. writeJSON(w, http.StatusOK, map[string]any{
  87. "success": true,
  88. "message": "Logged out successfully",
  89. })
  90. }
  91. func (s *Server) handleGetMe(w http.ResponseWriter, r *http.Request) {
  92. sess := getSessionFromContext(r.Context())
  93. if sess == nil {
  94. writeError(w, http.StatusUnauthorized, "Not authenticated")
  95. return
  96. }
  97. pubUser, err := s.authMgr.GetPublicUser(sess.Username)
  98. if err != nil {
  99. writeError(w, http.StatusNotFound, "User not found")
  100. return
  101. }
  102. writeJSON(w, http.StatusOK, pubUser)
  103. }
  104. func (s *Server) handleChangePassword(w http.ResponseWriter, r *http.Request) {
  105. sess := getSessionFromContext(r.Context())
  106. if sess == nil {
  107. writeError(w, http.StatusUnauthorized, "Not authenticated")
  108. return
  109. }
  110. var req auth.ChangePasswordRequest
  111. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  112. writeError(w, http.StatusBadRequest, "Invalid request payload")
  113. return
  114. }
  115. if req.NewPassword == "" {
  116. writeError(w, http.StatusBadRequest, "New password is required")
  117. return
  118. }
  119. if err := s.authMgr.ChangePassword(sess.Username, req.OldPassword, req.NewPassword, false); err != nil {
  120. writeError(w, http.StatusBadRequest, err.Error())
  121. return
  122. }
  123. writeJSON(w, http.StatusOK, map[string]any{
  124. "success": true,
  125. "message": "Password changed successfully",
  126. })
  127. }
  128. // ================= User Management Endpoints (Admin Only) =================
  129. func (s *Server) handleListUsers(w http.ResponseWriter, r *http.Request) {
  130. users := s.authMgr.ListUsers()
  131. writeJSON(w, http.StatusOK, users)
  132. }
  133. func (s *Server) handleCreateUser(w http.ResponseWriter, r *http.Request) {
  134. var req auth.CreateUserRequest
  135. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  136. writeError(w, http.StatusBadRequest, "Invalid request payload")
  137. return
  138. }
  139. req.Username = strings.TrimSpace(req.Username)
  140. if err := s.authMgr.CreateUser(req); err != nil {
  141. writeError(w, http.StatusBadRequest, err.Error())
  142. return
  143. }
  144. pubUser, _ := s.authMgr.GetPublicUser(req.Username)
  145. writeJSON(w, http.StatusCreated, pubUser)
  146. }
  147. func (s *Server) handleUpdateUser(w http.ResponseWriter, r *http.Request) {
  148. username := r.PathValue("username")
  149. if username == "" {
  150. writeError(w, http.StatusBadRequest, "Username is required")
  151. return
  152. }
  153. var req auth.UpdateUserRequest
  154. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  155. writeError(w, http.StatusBadRequest, "Invalid request payload")
  156. return
  157. }
  158. if err := s.authMgr.UpdateUser(username, req); err != nil {
  159. writeError(w, http.StatusBadRequest, err.Error())
  160. return
  161. }
  162. pubUser, _ := s.authMgr.GetPublicUser(username)
  163. writeJSON(w, http.StatusOK, pubUser)
  164. }
  165. func (s *Server) handleDeleteUser(w http.ResponseWriter, r *http.Request) {
  166. username := r.PathValue("username")
  167. if username == "" {
  168. writeError(w, http.StatusBadRequest, "Username is required")
  169. return
  170. }
  171. sess := getSessionFromContext(r.Context())
  172. actor := ""
  173. if sess != nil {
  174. actor = sess.Username
  175. }
  176. if err := s.authMgr.DeleteUser(username, actor); err != nil {
  177. writeError(w, http.StatusBadRequest, err.Error())
  178. return
  179. }
  180. writeJSON(w, http.StatusOK, map[string]any{
  181. "success": true,
  182. "message": "User " + username + " deleted successfully",
  183. })
  184. }
  185. func (s *Server) handleAdminResetPassword(w http.ResponseWriter, r *http.Request) {
  186. username := r.PathValue("username")
  187. if username == "" {
  188. writeError(w, http.StatusBadRequest, "Username is required")
  189. return
  190. }
  191. var req auth.AdminResetPasswordRequest
  192. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  193. writeError(w, http.StatusBadRequest, "Invalid request payload")
  194. return
  195. }
  196. if req.NewPassword == "" {
  197. writeError(w, http.StatusBadRequest, "New password cannot be empty")
  198. return
  199. }
  200. if err := s.authMgr.ChangePassword(username, "", req.NewPassword, true); err != nil {
  201. writeError(w, http.StatusBadRequest, err.Error())
  202. return
  203. }
  204. writeJSON(w, http.StatusOK, map[string]any{
  205. "success": true,
  206. "message": "Password for user " + username + " reset successfully",
  207. })
  208. }
  209. func extractToken(r *http.Request) string {
  210. // 1. Authorization header: Bearer <token>
  211. authHeader := r.Header.Get("Authorization")
  212. if strings.HasPrefix(strings.ToLower(authHeader), "bearer ") {
  213. return strings.TrimSpace(authHeader[7:])
  214. }
  215. // 2. Cookie: debtik_session
  216. if cookie, err := r.Cookie(CookieSessionName); err == nil {
  217. return strings.TrimSpace(cookie.Value)
  218. }
  219. // 3. Fallback query param for SSE or download links
  220. if q := r.URL.Query().Get("token"); q != "" {
  221. return strings.TrimSpace(q)
  222. }
  223. return ""
  224. }