package handlers import ( "bytes" "context" "encoding/json" "fmt" "io" "log" "net/http" "path/filepath" "strings" "time" "aggios-app/backend/internal/api/middleware" "github.com/google/uuid" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" ) // UploadLogo handles logo file uploads func (h *AgencyHandler) UploadLogo(w http.ResponseWriter, r *http.Request) { // Only accept POST if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } log.Printf("Logo upload request received from tenant") // Get tenant ID from context tenantIDVal := r.Context().Value(middleware.TenantIDKey) if tenantIDVal == nil { log.Printf("No tenant ID in context") http.Error(w, "Unauthorized", http.StatusUnauthorized) return } // Try to get as uuid.UUID first, if that fails try string and parse var tenantID uuid.UUID var ok bool tenantID, ok = tenantIDVal.(uuid.UUID) if !ok { // Try as string tenantIDStr, isString := tenantIDVal.(string) if !isString { log.Printf("Invalid tenant ID type: %T", tenantIDVal) http.Error(w, "Invalid tenant ID", http.StatusBadRequest) return } var err error tenantID, err = uuid.Parse(tenantIDStr) if err != nil { log.Printf("Failed to parse tenant ID: %v", err) http.Error(w, "Invalid tenant ID format", http.StatusBadRequest) return } } log.Printf("Processing logo upload for tenant: %s", tenantID) // Parse multipart form (2MB max) const maxLogoSize = 2 * 1024 * 1024 if err := r.ParseMultipartForm(maxLogoSize); err != nil { http.Error(w, "File too large", http.StatusBadRequest) return } file, header, err := r.FormFile("logo") if err != nil { http.Error(w, "Failed to read file", http.StatusBadRequest) return } defer file.Close() // Validate file type contentType := header.Header.Get("Content-Type") if contentType != "image/png" && contentType != "image/jpeg" && contentType != "image/svg+xml" && contentType != "image/jpg" { http.Error(w, "Only PNG, JPG or SVG files are allowed", http.StatusBadRequest) return } // Get logo type (logo or horizontal) logoType := r.FormValue("type") if logoType != "logo" && logoType != "horizontal" { logoType = "logo" } // Get current logo URL from database to delete old file var currentLogoURL string var queryErr error if logoType == "horizontal" { queryErr = h.tenantRepo.DB().QueryRow("SELECT logo_horizontal_url FROM tenants WHERE id = $1", tenantID).Scan(¤tLogoURL) } else { queryErr = h.tenantRepo.DB().QueryRow("SELECT logo_url FROM tenants WHERE id = $1", tenantID).Scan(¤tLogoURL) } if queryErr != nil && queryErr.Error() != "sql: no rows in result set" { log.Printf("Warning: Failed to get current logo URL: %v", queryErr) } // Initialize MinIO client minioClient, err := minio.New("aggios-minio:9000", &minio.Options{ Creds: credentials.NewStaticV4("minioadmin", "M1n10_S3cur3_P@ss_2025!", ""), Secure: false, }) if err != nil { log.Printf("Failed to create MinIO client: %v", err) http.Error(w, "Storage service unavailable", http.StatusInternalServerError) return } // Ensure bucket exists bucketName := "aggios-logos" ctx := context.Background() exists, err := minioClient.BucketExists(ctx, bucketName) if err != nil { log.Printf("Failed to check bucket: %v", err) http.Error(w, "Storage error", http.StatusInternalServerError) return } if !exists { err = minioClient.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{}) if err != nil { log.Printf("Failed to create bucket: %v", err) http.Error(w, "Storage error", http.StatusInternalServerError) return } // Set bucket policy to public-read policy := fmt.Sprintf(`{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"AWS": ["*"]}, "Action": ["s3:GetObject"], "Resource": ["arn:aws:s3:::%s/*"] }] }`, bucketName) err = minioClient.SetBucketPolicy(ctx, bucketName, policy) if err != nil { log.Printf("Warning: Failed to set bucket policy: %v", err) } } // Read file content fileBytes, err := io.ReadAll(file) if err != nil { http.Error(w, "Failed to read file", http.StatusInternalServerError) return } // Generate unique filename ext := filepath.Ext(header.Filename) filename := fmt.Sprintf("tenants/%s/%s-%d%s", tenantID, logoType, time.Now().Unix(), ext) // Upload to MinIO _, err = minioClient.PutObject( ctx, bucketName, filename, bytes.NewReader(fileBytes), int64(len(fileBytes)), minio.PutObjectOptions{ ContentType: contentType, }, ) if err != nil { log.Printf("Failed to upload to MinIO: %v", err) http.Error(w, "Failed to save file", http.StatusInternalServerError) return } // Generate public URL through API (not direct MinIO access) // This is more secure and doesn't require DNS configuration logoURL := fmt.Sprintf("http://api.localhost/api/files/%s/%s", bucketName, filename) log.Printf("Logo uploaded successfully: %s", logoURL) // Delete old logo file from MinIO if exists if currentLogoURL != "" && currentLogoURL != "https://via.placeholder.com/150" { // Extract object key from URL // Example: http://api.localhost/api/files/aggios-logos/tenants/uuid/logo-123.png -> tenants/uuid/logo-123.png oldFilename := "" if len(currentLogoURL) > 0 { // Split by /api/files/{bucket}/ to get the file path apiPrefix := fmt.Sprintf("http://api.localhost/api/files/%s/", bucketName) if strings.HasPrefix(currentLogoURL, apiPrefix) { oldFilename = strings.TrimPrefix(currentLogoURL, apiPrefix) } else { // Fallback for old MinIO URLs baseURL := fmt.Sprintf("%s/%s/", h.config.Minio.PublicURL, bucketName) if len(currentLogoURL) > len(baseURL) { oldFilename = currentLogoURL[len(baseURL):] } } } if oldFilename != "" { err = minioClient.RemoveObject(ctx, bucketName, oldFilename, minio.RemoveObjectOptions{}) if err != nil { log.Printf("Warning: Failed to delete old logo %s: %v", oldFilename, err) // Don't fail the request if deletion fails } else { log.Printf("Old logo deleted successfully: %s", oldFilename) } } } // Update tenant record in database var err2 error log.Printf("Updating database: tenant_id=%s, logo_type=%s, logo_url=%s", tenantID, logoType, logoURL) if logoType == "horizontal" { _, err2 = h.tenantRepo.DB().Exec("UPDATE tenants SET logo_horizontal_url = $1, updated_at = NOW() WHERE id = $2", logoURL, tenantID) } else { _, err2 = h.tenantRepo.DB().Exec("UPDATE tenants SET logo_url = $1, updated_at = NOW() WHERE id = $2", logoURL, tenantID) } if err2 != nil { log.Printf("ERROR: Failed to update logo in database: %v", err2) http.Error(w, fmt.Sprintf("Failed to update database: %v", err2), http.StatusInternalServerError) return } log.Printf("SUCCESS: Logo saved to database successfully!") // Return success response response := map[string]string{ "logo_url": logoURL, "message": "Logo uploaded successfully", } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) }