auth.go 6.0 KB

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