// handlers/user.go package handlers import ( "bytes" "encoding/json" "io" "net/http" "os" "path/filepath" "time" "api_bb/internal/models" "api_bb/internal/service" "api_bb/pkg/logger" "api_bb/pkg/middleware" "api_bb/pkg/utils" "github.com/go-chi/chi/v5" "go.uber.org/zap" ) type UserHandler struct { logger logger.LoggerInterface userService service.UserService } func NewUserHandler(userService service.UserService) *UserHandler { return &UserHandler{ logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "user"))), userService: userService, } } func (h *UserHandler) Routes() chi.Router { r := chi.NewRouter() r.Get("/profile", h.GetProfile) r.Post("/editProfile", h.UpdateProfile) r.Get("/avatar/{filename}", h.ServeAvatar) return r } type UserResponse struct { ID uint `json:"id"` Email string `json:"email"` FirstName string `json:"firstName"` LastName string `json:"lastName"` Avatar string `json:"avatar"` Phone string `json:"phone"` Experience string `json:"experience"` Goals string `json:"goals"` Newsletter bool `json:"newsletter"` Role string `json:"role"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } func (h *UserHandler) GetProfile(w http.ResponseWriter, r *http.Request) { h.logger.Info("handling get profile request", zap.String("method", r.Method), zap.String("path", r.URL.Path), zap.String("remote_addr", r.RemoteAddr), ) user, ok := middleware.GetUserFromContext(r.Context()) if !ok { h.logger.Warn("get profile failed - authentication required") utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required") return } h.logger.Info("profile retrieved successfully", zap.Uint("user_id", user.ID), zap.String("email", user.Email), zap.String("avatar", user.Avatar), ) utils.RespondWithJSON(w, http.StatusOK, toUserResponse(user)) } type UpdateProfileRequest struct { FirstName string `json:"firstName"` LastName string `json:"lastName"` Phone string `json:"phone"` Experience string `json:"experience"` Goals string `json:"goals"` Newsletter bool `json:"newsletter"` } func (h *UserHandler) UpdateProfile(w http.ResponseWriter, r *http.Request) { h.logger.Info("handling update profile request", zap.String("method", r.Method), zap.String("path", r.URL.Path), zap.String("remote_addr", r.RemoteAddr), ) // Логируем тело запроса для отладки bodyBytes, err := io.ReadAll(r.Body) if err != nil { h.logger.Error("failed to read request body", zap.Error(err)) utils.RespondWithError(w, http.StatusBadRequest, "Failed to read request body: "+err.Error()) return } // Восстанавливаем тело для дальнейшего использования r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) h.logger.Debug("raw request body", zap.String("body", string(bodyBytes))) // Получаем пользователя из контекста currentUser, ok := middleware.GetUserFromContext(r.Context()) if !ok { h.logger.Warn("update profile failed - authentication required") utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required") return } var req UpdateProfileRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.logger.Error("failed to decode JSON payload", zap.Error(err)) utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error()) return } // Валидация обязательных полей if req.FirstName == "" { h.logger.Warn("update profile failed - first name required") utils.RespondWithError(w, http.StatusBadRequest, "First name is required") return } if req.LastName == "" { h.logger.Warn("update profile failed - last name required") utils.RespondWithError(w, http.StatusBadRequest, "Last name is required") return } h.logger.Info("updating user profile", zap.Uint("user_id", currentUser.ID), zap.String("first_name", req.FirstName), zap.String("last_name", req.LastName), zap.String("experience", req.Experience), zap.String("goals", req.Goals), zap.Bool("newsletter", req.Newsletter), ) // Обновляем данные пользователя updatedUser := &models.User{ ID: currentUser.ID, FirstName: req.FirstName, LastName: req.LastName, Phone: req.Phone, Experience: req.Experience, Goals: req.Goals, Newsletter: req.Newsletter, UpdatedAt: time.Now(), } // Сохраняем обновленные данные if err := h.userService.UpdateProfile(updatedUser); err != nil { h.logger.Error("failed to update profile in service", zap.Uint("user_id", currentUser.ID), zap.Error(err), ) utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update profile: "+err.Error()) return } h.logger.Info("profile updated successfully", zap.Uint("user_id", currentUser.ID), ) utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{ "message": "Profile updated successfully", "user": toUserResponse(updatedUser), }) } func (h *UserHandler) ServeAvatar(w http.ResponseWriter, r *http.Request) { filename := chi.URLParam(r, "filename") h.logger.Info("handling serve avatar request", zap.String("method", r.Method), zap.String("filename", filename), zap.String("remote_addr", r.RemoteAddr), ) // Используем http.ServeFile для эффективной отдачи файла avatarPath := filepath.Join("./uploads/avatars", filename) // Проверяем существование файла if _, err := os.Stat(avatarPath); os.IsNotExist(err) { h.logger.Warn("avatar file not found", zap.String("path", avatarPath)) utils.RespondWithError(w, http.StatusNotFound, "Avatar not found") return } // Устанавливаем заголовки кэширования w.Header().Set("Cache-Control", "public, max-age=31536000") w.Header().Set("Expires", time.Now().Add(365*24*time.Hour).Format(http.TimeFormat)) // Отдаем файл http.ServeFile(w, r, avatarPath) h.logger.Info("avatar served via ServeFile", zap.String("filename", filename), ) }