package main import ( "bytes" "fmt" "io" "log/slog" "net/http" "path/filepath" "time" "lendry-erp/media/internal/processor" "lendry-erp/media/internal/storage" ) // InternalAuthMiddleware защищает Go-сервис. // Он пропускает только те запросы, в которых API Gateway заботливо положил X-User-Id func InternalAuthMiddleware(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { userId := r.Header.Get("X-User-Id") isModerator := r.Header.Get("X-Is-Admin") == "true" if userId == "" { slog.Warn("Блокировка несанкционированного доступа (отсутствует X-User-Id)", "ip", r.RemoteAddr) http.Error(w, `{"error": "Unauthorized: API Gateway only"}`, http.StatusUnauthorized) return } slog.Debug("Запрос авторизован Gateway", "user_id", userId, "moderator", isModerator) next.ServeHTTP(w, r) } } func main() { // Подключение к MinIO minioStore, err := storage.NewMinioStorage("minio:9000", "admin", "admin12345", "erp-media") if err != nil { panic(err) } // === Эндпоинт 1: ЗАГРУЗКА ПРИВАТНЫХ ФАЙЛОВ === http.HandleFunc("/upload", InternalAuthMiddleware(func(w http.ResponseWriter, r *http.Request) { r.ParseMultipartForm(50 << 20) // Лимит 50 МБ file, header, err := r.FormFile("file") if err != nil { http.Error(w, "Файл не найден", http.StatusBadRequest) return } defer file.Close() mode := r.FormValue("mode") if mode == "" { mode = "chat" } fileBytes, err := io.ReadAll(file) if err != nil { http.Error(w, "Ошибка чтения файла", http.StatusInternalServerError) return } finalBytes, contentType, err := processor.ProcessImage(fileBytes, mode) if err != nil { http.Error(w, "Ошибка обработки изображения", http.StatusInternalServerError) return } ext := filepath.Ext(header.Filename) if ext == "" { ext = ".jpg" } objectName := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext) fileName, err := minioStore.Upload(r.Context(), objectName, bytes.NewReader(finalBytes), int64(len(finalBytes)), contentType) if err != nil { http.Error(w, "Ошибка загрузки в MinIO", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{"success": true, "fileName": "%s", "size": %d}`, fileName, len(finalBytes)) })) // === Эндпоинт 2: ПОЛУЧЕНИЕ ПРИВАТНОЙ ССЫЛКИ === http.HandleFunc("/media/url", InternalAuthMiddleware(func(w http.ResponseWriter, r *http.Request) { fileName := r.URL.Query().Get("file") if fileName == "" { http.Error(w, "Укажите имя файла", http.StatusBadRequest) return } // ВАЖНО: Раз запрос дошел сюда, значит API Gateway УЖЕ проверил // JWT токен и принадлежность пользователя к чату (через gRPC). // Никакие проверки БД в Go больше не нужны. Мы доверяем Gateway! expiry := time.Minute * 2 // Приватные ссылки живут 2 минуты presignedURL, err := minioStore.GeneratePresignedURL(r.Context(), fileName, expiry) if err != nil { http.Error(w, "Ошибка генерации ссылки", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{"url": "%s"}`, presignedURL) })) // === Эндпоинт 3: ПОЛУЧЕНИЕ ПУБЛИЧНОЙ ССЫЛКИ (Аватарки, Баннеры) === // Обрати внимание: этот эндпоинт НЕ обернут в InternalAuthMiddleware http.HandleFunc("/media/public/url", func(w http.ResponseWriter, r *http.Request) { fileName := r.URL.Query().Get("file") if fileName == "" { http.Error(w, "Укажите имя файла", http.StatusBadRequest) return } // Публичные ссылки можно делать более долгоживущими (например, на 24 часа), // чтобы браузер мог их эффективно кэшировать expiry := time.Hour * 24 presignedURL, err := minioStore.GeneratePresignedURL(r.Context(), fileName, expiry) if err != nil { http.Error(w, "Ошибка генерации ссылки", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{"url": "%s"}`, presignedURL) }) slog.Info("Media Service запущен на внутреннем порту :8081") if err := http.ListenAndServe(":8081", nil); err != nil { slog.Error("Ошибка работы HTTP сервера", "err", err) } }