// Package handlers package handlers import ( "encoding/json" "errors" "net/http" "strconv" "github.com/go-chi/chi/v5" "golang.org/x/crypto/bcrypt" "gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/middleware" "gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/models" "gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/repository" "gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/validator" ) type UserHandler struct { userRepo UserRepo } func NewUserHandler(userRepo UserRepo) *UserHandler { return &UserHandler{userRepo: userRepo} } func (h *UserHandler) GetProfile(w http.ResponseWriter, r *http.Request) { userID := chi.URLParam(r, "id") if userID == "" { userID = middleware.GetUserID(r.Context()) } user, err := h.userRepo.GetByID(r.Context(), userID) if err != nil { writeError(w, http.StatusInternalServerError, "failed to get user", err) return } if user == nil { writeError(w, http.StatusNotFound, "user not found", nil) return } writeJSON(w, http.StatusOK, user) } type updateMeRequest struct { Name *string `json:"name" validate:"omitempty,max=255"` Phone *string `json:"phone" validate:"omitempty,max=20"` Bio *string `json:"bio" validate:"omitempty,max=2000"` AvatarURL *string `json:"avatar_url" validate:"omitempty,url,max=500"` Country *string `json:"country" validate:"omitempty,len=2"` } func (h *UserHandler) UpdateMe(w http.ResponseWriter, r *http.Request) { userID := middleware.GetUserID(r.Context()) r.Body = http.MaxBytesReader(w, r.Body, maxRequestBodySize) user, err := h.userRepo.GetByID(r.Context(), userID) if err != nil { writeError(w, http.StatusInternalServerError, "failed to get user", err) return } if user == nil { writeError(w, http.StatusNotFound, "user not found", nil) return } var req updateMeRequest 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.Name != nil { user.Name = req.Name } if req.Phone != nil { user.Phone = req.Phone } if req.Bio != nil { user.Bio = req.Bio } if req.AvatarURL != nil { user.AvatarURL = req.AvatarURL } if req.Country != nil { user.Country = req.Country } if err := h.userRepo.Update(r.Context(), user); err != nil { writeError(w, http.StatusInternalServerError, "failed to update user", err) return } writeJSON(w, http.StatusOK, user) } // Admin endpoints type adminCreateUserRequest 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:"required,user_role"` Name string `json:"name" validate:"omitempty,max=255"` } func (h *UserHandler) AdminCreateUser(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestBodySize) var req adminCreateUserRequest 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 } hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { writeError(w, http.StatusInternalServerError, "failed to hash password", err) return } user := &models.User{ Email: req.Email, PasswordHash: string(hash), Role: req.Role, Name: &req.Name, } if err := h.userRepo.Create(r.Context(), user); err != nil { if errors.Is(err, repository.ErrEmailExists) { writeError(w, http.StatusConflict, "email already exists", nil) return } writeError(w, http.StatusInternalServerError, "failed to create user", err) return } writeJSON(w, http.StatusCreated, user) } type adminUpdateUserRequest struct { Role *string `json:"role" validate:"omitempty,user_role"` Status *string `json:"status" validate:"omitempty,user_status"` } func (h *UserHandler) AdminUpdateUser(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") currentUserID := middleware.GetUserID(r.Context()) currentUserRole := middleware.GetUserRole(r.Context()) r.Body = http.MaxBytesReader(w, r.Body, maxRequestBodySize) var req adminUpdateUserRequest 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 != nil { if currentUserRole != "superadmin" { if *req.Role == "superadmin" { writeError(w, http.StatusForbidden, "только superadmin может назначить superadmin", nil) return } } if id == currentUserID { if currentUserRole != "superadmin" { writeError(w, http.StatusForbidden, "вы не можете сменить свою роль", nil) return } count, err := h.userRepo.CountByRole(r.Context(), "superadmin") if err != nil { writeError(w, http.StatusInternalServerError, "failed to count superadmins", err) return } if count <= 1 { writeError(w, http.StatusForbidden, "нельзя сменить роль единственного суперадмина", nil) return } } if err := h.userRepo.UpdateRole(r.Context(), id, *req.Role); err != nil { writeError(w, http.StatusInternalServerError, "failed to update role", err) return } } if req.Status != nil { if err := h.userRepo.UpdateStatus(r.Context(), id, *req.Status); err != nil { writeError(w, http.StatusInternalServerError, "failed to update status", err) return } } user, err := h.userRepo.GetByID(r.Context(), id) if err != nil { writeError(w, http.StatusInternalServerError, "failed to get user", err) return } writeJSON(w, http.StatusOK, user) } func (h *UserHandler) AdminListUsers(w http.ResponseWriter, r *http.Request) { filter := models.UserFilter{ Role: r.URL.Query().Get("role"), Status: r.URL.Query().Get("status"), } if limit := r.URL.Query().Get("limit"); limit != "" { if v, err := strconv.Atoi(limit); err == nil { filter.Limit_ = v } } users, err := h.userRepo.List(r.Context(), filter) if err != nil { writeError(w, http.StatusInternalServerError, "failed to list users", err) return } if users == nil { users = []*models.User{} } writeJSON(w, http.StatusOK, map[string]interface{}{"data": users}) }