// handlers/user.go package handlers import ( "bytes" "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "strings" "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("/avatars/{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), ) // Валидация if filename == "" || strings.Contains(filename, "..") || strings.Contains(filename, "/") { utils.RespondWithError(w, http.StatusBadRequest, "Invalid filename") return } avatarPath := filepath.Join("./uploads/avatars", filename) // Проверяем существование файла fileInfo, err := os.Stat(avatarPath) if os.IsNotExist(err) { h.logger.Warn("avatar file not found", zap.String("path", avatarPath)) utils.RespondWithError(w, http.StatusNotFound, "Avatar not found") return } if err != nil { h.logger.Error("failed to access file", zap.Error(err)) utils.RespondWithError(w, http.StatusInternalServerError, "File access error") return } // Открываем файл file, err := os.Open(avatarPath) if err != nil { h.logger.Error("failed to open file", zap.Error(err)) utils.RespondWithError(w, http.StatusInternalServerError, "File open error") return } defer file.Close() // Определяем Content-Type ext := strings.ToLower(filepath.Ext(filename)) contentType := "application/octet-stream" switch ext { case ".png": contentType = "image/png" case ".jpg", ".jpeg": contentType = "image/jpeg" case ".gif": contentType = "image/gif" case ".webp": contentType = "image/webp" } // ✅ Устанавливаем ВСЕ заголовки w.Header().Set("Content-Type", contentType) w.Header().Set("Content-Length", fmt.Sprintf("%d", fileInfo.Size())) w.Header().Set("Cache-Control", "public, max-age=31536000") // 1 год w.Header().Set("Last-Modified", fileInfo.ModTime().Format(http.TimeFormat)) // Копируем файл в ResponseWriter _, err = io.Copy(w, file) if err != nil { h.logger.Error("failed to send file", zap.Error(err)) // Не отправляем ошибку - соединение уже испорчено return } h.logger.Info("avatar served successfully", zap.String("filename", filename), zap.String("content_type", contentType), zap.Int64("file_size", fileInfo.Size()), ) }