From 916225f19e6e5276a51a642d21a0f81a1e0c598a Mon Sep 17 00:00:00 2001 From: Tiago Yamamoto Date: Fri, 19 Dec 2025 17:34:30 -0300 Subject: [PATCH] feat(backend): add configurable CORS and optimize Dockerfile - Add CORS_ORIGINS env var for multiple domains support - Update config.go with CORSOrigins field and getEnvStringSlice helper - Rewrite CORS middleware with CORSWithConfig for dynamic origins - Update server.go to use configurable CORS - Update .env.example with all configuration variables - Optimize Dockerfile: switch to distroless image, update port to 8214 --- backend/.env.example | 27 +++++++++- backend/Dockerfile | 18 ++----- backend/internal/config/config.go | 19 +++++++ backend/internal/http/middleware/cors.go | 65 ++++++++++++++++++------ backend/internal/server/server.go | 3 +- 5 files changed, 101 insertions(+), 31 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index bdcb825..e5a68a0 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,7 +1,32 @@ -DATABASE_URL=postgres://user:password@host:port/dbname?sslmode=disable +# ============================================ +# SaveInMed Backend - Environment Variables +# ============================================ + +# Application Settings +APP_NAME=saveinmed-performance-core PORT=8214 + +# Database Configuration +DATABASE_URL=postgres://user:password@host:port/dbname?sslmode=disable DB_MAX_OPEN_CONNS=25 DB_MAX_IDLE_CONNS=25 DB_CONN_MAX_IDLE=15m + +# JWT Authentication +JWT_SECRET=your-secret-key-here +JWT_EXPIRES_IN=24h + +# MercadoPago Payment Gateway MERCADOPAGO_BASE_URL=https://api.mercadopago.com MARKETPLACE_COMMISSION=2.5 + +# CORS Configuration +# Comma-separated list of allowed origins, use * for all +# Examples: +# CORS_ORIGINS=* +# CORS_ORIGINS=https://example.com +# CORS_ORIGINS=https://app.saveinmed.com,https://admin.saveinmed.com,http://localhost:3000 +CORS_ORIGINS=* + +# Testing (Optional) +# SKIP_DB_TEST=1 diff --git a/backend/Dockerfile b/backend/Dockerfile index 53a7675..55fd21d 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -3,9 +3,6 @@ # ===== STAGE 1: Build ===== FROM golang:1.24-alpine AS builder -# Instala certificados SSL para HTTPS -RUN apk add --no-cache ca-certificates tzdata - WORKDIR /build # Cache de dependências - só rebuild se go.mod/go.sum mudar @@ -20,22 +17,15 @@ COPY . . RUN --mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/root/.cache/go-build \ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ - go build -trimpath -ldflags="-s -w -extldflags '-static'" \ + go build -trimpath -ldflags="-s -w" \ -o /app/server ./cmd/api -# ===== STAGE 2: Runtime (scratch - imagem mínima ~5MB) ===== -FROM scratch - -# Certificados SSL e timezone -COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ -COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo +# ===== STAGE 2: Runtime (distroless - segurança + mínimo ~2MB) ===== +FROM gcr.io/distroless/static-debian12:nonroot # Binary COPY --from=builder /app/server /server -# Usuário não-root (UID 65534 = nobody) -USER 65534:65534 - -EXPOSE 8080 +EXPOSE 8214 ENTRYPOINT ["/server"] diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index 1bb031f..5cc1892 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "strconv" + "strings" "time" ) @@ -19,6 +20,7 @@ type Config struct { MarketplaceCommission float64 JWTSecret string JWTExpiresIn time.Duration + CORSOrigins []string } // Load reads configuration from environment variables and applies sane defaults @@ -35,6 +37,7 @@ func Load() Config { MarketplaceCommission: getEnvFloat("MARKETPLACE_COMMISSION", 2.5), JWTSecret: getEnv("JWT_SECRET", "dev-secret"), JWTExpiresIn: getEnvDuration("JWT_EXPIRES_IN", 24*time.Hour), + CORSOrigins: getEnvStringSlice("CORS_ORIGINS", []string{"*"}), } return cfg @@ -78,3 +81,19 @@ func getEnvFloat(key string, fallback float64) float64 { } return fallback } + +func getEnvStringSlice(key string, fallback []string) []string { + if value := os.Getenv(key); value != "" { + parts := strings.Split(value, ",") + result := make([]string, 0, len(parts)) + for _, p := range parts { + if trimmed := strings.TrimSpace(p); trimmed != "" { + result = append(result, trimmed) + } + } + if len(result) > 0 { + return result + } + } + return fallback +} diff --git a/backend/internal/http/middleware/cors.go b/backend/internal/http/middleware/cors.go index e6f4d4f..0c660ce 100644 --- a/backend/internal/http/middleware/cors.go +++ b/backend/internal/http/middleware/cors.go @@ -1,22 +1,57 @@ package middleware -import "net/http" +import ( + "net/http" + "strings" +) + +// CORSConfig holds the configuration for CORS middleware. +type CORSConfig struct { + AllowedOrigins []string +} // CORS adds Cross-Origin Resource Sharing headers to the response. -// For now, it allows all origins (*). -func CORS(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH") - w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") - w.Header().Set("Access-Control-Max-Age", "86400") - - // Handle preflight requests - if r.Method == http.MethodOptions { - w.WriteHeader(http.StatusOK) - return +// If allowedOrigins contains "*", it allows all origins. +// Otherwise, it checks if the request origin is in the allowed list. +func CORSWithConfig(cfg CORSConfig) func(http.Handler) http.Handler { + allowAll := false + originsMap := make(map[string]bool) + for _, origin := range cfg.AllowedOrigins { + if origin == "*" { + allowAll = true + break } + originsMap[strings.ToLower(origin)] = true + } - next.ServeHTTP(w, r) - }) + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + origin := r.Header.Get("Origin") + + if allowAll { + w.Header().Set("Access-Control-Allow-Origin", "*") + } else if origin != "" && originsMap[strings.ToLower(origin)] { + w.Header().Set("Access-Control-Allow-Origin", origin) + w.Header().Set("Vary", "Origin") + } + + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + w.Header().Set("Access-Control-Max-Age", "86400") + + // Handle preflight requests + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusOK) + return + } + + next.ServeHTTP(w, r) + }) + } +} + +// CORS is a compatibility wrapper that allows all origins. +// Deprecated: Use CORSWithConfig for more control. +func CORS(next http.Handler) http.Handler { + return CORSWithConfig(CORSConfig{AllowedOrigins: []string{"*"}})(next) } diff --git a/backend/internal/server/server.go b/backend/internal/server/server.go index 3588897..4cf1859 100644 --- a/backend/internal/server/server.go +++ b/backend/internal/server/server.go @@ -117,9 +117,10 @@ func (s *Server) Start(ctx context.Context) error { return err } + corsConfig := middleware.CORSConfig{AllowedOrigins: s.cfg.CORSOrigins} srv := &http.Server{ Addr: s.cfg.Addr(), - Handler: middleware.CORS(s.mux), + Handler: middleware.CORSWithConfig(corsConfig)(s.mux), ReadHeaderTimeout: 5 * time.Second, }