Files
template-vitrine/backend/internal/platform/config/config.go
T
Xor290 919c807004
ci-api / test (push) Failing after 8m7s
ci-web / test (push) Failing after 5m5s
chore: build
2026-09-20 12:18:33 +02:00

183 lines
5.1 KiB
Go

// Package config loads application configuration from environment variables.
package config
import (
"fmt"
"os"
"strconv"
"time"
"github.com/joho/godotenv"
)
type Config struct {
Database DatabaseConfig
Redis RedisConfig
JWT JWTConfig
Server ServerConfig
Seed SeedConfig
Media MediaConfig
Verification VerificationConfig
}
type DatabaseConfig struct {
URL string
}
type RedisConfig struct {
URL string
}
type JWTConfig struct {
Secret string
AccessTTL time.Duration
RefreshTTL time.Duration
}
type ServerConfig struct {
Port string
GinMode string
CORSOrigin string
CookieSecure bool
}
type SeedConfig struct {
AdminUsername string
AdminPassword string
AdminNumber int64
}
// MediaConfig configures the pluggable media storage backend. Driver
// selects "local" (files on disk, served via /uploads) or "s3" (any
// S3-compatible object store). Only the fields relevant to the selected
// driver need to be set.
type MediaConfig struct {
Driver string
MaxUploadMB int64
LocalDir string
LocalBaseURL string
S3Bucket string
S3Region string
S3Endpoint string
S3AccessKeyID string
S3SecretKey string
S3UsePathStyle bool
S3PublicBaseURL string
}
// VerificationConfig configures where customer identity-verification
// documents (front/back ID photos) are stored on disk. Unlike media, this
// is never served through a public URL -- see customerverification.Storage.
type VerificationConfig struct {
UploadDir string
}
func Load() (*Config, error) {
// The .env file lives at the repo root (next to docker-compose.yml),
// but `go run` is commonly invoked from inside backend/. Try both so
// either working directory picks it up; missing files are ignored.
_ = godotenv.Load(".env")
_ = godotenv.Load("../../../.env")
accessMinutes, err := strconv.Atoi(getEnv("ACCESS_TOKEN_TTL_MINUTES", "15"))
if err != nil {
return nil, fmt.Errorf("invalid ACCESS_TOKEN_TTL_MINUTES: %w", err)
}
refreshHours, err := strconv.Atoi(getEnv("REFRESH_TOKEN_TTL_HOURS", "168"))
if err != nil {
return nil, fmt.Errorf("invalid REFRESH_TOKEN_TTL_HOURS: %w", err)
}
secret := os.Getenv("JWT_SECRET")
if secret == "" || secret == "change-me-to-a-long-random-secret" {
return nil, fmt.Errorf("JWT_SECRET must be set to a strong, unique value")
}
dbURL := os.Getenv("DATABASE_URL")
if dbURL == "" {
return nil, fmt.Errorf("DATABASE_URL must be set")
}
redisURL := os.Getenv("REDIS_URL")
if redisURL == "" {
return nil, fmt.Errorf("REDIS_URL must be set")
}
ginMode := getEnv("GIN_MODE", "debug")
cookieSecure, err := strconv.ParseBool(getEnv("COOKIE_SECURE", strconv.FormatBool(ginMode != "debug")))
if err != nil {
return nil, fmt.Errorf("invalid COOKIE_SECURE: %w", err)
}
mediaDriver := getEnv("MEDIA_STORAGE_DRIVER", "local")
if mediaDriver != "local" && mediaDriver != "s3" {
return nil, fmt.Errorf("invalid MEDIA_STORAGE_DRIVER %q: must be \"local\" or \"s3\"", mediaDriver)
}
maxUploadMB, err := strconv.ParseInt(getEnv("MEDIA_MAX_UPLOAD_MB", "10"), 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid MEDIA_MAX_UPLOAD_MB: %w", err)
}
s3UsePathStyle, err := strconv.ParseBool(getEnv("S3_USE_PATH_STYLE", "false"))
if err != nil {
return nil, fmt.Errorf("invalid S3_USE_PATH_STYLE: %w", err)
}
if mediaDriver == "s3" {
for _, key := range []string{"S3_BUCKET", "S3_REGION", "S3_ACCESS_KEY_ID", "S3_SECRET_ACCESS_KEY"} {
if os.Getenv(key) == "" {
return nil, fmt.Errorf("%s must be set when MEDIA_STORAGE_DRIVER=s3", key)
}
}
}
adminNumber, err := strconv.ParseInt(getEnv("ADMIN_NUMBER", "2"), 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid ADMIN_NUMBER: %w", err)
}
return &Config{
Database: DatabaseConfig{
URL: dbURL,
},
Redis: RedisConfig{
URL: redisURL,
},
JWT: JWTConfig{
Secret: secret,
AccessTTL: time.Duration(accessMinutes) * time.Minute,
RefreshTTL: time.Duration(refreshHours) * time.Hour,
},
Server: ServerConfig{
Port: getEnv("PORT", "8080"),
GinMode: ginMode,
CORSOrigin: getEnv("CORS_ORIGIN", "http://localhost:5173"),
CookieSecure: cookieSecure,
},
Seed: SeedConfig{
AdminUsername: os.Getenv("SEED_ADMIN_USERNAME"),
AdminPassword: os.Getenv("SEED_ADMIN_PASSWORD"),
AdminNumber: adminNumber,
},
Media: MediaConfig{
Driver: mediaDriver,
MaxUploadMB: maxUploadMB,
LocalDir: getEnv("MEDIA_LOCAL_DIR", "./uploads"),
LocalBaseURL: getEnv("MEDIA_LOCAL_BASE_URL", "http://localhost:8080/uploads"),
S3Bucket: os.Getenv("S3_BUCKET"),
S3Region: os.Getenv("S3_REGION"),
S3Endpoint: os.Getenv("S3_ENDPOINT"),
S3AccessKeyID: os.Getenv("S3_ACCESS_KEY_ID"),
S3SecretKey: os.Getenv("S3_SECRET_ACCESS_KEY"),
S3UsePathStyle: s3UsePathStyle,
S3PublicBaseURL: os.Getenv("S3_PUBLIC_BASE_URL"),
},
Verification: VerificationConfig{
UploadDir: getEnv("VERIFICATION_UPLOAD_DIR", "./verification-uploads"),
},
}, nil
}
func getEnv(key, fallback string) string {
if v, ok := os.LookupEnv(key); ok && v != "" {
return v
}
return fallback
}