// Package handlers package handlers import ( "encoding/json" "errors" "net/http" "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 AuthService } func NewAuthHandler(authSvc AuthService) *AuthHandler { return &AuthHandler{authSvc: authSvc} } 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) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestBodySize) 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 } setRefreshTokenCookie(w, refreshToken, r) 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) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestBodySize) 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 } setRefreshTokenCookie(w, refreshToken, r) 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, r) writeError(w, http.StatusUnauthorized, "token reused, session revoked", err) return } h.clearRefreshTokenCookie(w, r) writeError(w, http.StatusUnauthorized, "invalid or expired refresh token", err) return } setRefreshTokenCookie(w, newRefreshToken, r) 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, r) 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 } user, err := h.authSvc.GetUser(r.Context(), userID) if err != nil { writeError(w, http.StatusInternalServerError, "failed to get user", err) return } if user == nil { writeError(w, http.StatusUnauthorized, "user not found", nil) return } writeJSON(w, http.StatusOK, user) } func (h *AuthHandler) clearRefreshTokenCookie(w http.ResponseWriter, r *http.Request) { secure := isSecure(r) sameSite := http.SameSiteLaxMode if secure { sameSite = http.SameSiteStrictMode } http.SetCookie(w, &http.Cookie{ Name: refreshTokenCookieName, Value: "", Path: "/", HttpOnly: true, Secure: secure, SameSite: sameSite, MaxAge: -1, }) } 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 } }