auth.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. // Package handlers
  2. package handlers
  3. import (
  4. "encoding/json"
  5. "errors"
  6. "net/http"
  7. "gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/middleware"
  8. "gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/services"
  9. "gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/validator"
  10. )
  11. type AuthHandler struct {
  12. authSvc AuthService
  13. }
  14. func NewAuthHandler(authSvc AuthService) *AuthHandler {
  15. return &AuthHandler{authSvc: authSvc}
  16. }
  17. type registerRequest struct {
  18. Email string `json:"email" validate:"required,email,max=255"`
  19. Password string `json:"password" validate:"required,min=8,max=72"`
  20. Role string `json:"role" validate:"omitempty,user_role"`
  21. Name string `json:"name" validate:"omitempty,max=255"`
  22. }
  23. func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
  24. r.Body = http.MaxBytesReader(w, r.Body, maxRequestBodySize)
  25. var req registerRequest
  26. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  27. writeError(w, http.StatusBadRequest, "invalid request body", err)
  28. return
  29. }
  30. if err := validator.Validate(req); err != nil {
  31. writeValidationError(w, err)
  32. return
  33. }
  34. if req.Role == "" {
  35. req.Role = "customer"
  36. }
  37. result, refreshToken, err := h.authSvc.Register(r.Context(), services.RegisterInput{
  38. Email: req.Email,
  39. Password: req.Password,
  40. Role: req.Role,
  41. Name: req.Name,
  42. })
  43. if err != nil {
  44. if errors.Is(err, services.ErrEmailExists) {
  45. writeError(w, http.StatusConflict, "email already exists", err)
  46. return
  47. }
  48. writeError(w, http.StatusInternalServerError, "registration failed", err)
  49. return
  50. }
  51. setRefreshTokenCookie(w, refreshToken, r)
  52. writeJSON(w, http.StatusCreated, result)
  53. }
  54. type loginRequest struct {
  55. Email string `json:"email" validate:"required,email,max=255"`
  56. Password string `json:"password" validate:"required,max=72"`
  57. }
  58. func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
  59. r.Body = http.MaxBytesReader(w, r.Body, maxRequestBodySize)
  60. var req loginRequest
  61. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  62. writeError(w, http.StatusBadRequest, "invalid request body", err)
  63. return
  64. }
  65. if err := validator.Validate(req); err != nil {
  66. writeValidationError(w, err)
  67. return
  68. }
  69. result, refreshToken, err := h.authSvc.Login(r.Context(), services.LoginInput{
  70. Email: req.Email,
  71. Password: req.Password,
  72. })
  73. if err != nil {
  74. if errors.Is(err, services.ErrInvalidCreds) {
  75. writeError(w, http.StatusUnauthorized, "invalid email or password", err)
  76. return
  77. }
  78. if errors.Is(err, services.ErrUserBanned) {
  79. writeError(w, http.StatusForbidden, "account is banned", err)
  80. return
  81. }
  82. writeError(w, http.StatusInternalServerError, "login failed", err)
  83. return
  84. }
  85. setRefreshTokenCookie(w, refreshToken, r)
  86. writeJSON(w, http.StatusOK, result)
  87. }
  88. func (h *AuthHandler) Refresh(w http.ResponseWriter, r *http.Request) {
  89. refreshToken := getRefreshTokenFromCookie(r)
  90. if refreshToken == "" {
  91. writeError(w, http.StatusBadRequest, "refresh token required", nil)
  92. return
  93. }
  94. result, newRefreshToken, err := h.authSvc.RefreshSession(r.Context(), refreshToken)
  95. if err != nil {
  96. if errors.Is(err, services.ErrTokenReused) {
  97. h.clearRefreshTokenCookie(w, r)
  98. writeError(w, http.StatusUnauthorized, "token reused, session revoked", err)
  99. return
  100. }
  101. h.clearRefreshTokenCookie(w, r)
  102. writeError(w, http.StatusUnauthorized, "invalid or expired refresh token", err)
  103. return
  104. }
  105. setRefreshTokenCookie(w, newRefreshToken, r)
  106. writeJSON(w, http.StatusOK, result)
  107. }
  108. func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
  109. refreshToken := getRefreshTokenFromCookie(r)
  110. if refreshToken != "" {
  111. _ = h.authSvc.RevokeSession(r.Context(), refreshToken)
  112. }
  113. h.clearRefreshTokenCookie(w, r)
  114. w.WriteHeader(http.StatusNoContent)
  115. }
  116. func (h *AuthHandler) Me(w http.ResponseWriter, r *http.Request) {
  117. userID := middleware.GetUserID(r.Context())
  118. if userID == "" {
  119. writeError(w, http.StatusUnauthorized, "not authenticated", nil)
  120. return
  121. }
  122. user, err := h.authSvc.GetUser(r.Context(), userID)
  123. if err != nil {
  124. writeError(w, http.StatusInternalServerError, "failed to get user", err)
  125. return
  126. }
  127. if user == nil {
  128. writeError(w, http.StatusUnauthorized, "user not found", nil)
  129. return
  130. }
  131. writeJSON(w, http.StatusOK, user)
  132. }
  133. func (h *AuthHandler) clearRefreshTokenCookie(w http.ResponseWriter, r *http.Request) {
  134. secure := isSecure(r)
  135. sameSite := http.SameSiteLaxMode
  136. if secure {
  137. sameSite = http.SameSiteStrictMode
  138. }
  139. http.SetCookie(w, &http.Cookie{
  140. Name: refreshTokenCookieName,
  141. Value: "",
  142. Path: "/",
  143. HttpOnly: true,
  144. Secure: secure,
  145. SameSite: sameSite,
  146. MaxAge: -1,
  147. })
  148. http.SetCookie(w, &http.Cookie{
  149. Name: "session",
  150. Value: "",
  151. Path: "/",
  152. HttpOnly: false,
  153. Secure: secure,
  154. SameSite: sameSite,
  155. MaxAge: -1,
  156. })
  157. }
  158. func validationMessage(tag, param string) string {
  159. switch tag {
  160. case "required":
  161. return "field is required"
  162. case "email":
  163. return "invalid email format"
  164. case "min":
  165. return "value too short (min " + param + ")"
  166. case "max":
  167. return "value too long (max " + param + ")"
  168. case "user_role":
  169. return "invalid role (customer, landlord, executor, moderator, superadmin)"
  170. case "latitude":
  171. return "latitude must be between -90 and 90"
  172. case "longitude":
  173. return "longitude must be between -180 and 180"
  174. case "place_type":
  175. return "type must be 'place' or 'studio'"
  176. case "currency":
  177. return "currency must be 3-letter ISO code"
  178. case "uuid":
  179. return "invalid UUID format"
  180. case "slug":
  181. return "invalid slug format (lowercase, numbers, hyphens only)"
  182. default:
  183. return "validation failed: " + tag
  184. }
  185. }