// Package handlers package handlers import ( "errors" "net/http" "time" "gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/middleware" "gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/services" "gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/validator" ) type AuthHandler struct { authSvc *services.AuthService isProd bool } func NewAuthHandler(authSvc *services.AuthService, appEnv string) *AuthHandler { return &AuthHandler{ authSvc: authSvc, isProd: appEnv == "production", } } type registerRequest struct { Email string `json:"email" validate:"required,email,max=255"` Password string `json:"password" validate:"required,min=8,max=72"` Role string `json:"role" validate:"omitempty,user_role"` Name string `json:"name" validate:"omitempty,max=255"` } func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) { var req registerRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body", err) return } if err := validator.Validate(req); err != nil { writeValidationError(w, err) return } if req.Role == "" { req.Role = "customer" } result, refreshToken, err := h.authSvc.Register(r.Context(), services.RegisterInput{ Email: req.Email, Password: req.Password, Role: req.Role, Name: req.Name, }) if err != nil { if errors.Is(err, services.ErrEmailExists) { writeError(w, http.StatusConflict, "email already exists", err) return } writeError(w, http.StatusInternalServerError, "registration failed", err) return } h.setRefreshTokenCookie(w, refreshToken) writeJSON(w, http.StatusCreated, result) } type loginRequest struct { Email string `json:"email" validate:"required,email,max=255"` Password string `json:"password" validate:"required,max=72"` } func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { var req loginRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body", err) return } if err := validator.Validate(req); err != nil { writeValidationError(w, err) return } result, refreshToken, err := h.authSvc.Login(r.Context(), services.LoginInput{ Email: req.Email, Password: req.Password, }) if err != nil { if errors.Is(err, services.ErrInvalidCreds) { writeError(w, http.StatusUnauthorized, "invalid email or password", err) return } if errors.Is(err, services.ErrUserBanned) { writeError(w, http.StatusForbidden, "account is banned", err) return } writeError(w, http.StatusInternalServerError, "login failed", err) return } h.setRefreshTokenCookie(w, refreshToken) writeJSON(w, http.StatusOK, result) } func (h *AuthHandler) Refresh(w http.ResponseWriter, r *http.Request) { refreshToken := getRefreshTokenFromCookie(r) if refreshToken == "" { writeError(w, http.StatusBadRequest, "refresh token required", nil) return } result, newRefreshToken, err := h.authSvc.RefreshSession(r.Context(), refreshToken) if err != nil { if errors.Is(err, services.ErrTokenReused) { h.clearRefreshTokenCookie(w) writeError(w, http.StatusUnauthorized, "token reused, session revoked", err) return } h.clearRefreshTokenCookie(w) writeError(w, http.StatusUnauthorized, "invalid or expired refresh token", err) return } h.setRefreshTokenCookie(w, newRefreshToken) writeJSON(w, http.StatusOK, result) } func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) { refreshToken := getRefreshTokenFromCookie(r) if refreshToken != "" { _ = h.authSvc.RevokeSession(r.Context(), refreshToken) } h.clearRefreshTokenCookie(w) w.WriteHeader(http.StatusNoContent) } func (h *AuthHandler) Me(w http.ResponseWriter, r *http.Request) { userID := middleware.GetUserID(r.Context()) if userID == "" { writeError(w, http.StatusUnauthorized, "not authenticated", nil) return } writeJSON(w, http.StatusOK, map[string]string{"user_id": userID, "role": middleware.GetUserRole(r.Context())}) } const refreshTokenCookieName = "refresh_token" func (h *AuthHandler) setRefreshTokenCookie(w http.ResponseWriter, token string) { cookie := &http.Cookie{ Name: refreshTokenCookieName, Value: token, Path: "/", HttpOnly: true, Secure: h.isProd, SameSite: http.SameSiteLaxMode, MaxAge: int((30 * 24 * time.Hour).Seconds()), } http.SetCookie(w, cookie) } func getRefreshTokenFromCookie(r *http.Request) string { cookie, err := r.Cookie(refreshTokenCookieName) if err != nil { return "" } return cookie.Value } func (h *AuthHandler) clearRefreshTokenCookie(w http.ResponseWriter) { cookie := &http.Cookie{ Name: refreshTokenCookieName, Value: "", Path: "/", HttpOnly: true, Secure: h.isProd, SameSite: http.SameSiteLaxMode, MaxAge: -1, } http.SetCookie(w, cookie) } func validationMessage(tag, param string) string { switch tag { case "required": return "field is required" case "email": return "invalid email format" case "min": return "value too short (min " + param + ")" case "max": return "value too long (max " + param + ")" case "user_role": return "invalid role (customer, landlord, executor, moderator, superadmin)" case "latitude": return "latitude must be between -90 and 90" case "longitude": return "longitude must be between -180 and 180" case "place_type": return "type must be 'place' or 'studio'" case "currency": return "currency must be 3-letter ISO code" case "uuid": return "invalid UUID format" case "slug": return "invalid slug format (lowercase, numbers, hyphens only)" default: return "validation failed: " + tag } }