From bbf470617bcd08f4b31e8c811403c71c3b38f7ce Mon Sep 17 00:00:00 2001 From: valitovgaziz Date: Tue, 14 Oct 2025 12:41:16 +0500 Subject: [PATCH] modified: serv_nginx/api_bb/internal/handlers/avatar.go modified: serv_nginx/api_bb/internal/handlers/handlers.go modified: serv_nginx/api_bb/internal/handlers/user.go modified: serv_nginx/api_bb/internal/routes/routes.go modified: serv_nginx/api_bb/internal/service/avatar_service.go modified: serv_nginx/nginx/nginx-ssl.conf try to serve file name throught path --- serv_nginx/api_bb/internal/handlers/avatar.go | 45 ++++++++ .../api_bb/internal/handlers/handlers.go | 18 ++++ serv_nginx/api_bb/internal/handlers/user.go | 35 +++++- serv_nginx/api_bb/internal/routes/routes.go | 48 ++++----- .../api_bb/internal/service/avatar_service.go | 102 ++++++++++++++++++ serv_nginx/nginx/nginx-ssl.conf | 8 +- 6 files changed, 226 insertions(+), 30 deletions(-) diff --git a/serv_nginx/api_bb/internal/handlers/avatar.go b/serv_nginx/api_bb/internal/handlers/avatar.go index 0ac0585..8e0ec3a 100644 --- a/serv_nginx/api_bb/internal/handlers/avatar.go +++ b/serv_nginx/api_bb/internal/handlers/avatar.go @@ -3,6 +3,7 @@ package handlers import ( "net/http" + "time" "api_bb/internal/service" "api_bb/pkg/logger" @@ -27,8 +28,12 @@ func NewAvatarHandler(avatarService service.AvatarService) *AvatarHandler { func (h *AvatarHandler) Routes() chi.Router { r := chi.NewRouter() + + // r.Get("/avatar/{filename}", h.ServeAvatar) r.Post("/upload", h.UploadAvatar) r.Delete("/delete", h.DeleteAvatar) + r.Get("/{filename}", h.GetAvatar) + return r } @@ -99,3 +104,43 @@ func (h *AvatarHandler) DeleteAvatar(w http.ResponseWriter, r *http.Request) { "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.Info("handling get avatar request", + zap.String("method", r.Method), + zap.String("filename", filename), + zap.String("remote_addr", r.RemoteAddr), + ) + + // Вариант 1: Используем ServeAvatarFile (более эффективно для больших файлов) + contentType, err := h.avatarService.ServeAvatarFile(w, filename) + if err != nil { + h.logger.Warn("failed to serve avatar file", + zap.String("filename", filename), + zap.Error(err), + ) + + switch { + case err.Error() == "avatar file not found": + utils.RespondWithError(w, http.StatusNotFound, "Avatar not found") + case err.Error() == "invalid filename" || err.Error() == "unsupported file format": + utils.RespondWithError(w, http.StatusBadRequest, err.Error()) + default: + utils.RespondWithError(w, http.StatusInternalServerError, "Failed to serve avatar") + } + return + } + + // Устанавливаем заголовки + 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("avatar served successfully", + zap.String("filename", filename), + zap.String("content_type", contentType), + ) +} diff --git a/serv_nginx/api_bb/internal/handlers/handlers.go b/serv_nginx/api_bb/internal/handlers/handlers.go index 95d8082..b32978b 100644 --- a/serv_nginx/api_bb/internal/handlers/handlers.go +++ b/serv_nginx/api_bb/internal/handlers/handlers.go @@ -14,6 +14,8 @@ type Handler struct { healthHandler *HealthHandler authHandler *AuthHandler userHandler *UserHandler + avatarHandler *AvatarHandler + newsHandler *NewsHandler // Здесь будут добавлены другие обработчики // userHandler *UserHandler // eventHandler *EventHandler @@ -23,6 +25,8 @@ type Handler struct { func NewHandler(db *gorm.DB, cfg *config.Config) *Handler { // Инициализация репозиториев userRepo := repository.NewUserRepository(db) + newsRepo := repository.NewNewsRepository(db) + commentRepo := repository.NewCommentRepository(db) // Initialize logger baseLogger := logger.NewWrapper(logger.Get()) // Создаем базовый логгер @@ -31,16 +35,22 @@ func NewHandler(db *gorm.DB, cfg *config.Config) *Handler { jwtService := service.NewJWTService(cfg.JWTSecret) authService := service.NewAuthService(userRepo, jwtService, baseLogger) userService := service.NewUserService(userRepo, jwtService, baseLogger) + avatarService := service.NewAvatarService(userRepo, baseLogger) + newsService := service.NewNewsService(newsRepo, commentRepo, baseLogger) // Инициализация обработчиков healthHandler := NewHealthHandler() authHandler := NewAuthHandler(authService, jwtService) userHandler := NewUserHandler(&userService) + newsHandler := NewNewsHandler(newsService, baseLogger) + avatarHandler := NewAvatarHandler(avatarService) return &Handler{ healthHandler: healthHandler, authHandler: authHandler, userHandler: userHandler, + newsHandler: newsHandler, + avatarHandler: avatarHandler, } } @@ -56,3 +66,11 @@ func (h *Handler) AuthHandler() *AuthHandler { func (h *Handler) UserHandler() *UserHandler { // ДОБАВЛЕН геттер для UserHandler return h.userHandler } + +func (h *Handler) AvatarHandler() *AvatarHandler { + return h.avatarHandler +} + +func (h *Handler) NewsHandler() *NewsHandler { + return h.newsHandler +} diff --git a/serv_nginx/api_bb/internal/handlers/user.go b/serv_nginx/api_bb/internal/handlers/user.go index a8c0e88..951472d 100644 --- a/serv_nginx/api_bb/internal/handlers/user.go +++ b/serv_nginx/api_bb/internal/handlers/user.go @@ -6,6 +6,8 @@ import ( "encoding/json" "io" "net/http" + "os" + "path/filepath" "time" "api_bb/internal/models" @@ -35,6 +37,7 @@ func (h *UserHandler) Routes() chi.Router { r.Get("/profile", h.GetProfile) r.Post("/editProfile", h.UpdateProfile) + r.Get("/avatar/{filename}", h.ServeAvatar) return r } @@ -54,7 +57,6 @@ type UserResponse struct { UpdatedAt time.Time `json:"updated_at"` } - func (h *UserHandler) GetProfile(w http.ResponseWriter, r *http.Request) { h.logger.Info("handling get profile request", @@ -176,3 +178,34 @@ func (h *UserHandler) UpdateProfile(w http.ResponseWriter, r *http.Request) { "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), + ) +} diff --git a/serv_nginx/api_bb/internal/routes/routes.go b/serv_nginx/api_bb/internal/routes/routes.go index bab4beb..cf2ca76 100644 --- a/serv_nginx/api_bb/internal/routes/routes.go +++ b/serv_nginx/api_bb/internal/routes/routes.go @@ -23,50 +23,42 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler { r.Use(m) } + // handler + allHandler := handlers.NewHandler(db, config) + // Serve static files (avatars) - ДОБАВЬТЕ ЭТО - r.Handle("/uploads/*", http.StripPrefix("/uploads/", + r.Handle("/uploads/*", http.StripPrefix("/uploads/", http.FileServer(http.Dir("./uploads")))) // Initialize repositories userRepo := repository.NewUserRepository(db) - newsRepo := repository.NewNewsRepository(db) - commentRepo := repository.NewCommentRepository(db) // Initialize logger baseLogger := logger.NewWrapper(logger.Get()) // Создаем базовый логгер // Initialize services with logger jwtService := service.NewJWTService(config.JWTSecret) - authService := service.NewAuthService(userRepo, jwtService, baseLogger) // Передаем логгер - userService := service.NewUserService(userRepo, jwtService, baseLogger) - newsService := service.NewNewsService(newsRepo, commentRepo, baseLogger) - avatarService := service.NewAvatarService(userRepo, baseLogger) // Initialize handlers - healthHandler := handlers.NewHealthHandler() - authHandler := handlers.NewAuthHandler(authService, jwtService) - userHandler := handlers.NewUserHandler(&userService) - newsHandler := handlers.NewNewsHandler(newsService, baseLogger) - avatarHandler := handlers.NewAvatarHandler(avatarService) // Health routes - r.Mount("/api", healthHandler.Routes()) + r.Mount("/api", allHandler.HealthHandler().Routes()) // API v1 routes r.Route("/v1", func(r chi.Router) { - r.Get("/check", healthHandler.Check) + r.Get("/check", allHandler.HealthHandler().Check) // Public auth routes - r.Mount("/auth", authHandler.Routes()) + r.Mount("/auth", allHandler.AuthHandler().Routes()) // Protected routes r.Route("/user", func(r chi.Router) { r.Use(middleware.AuthMiddleware(jwtService, userRepo)) r.Use(middleware.RequireAuth) - r.Mount("/", userHandler.Routes()) + r.Mount("/", allHandler.UserHandler().Routes()) - r.Mount("/avatar", avatarHandler.Routes()) + r.Mount("/avatar", allHandler.AvatarHandler().Routes()) // Здесь будут другие защищенные маршруты пользователя }) @@ -74,23 +66,23 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler { r.Route("/news", func(r chi.Router) { // Публичные маршруты - r.Get("/", newsHandler.GetNews) - r.Get("/{id}", newsHandler.GetNewsByID) - r.Get("/{id}/comments", newsHandler.GetComments) - r.Get("/check", healthHandler.Check) + r.Get("/", allHandler.NewsHandler().GetNews) + r.Get("/{id}", allHandler.NewsHandler().GetNewsByID) + r.Get("/{id}/comments", allHandler.NewsHandler().GetComments) + r.Get("/check", allHandler.HealthHandler().Check) // Защищенные маршруты r.Group(func(r chi.Router) { r.Use(middleware.AuthMiddleware(jwtService, userRepo)) r.Use(middleware.RequireAuth) - r.Post("/", newsHandler.CreateNews) - r.Put("/{id}", newsHandler.UpdateNews) - r.Delete("/{id}", newsHandler.DeleteNews) - r.Post("/{id}/comments", newsHandler.CreateComment) - r.Delete("/comments/{commentId}", newsHandler.DeleteComment) - r.Get("/my/news", newsHandler.GetUserNews) - r.Get("/check", healthHandler.Check) + r.Post("/", allHandler.NewsHandler().CreateNews) + r.Put("/{id}", allHandler.NewsHandler().UpdateNews) + r.Delete("/{id}", allHandler.NewsHandler().DeleteNews) + r.Post("/{id}/comments", allHandler.NewsHandler().CreateComment) + r.Delete("/comments/{commentId}", allHandler.NewsHandler().DeleteComment) + r.Get("/my/news", allHandler.NewsHandler().GetUserNews) + r.Get("/check", allHandler.HealthHandler().Check) }) }) diff --git a/serv_nginx/api_bb/internal/service/avatar_service.go b/serv_nginx/api_bb/internal/service/avatar_service.go index dd3dc90..af62e90 100644 --- a/serv_nginx/api_bb/internal/service/avatar_service.go +++ b/serv_nginx/api_bb/internal/service/avatar_service.go @@ -19,6 +19,8 @@ type AvatarService interface { UploadAvatar(userID uint, file multipart.File, header *multipart.FileHeader) (string, error) DeleteAvatar(userID uint) error GetAvatarPath(userID uint) (string, error) + GetAvatarFile(filename string) ([]byte, string, error) // НОВЫЙ МЕТОД + ServeAvatarFile(w io.Writer, filename string) (string, error) } type avatarService struct { @@ -111,3 +113,103 @@ func (s *avatarService) GetAvatarPath(userID uint) (string, error) { } return user.Avatar, nil } + +func (s *avatarService) GetAvatarFile(filename string) ([]byte, string, error) { + // Валидация имени файла + if filename == "" || strings.Contains(filename, "..") || strings.Contains(filename, "/") { + return nil, "", fmt.Errorf("invalid filename") + } + + // Проверяем допустимые расширения + allowedExts := map[string]string{ + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".webp": "image/webp", + } + + fileExt := strings.ToLower(filepath.Ext(filename)) + contentType, exists := allowedExts[fileExt] + if !exists { + return nil, "", fmt.Errorf("unsupported file format") + } + + // Формируем путь к файлу + filePath := filepath.Join("./uploads/avatars", filename) + + // Проверяем существование файла + fileInfo, err := os.Stat(filePath) + if err != nil { + if os.IsNotExist(err) { + return nil, "", fmt.Errorf("avatar file not found") + } + return nil, "", fmt.Errorf("failed to access file: %v", err) + } + + // Проверяем размер файла (максимум 10MB) + if fileInfo.Size() > 10*1024*1024 { + return nil, "", fmt.Errorf("file too large") + } + + // Читаем файл + fileData, err := os.ReadFile(filePath) + if err != nil { + return nil, "", fmt.Errorf("failed to read file: %v", err) + } + + return fileData, contentType, nil +} + +func (s *avatarService) ServeAvatarFile(w io.Writer, filename string) (string, error) { + // Валидация имени файла + if filename == "" || strings.Contains(filename, "..") || strings.Contains(filename, "/") { + return "", fmt.Errorf("invalid filename") + } + + // Проверяем допустимые расширения + allowedExts := map[string]string{ + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".webp": "image/webp", + } + + fileExt := strings.ToLower(filepath.Ext(filename)) + contentType, exists := allowedExts[fileExt] + if !exists { + return "", fmt.Errorf("unsupported file format") + } + + // Формируем путь к файлу + filePath := filepath.Join("./uploads/avatars", filename) + + // Проверяем существование файла + fileInfo, err := os.Stat(filePath) + if err != nil { + if os.IsNotExist(err) { + return "", fmt.Errorf("avatar file not found") + } + return "", fmt.Errorf("failed to access file: %v", err) + } + + // Проверяем размер файла + if fileInfo.Size() > 10*1024*1024 { + return "", fmt.Errorf("file too large") + } + + // Открываем и копируем файл + file, err := os.Open(filePath) + if err != nil { + return "", fmt.Errorf("failed to open file: %v", err) + } + defer file.Close() + + _, err = io.Copy(w, file) + if err != nil { + return "", fmt.Errorf("failed to serve file: %v", err) + } + + return contentType, nil +} diff --git a/serv_nginx/nginx/nginx-ssl.conf b/serv_nginx/nginx/nginx-ssl.conf index 4ab33e8..68deca9 100644 --- a/serv_nginx/nginx/nginx-ssl.conf +++ b/serv_nginx/nginx/nginx-ssl.conf @@ -9,7 +9,13 @@ server { location / { return 301 https://$host$request_uri; } - + # ✅ Добавляем uploads и в HTTP редирект + location /uploads/ { + alias /uploads/; + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } }