package handlers import ( "context" "encoding/json" "fmt" "net/http" "path/filepath" "strings" "aggios-app/backend/internal/api/middleware" "aggios-app/backend/internal/config" "github.com/google/uuid" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" ) // UploadHandler handles file upload endpoints type UploadHandler struct { minioClient *minio.Client cfg *config.Config } // NewUploadHandler creates a new upload handler func NewUploadHandler(cfg *config.Config) (*UploadHandler, error) { // Initialize MinIO client minioClient, err := minio.New(cfg.Minio.Endpoint, &minio.Options{ Creds: credentials.NewStaticV4(cfg.Minio.RootUser, cfg.Minio.RootPassword, ""), Secure: cfg.Minio.UseSSL, }) if err != nil { return nil, fmt.Errorf("failed to create MinIO client: %w", err) } // Ensure bucket exists ctx := context.Background() bucketName := cfg.Minio.BucketName exists, err := minioClient.BucketExists(ctx, bucketName) if err != nil { return nil, fmt.Errorf("failed to check bucket existence: %w", err) } if !exists { err = minioClient.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{}) if err != nil { return nil, fmt.Errorf("failed to create bucket: %w", err) } } return &UploadHandler{ minioClient: minioClient, cfg: cfg, }, nil } // UploadResponse represents the upload response type UploadResponse struct { FileID string `json:"file_id"` FileName string `json:"file_name"` FileURL string `json:"file_url"` FileSize int64 `json:"file_size"` } // Upload handles file upload func (h *UploadHandler) Upload(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Try to get user ID from context (optional for signup flow) userIDStr, _ := r.Context().Value(middleware.UserIDKey).(string) // Use temp tenant for unauthenticated uploads (signup flow) tenantID := uuid.MustParse("00000000-0000-0000-0000-000000000000") if userIDStr != "" { // TODO: Query database to get tenant_id from user_id when authenticated } // Parse multipart form (max 10MB) if err := r.ParseMultipartForm(10 << 20); err != nil { http.Error(w, "File too large (max 10MB)", http.StatusBadRequest) return } // Get file from form file, header, err := r.FormFile("file") if err != nil { http.Error(w, "Failed to read file", http.StatusBadRequest) return } defer file.Close() // Validate file type (images only) contentType := header.Header.Get("Content-Type") if !strings.HasPrefix(contentType, "image/") { http.Error(w, "Only images are allowed", http.StatusBadRequest) return } // Generate unique file ID fileID := uuid.New() ext := filepath.Ext(header.Filename) objectName := fmt.Sprintf("tenants/%s/logos/%s%s", tenantID.String(), fileID.String(), ext) // Upload to MinIO ctx := context.Background() _, err = h.minioClient.PutObject(ctx, h.cfg.Minio.BucketName, objectName, file, header.Size, minio.PutObjectOptions{ ContentType: contentType, }) if err != nil { http.Error(w, "Failed to upload file", http.StatusInternalServerError) return } // Generate public URL (replace internal hostname with localhost for browser access) fileURL := fmt.Sprintf("http://localhost:9000/%s/%s", h.cfg.Minio.BucketName, objectName) // Return response response := UploadResponse{ FileID: fileID.String(), FileName: header.Filename, FileURL: fileURL, FileSize: header.Size, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) }