package handlers import ( "encoding/json" "errors" "net/http" "time" "github.com/photoplaces/backend/internal/middleware" "github.com/photoplaces/backend/internal/services" ) 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"` Password string `json:"password"` Role string `json:"role"` Name string `json:"name"` } 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") return } if req.Email == "" || req.Password == "" { writeError(w, http.StatusBadRequest, "email and password are required") return } validRoles := map[string]bool{"customer": true, "landlord": true, "executor": true} if !validRoles[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") return } writeError(w, http.StatusInternalServerError, err.Error()) return } h.setRefreshTokenCookie(w, refreshToken) writeJSON(w, http.StatusCreated, result) } type loginRequest struct { Email string `json:"email"` Password string `json:"password"` } 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") 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") return } if errors.Is(err, services.ErrUserBanned) { writeError(w, http.StatusForbidden, "account is banned") return } writeError(w, http.StatusInternalServerError, err.Error()) 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") 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") return } h.clearRefreshTokenCookie(w) writeError(w, http.StatusUnauthorized, "invalid or expired refresh token") 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") 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 writeJSON(w http.ResponseWriter, status int, v interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(v) } func writeError(w http.ResponseWriter, status int, msg string) { writeJSON(w, status, map[string]string{"error": msg}) }