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:
parent
851dd4f265
commit
916225f19e
5 changed files with 101 additions and 31 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue