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
|
PORT=8214
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
DATABASE_URL=postgres://user:password@host:port/dbname?sslmode=disable
|
||||||
DB_MAX_OPEN_CONNS=25
|
DB_MAX_OPEN_CONNS=25
|
||||||
DB_MAX_IDLE_CONNS=25
|
DB_MAX_IDLE_CONNS=25
|
||||||
DB_CONN_MAX_IDLE=15m
|
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
|
MERCADOPAGO_BASE_URL=https://api.mercadopago.com
|
||||||
MARKETPLACE_COMMISSION=2.5
|
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 =====
|
# ===== STAGE 1: Build =====
|
||||||
FROM golang:1.24-alpine AS builder
|
FROM golang:1.24-alpine AS builder
|
||||||
|
|
||||||
# Instala certificados SSL para HTTPS
|
|
||||||
RUN apk add --no-cache ca-certificates tzdata
|
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
# Cache de dependências - só rebuild se go.mod/go.sum mudar
|
# 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 \
|
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||||
--mount=type=cache,target=/root/.cache/go-build \
|
--mount=type=cache,target=/root/.cache/go-build \
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
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
|
-o /app/server ./cmd/api
|
||||||
|
|
||||||
# ===== STAGE 2: Runtime (scratch - imagem mínima ~5MB) =====
|
# ===== STAGE 2: Runtime (distroless - segurança + mínimo ~2MB) =====
|
||||||
FROM scratch
|
FROM gcr.io/distroless/static-debian12:nonroot
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# Binary
|
# Binary
|
||||||
COPY --from=builder /app/server /server
|
COPY --from=builder /app/server /server
|
||||||
|
|
||||||
# Usuário não-root (UID 65534 = nobody)
|
EXPOSE 8214
|
||||||
USER 65534:65534
|
|
||||||
|
|
||||||
EXPOSE 8080
|
|
||||||
|
|
||||||
ENTRYPOINT ["/server"]
|
ENTRYPOINT ["/server"]
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -19,6 +20,7 @@ type Config struct {
|
||||||
MarketplaceCommission float64
|
MarketplaceCommission float64
|
||||||
JWTSecret string
|
JWTSecret string
|
||||||
JWTExpiresIn time.Duration
|
JWTExpiresIn time.Duration
|
||||||
|
CORSOrigins []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load reads configuration from environment variables and applies sane defaults
|
// Load reads configuration from environment variables and applies sane defaults
|
||||||
|
|
@ -35,6 +37,7 @@ func Load() Config {
|
||||||
MarketplaceCommission: getEnvFloat("MARKETPLACE_COMMISSION", 2.5),
|
MarketplaceCommission: getEnvFloat("MARKETPLACE_COMMISSION", 2.5),
|
||||||
JWTSecret: getEnv("JWT_SECRET", "dev-secret"),
|
JWTSecret: getEnv("JWT_SECRET", "dev-secret"),
|
||||||
JWTExpiresIn: getEnvDuration("JWT_EXPIRES_IN", 24*time.Hour),
|
JWTExpiresIn: getEnvDuration("JWT_EXPIRES_IN", 24*time.Hour),
|
||||||
|
CORSOrigins: getEnvStringSlice("CORS_ORIGINS", []string{"*"}),
|
||||||
}
|
}
|
||||||
|
|
||||||
return cfg
|
return cfg
|
||||||
|
|
@ -78,3 +81,19 @@ func getEnvFloat(key string, fallback float64) float64 {
|
||||||
}
|
}
|
||||||
return fallback
|
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,12 +1,40 @@
|
||||||
package middleware
|
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.
|
// CORS adds Cross-Origin Resource Sharing headers to the response.
|
||||||
// For now, it allows all origins (*).
|
// If allowedOrigins contains "*", it allows all origins.
|
||||||
func CORS(next http.Handler) http.Handler {
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
origin := r.Header.Get("Origin")
|
||||||
|
|
||||||
|
if allowAll {
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
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-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH")
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||||
w.Header().Set("Access-Control-Max-Age", "86400")
|
w.Header().Set("Access-Control-Max-Age", "86400")
|
||||||
|
|
@ -20,3 +48,10 @@ func CORS(next http.Handler) http.Handler {
|
||||||
next.ServeHTTP(w, r)
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
corsConfig := middleware.CORSConfig{AllowedOrigins: s.cfg.CORSOrigins}
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Addr: s.cfg.Addr(),
|
Addr: s.cfg.Addr(),
|
||||||
Handler: middleware.CORS(s.mux),
|
Handler: middleware.CORSWithConfig(corsConfig)(s.mux),
|
||||||
ReadHeaderTimeout: 5 * time.Second,
|
ReadHeaderTimeout: 5 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue