auth.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  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. var req registerRequest
  29. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  30. writeError(w, http.StatusBadRequest, "invalid request body", err)
  31. return
  32. }
  33. if err := validator.Validate(req); err != nil {
  34. writeValidationError(w, err)
  35. return
  36. }
  37. if req.Role == "" {
  38. req.Role = "customer"
  39. }
  40. result, refreshToken, err := h.authSvc.Register(r.Context(), services.RegisterInput{
  41. Email: req.Email,
  42. Password: req.Password,
  43. Role: req.Role,
  44. Name: req.Name,
  45. })
  46. if err != nil {
  47. if errors.Is(err, services.ErrEmailExists) {
  48. writeError(w, http.StatusConflict, "email already exists", err)
  49. return
  50. }
  51. writeError(w, http.StatusInternalServerError, "registration failed", err)
  52. return
  53. }
  54. h.setRefreshTokenCookie(w, refreshToken)
  55. writeJSON(w, http.StatusCreated, result)
  56. }
  57. type loginRequest struct {
  58. Email string `json:"email" validate:"required,email,max=255"`
  59. Password string `json:"password" validate:"required,max=72"`
  60. }
  61. func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
  62. var req loginRequest
  63. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  64. writeError(w, http.StatusBadRequest, "invalid request body", err)
  65. return
  66. }
  67. if err := validator.Validate(req); err != nil {
  68. writeValidationError(w, err)
  69. return
  70. }
  71. result, refreshToken, err := h.authSvc.Login(r.Context(), services.LoginInput{
  72. Email: req.Email,
  73. Password: req.Password,
  74. })
  75. if err != nil {
  76. if errors.Is(err, services.ErrInvalidCreds) {
  77. writeError(w, http.StatusUnauthorized, "invalid email or password", err)
  78. return
  79. }
  80. if errors.Is(err, services.ErrUserBanned) {
  81. writeError(w, http.StatusForbidden, "account is banned", err)
  82. return
  83. }
  84. writeError(w, http.StatusInternalServerError, "login failed", err)
  85. return
  86. }
  87. h.setRefreshTokenCookie(w, refreshToken)
  88. writeJSON(w, http.StatusOK, result)
  89. }
  90. func (h *AuthHandler) Refresh(w http.ResponseWriter, r *http.Request) {
  91. refreshToken := getRefreshTokenFromCookie(r)
  92. if refreshToken == "" {
  93. writeError(w, http.StatusBadRequest, "refresh token required", nil)
  94. return
  95. }
  96. result, newRefreshToken, err := h.authSvc.RefreshSession(r.Context(), refreshToken)
  97. if err != nil {
  98. if errors.Is(err, services.ErrTokenReused) {
  99. h.clearRefreshTokenCookie(w)
  100. writeError(w, http.StatusUnauthorized, "token reused, session revoked", err)
  101. return
  102. }
  103. h.clearRefreshTokenCookie(w)
  104. writeError(w, http.StatusUnauthorized, "invalid or expired refresh token", err)
  105. return
  106. }
  107. h.setRefreshTokenCookie(w, newRefreshToken)
  108. writeJSON(w, http.StatusOK, result)
  109. }
  110. func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
  111. refreshToken := getRefreshTokenFromCookie(r)
  112. if refreshToken != "" {
  113. _ = h.authSvc.RevokeSession(r.Context(), refreshToken)
  114. }
  115. h.clearRefreshTokenCookie(w)
  116. w.WriteHeader(http.StatusNoContent)
  117. }
  118. func (h *AuthHandler) Me(w http.ResponseWriter, r *http.Request) {
  119. userID := middleware.GetUserID(r.Context())
  120. if userID == "" {
  121. writeError(w, http.StatusUnauthorized, "not authenticated", nil)
  122. return
  123. }
  124. writeJSON(w, http.StatusOK, map[string]string{"user_id": userID, "role": middleware.GetUserRole(r.Context())})
  125. }
  126. const refreshTokenCookieName = "refresh_token"
  127. func (h *AuthHandler) setRefreshTokenCookie(w http.ResponseWriter, token string) {
  128. cookie := &http.Cookie{
  129. Name: refreshTokenCookieName,
  130. Value: token,
  131. Path: "/",
  132. HttpOnly: true,
  133. Secure: h.isProd,
  134. SameSite: http.SameSiteLaxMode,
  135. MaxAge: int((30 * 24 * time.Hour).Seconds()),
  136. }
  137. http.SetCookie(w, cookie)
  138. }
  139. func getRefreshTokenFromCookie(r *http.Request) string {
  140. cookie, err := r.Cookie(refreshTokenCookieName)
  141. if err != nil {
  142. return ""
  143. }
  144. return cookie.Value
  145. }
  146. func (h *AuthHandler) clearRefreshTokenCookie(w http.ResponseWriter) {
  147. cookie := &http.Cookie{
  148. Name: refreshTokenCookieName,
  149. Value: "",
  150. Path: "/",
  151. HttpOnly: true,
  152. Secure: h.isProd,
  153. SameSite: http.SameSiteLaxMode,
  154. MaxAge: -1,
  155. }
  156. http.SetCookie(w, cookie)
  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. }