// handlers/avatar.go package handlers import ( "net/http" "time" "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 AvatarHandler struct { logger logger.LoggerInterface avatarService service.AvatarService } func NewAvatarHandler(avatarService service.AvatarService) *AvatarHandler { return &AvatarHandler{ logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "avatar"))), avatarService: avatarService, } } func (h *AvatarHandler) Routes() chi.Router { r := chi.NewRouter() h.logger.Debug("Registering avatar routes", zap.String("POST", "/upload"), zap.String("DELETE", "/delete"), zap.String("GET", "/{filename}"), ) // r.Get("/avatar/{filename}", h.ServeAvatar) r.Post("/upload", h.UploadAvatar) r.Delete("/delete", h.DeleteAvatar) r.Get("/{filename}", h.GetAvatar) return r } func (h *AvatarHandler) UploadAvatar(w http.ResponseWriter, r *http.Request) { h.logger.Debug("UploadAvatar START", zap.String("method", r.Method), zap.String("path", r.URL.Path), zap.String("remote_addr", r.RemoteAddr), ) defer func() { h.logger.Debug("UploadAvatar END", zap.String("method", r.Method), zap.String("path", r.URL.Path), ) }() user, ok := middleware.GetUserFromContext(r.Context()) if !ok { h.logger.Warn("UploadAvatar: authentication required") utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required") return } h.logger.Debug("UploadAvatar: user authenticated", zap.Int64("user_id", int64(user.ID)), zap.String("username", user.FirstName+user.LastName), ) // Парсим multipart форму h.logger.Debug("UploadAvatar: parsing multipart form") if err := r.ParseMultipartForm(10 << 20); err != nil { // 10MB limit h.logger.Error("UploadAvatar: failed to parse form", zap.Error(err)) utils.RespondWithError(w, http.StatusBadRequest, "Failed to parse form: "+err.Error()) return } h.logger.Debug("UploadAvatar: getting file from form") file, header, err := r.FormFile("avatar") if err != nil { h.logger.Error("UploadAvatar: failed to get file from form", zap.Error(err)) utils.RespondWithError(w, http.StatusBadRequest, "Failed to get file: "+err.Error()) return } defer file.Close() h.logger.Debug("UploadAvatar: file received", zap.String("filename", header.Filename), zap.Int64("size", header.Size), zap.String("content_type", header.Header.Get("Content-Type")), ) // Проверяем тип файла allowedTypes := map[string]bool{ "image/jpeg": true, "image/jpg": true, "image/png": true, "image/gif": true, } contentType := header.Header.Get("Content-Type") if !allowedTypes[contentType] { h.logger.Warn("UploadAvatar: invalid file type", zap.String("content_type", contentType), zap.String("filename", header.Filename), ) utils.RespondWithError(w, http.StatusBadRequest, "Only JPEG, PNG and GIF images are allowed") return } h.logger.Debug("UploadAvatar: file type validated successfully") // Загружаем аватар h.logger.Debug("UploadAvatar: calling avatarService.UploadAvatar", zap.Int64("user_id", int64(user.ID)), ) avatarPath, err := h.avatarService.UploadAvatar(user.ID, file, header) if err != nil { h.logger.Error("UploadAvatar: failed to upload avatar", zap.Error(err)) utils.RespondWithError(w, http.StatusInternalServerError, "Failed to upload avatar: "+err.Error()) return } h.logger.Info("UploadAvatar: avatar uploaded successfully", zap.Int64("user_id", int64(user.ID)), zap.String("avatar_path", avatarPath), ) // Возвращаем ответ с полем success utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{ "success": true, "message": "Avatar uploaded successfully", "avatar": avatarPath, }) } func (h *AvatarHandler) DeleteAvatar(w http.ResponseWriter, r *http.Request) { h.logger.Debug("DeleteAvatar START", zap.String("method", r.Method), zap.String("path", r.URL.Path), zap.String("remote_addr", r.RemoteAddr), ) defer func() { h.logger.Debug("DeleteAvatar END", zap.String("method", r.Method), zap.String("path", r.URL.Path), ) }() user, ok := middleware.GetUserFromContext(r.Context()) if !ok { h.logger.Warn("DeleteAvatar: authentication required") utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required") return } h.logger.Debug("DeleteAvatar: user authenticated", zap.Int64("user_id", int64(user.ID)), zap.String("username", user.FirstName+user.LastName), ) h.logger.Debug("DeleteAvatar: calling avatarService.DeleteAvatar", zap.Int64("user_id", int64(user.ID)), ) if err := h.avatarService.DeleteAvatar(user.ID); err != nil { h.logger.Error("DeleteAvatar: failed to delete avatar", zap.Error(err)) utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete avatar: "+err.Error()) return } h.logger.Info("DeleteAvatar: avatar deleted successfully", zap.Int64("user_id", int64(user.ID)), ) // Возвращаем ответ с полем success utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{ "success": true, "message": "Avatar deleted successfully", }) } // GET /v1/user/avatar/avatar_22_1760417314.png func (h *AvatarHandler) GetAvatar(w http.ResponseWriter, r *http.Request) { filename := chi.URLParam(r, "filename") h.logger.Debug("GetAvatar START", zap.String("method", r.Method), zap.String("filename", filename), zap.String("remote_addr", r.RemoteAddr), zap.String("url", r.URL.String()), ) defer func() { h.logger.Debug("GetAvatar END", zap.String("method", r.Method), zap.String("filename", filename), ) }() h.logger.Info("GetAvatar: handling get avatar request", zap.String("method", r.Method), zap.String("filename", filename), zap.String("remote_addr", r.RemoteAddr), ) // Вариант 1: Используем ServeAvatarFile (более эффективно для больших файлов) h.logger.Debug("GetAvatar: calling avatarService.ServeAvatarFile", zap.String("filename", filename), ) contentType, err := h.avatarService.ServeAvatarFile(w, filename) if err != nil { h.logger.Warn("GetAvatar: failed to serve avatar file", zap.String("filename", filename), zap.Error(err), ) switch { case err.Error() == "avatar file not found": h.logger.Warn("GetAvatar: avatar file not found", zap.String("filename", filename)) utils.RespondWithError(w, http.StatusNotFound, "Avatar not found") case err.Error() == "invalid filename" || err.Error() == "unsupported file format": h.logger.Warn("GetAvatar: invalid filename or format", zap.String("filename", filename), zap.Error(err), ) utils.RespondWithError(w, http.StatusBadRequest, err.Error()) default: h.logger.Error("GetAvatar: internal server error", zap.Error(err)) utils.RespondWithError(w, http.StatusInternalServerError, "Failed to serve avatar") } return } // Устанавливаем заголовки h.logger.Debug("GetAvatar: setting response headers", zap.String("content_type", contentType), ) w.Header().Set("Content-Type", contentType) w.Header().Set("Cache-Control", "public, max-age=31536000") // Кэш на 1 год w.Header().Set("Expires", time.Now().Add(365*24*time.Hour).Format(http.TimeFormat)) h.logger.Info("GetAvatar: avatar served successfully", zap.String("filename", filename), zap.String("content_type", contentType), ) }