- Create CreatePlanModal component with Headless UI Dialog - Implement dark mode support throughout plans UI - Update plans/page.tsx with professional card layout - Update plans/[id]/page.tsx with consistent styling - Add proper spacing, typography, and color consistency - Implement smooth animations and transitions - Add success/error message feedback - Improve form UX with better input styling
105 lines
2.7 KiB
Go
105 lines
2.7 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"aggios-app/backend/internal/config"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/minio/minio-go/v7"
|
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
|
)
|
|
|
|
type FilesHandler struct {
|
|
config *config.Config
|
|
}
|
|
|
|
func NewFilesHandler(cfg *config.Config) *FilesHandler {
|
|
return &FilesHandler{
|
|
config: cfg,
|
|
}
|
|
}
|
|
|
|
// ServeFile serves files from MinIO through the API
|
|
func (h *FilesHandler) ServeFile(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
bucket := vars["bucket"]
|
|
|
|
// Get the file path (everything after /api/files/{bucket}/)
|
|
prefix := fmt.Sprintf("/api/files/%s/", bucket)
|
|
filePath := strings.TrimPrefix(r.URL.Path, prefix)
|
|
|
|
if filePath == "" {
|
|
http.Error(w, "File path is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Whitelist de buckets públicos permitidos
|
|
allowedBuckets := map[string]bool{
|
|
"aggios-logos": true,
|
|
}
|
|
if !allowedBuckets[bucket] {
|
|
log.Printf("🚫 Access denied to bucket: %s", bucket)
|
|
http.Error(w, "Access denied", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
// Proteção contra path traversal
|
|
if strings.Contains(filePath, "..") {
|
|
log.Printf("🚫 Path traversal attempt detected: %s", filePath)
|
|
http.Error(w, "Invalid path", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
log.Printf("📁 Serving file: bucket=%s, path=%s", bucket, filePath)
|
|
|
|
// 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
|
|
}
|
|
|
|
// Get object from MinIO
|
|
ctx := context.Background()
|
|
object, err := minioClient.GetObject(ctx, bucket, filePath, minio.GetObjectOptions{})
|
|
if err != nil {
|
|
log.Printf("Failed to get object: %v", err)
|
|
http.Error(w, "File not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
defer object.Close()
|
|
|
|
// Get object info for content type and size
|
|
objInfo, err := object.Stat()
|
|
if err != nil {
|
|
log.Printf("Failed to stat object: %v", err)
|
|
http.Error(w, "File not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
// Set appropriate headers
|
|
w.Header().Set("Content-Type", objInfo.ContentType)
|
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", objInfo.Size))
|
|
w.Header().Set("Cache-Control", "public, max-age=31536000") // Cache for 1 year
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
|
|
// Copy file content to response
|
|
_, err = io.Copy(w, object)
|
|
if err != nil {
|
|
log.Printf("Failed to copy object content: %v", err)
|
|
return
|
|
}
|
|
|
|
log.Printf("✅ File served successfully: %s", filePath)
|
|
}
|