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
This commit is contained in:
Tiago Yamamoto 2025-12-19 17:34:30 -03:00
parent 851dd4f265
commit 916225f19e
5 changed files with 101 additions and 31 deletions

View file

@ -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

View file

@ -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"]

View file

@ -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
}

View file

@ -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)
}

View file

@ -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,
}