auth.go 5.9 KB

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