v1.4: Segurança multi-tenant, file serving via API e UX humanizada

-  Validação cross-tenant no login e rotas protegidas
-  File serving via /api/files/{bucket}/{path} (eliminação DNS)
-  Mensagens de erro humanizadas inline (sem pop-ups)
-  Middleware tenant detection via headers customizados
-  Upload de logos retorna URLs via API
-  README atualizado com changelog v1.4 completo
This commit is contained in:
Erik Silva
2025-12-13 15:05:51 -03:00
parent 04c954c3d9
commit 2f1cf2bb2a
42 changed files with 2215 additions and 872 deletions

View File

@@ -9,6 +9,7 @@ import (
"log"
"net/http"
"path/filepath"
"strings"
"time"
"aggios-app/backend/internal/api/middleware"
@@ -172,20 +173,28 @@ func (h *AgencyHandler) UploadLogo(w http.ResponseWriter, r *http.Request) {
return
}
// Generate public URL
logoURL := fmt.Sprintf("http://localhost:9000/%s/%s", bucketName, filename)
// 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://localhost:9000/aggios-logos/tenants/uuid/logo-123.png -> tenants/uuid/logo-123.png
// 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 bucket name
if idx := len("http://localhost:9000/aggios-logos/"); idx < len(currentLogoURL) {
oldFilename = currentLogoURL[idx:]
// 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):]
}
}
}
@@ -202,6 +211,8 @@ func (h *AgencyHandler) UploadLogo(w http.ResponseWriter, r *http.Request) {
// 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 {
@@ -209,10 +220,12 @@ func (h *AgencyHandler) UploadLogo(w http.ResponseWriter, r *http.Request) {
}
if err2 != nil {
log.Printf("Failed to update logo: %v", err2)
http.Error(w, "Failed to update database", http.StatusInternalServerError)
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{