167 lines
4.5 KiB
Go
167 lines
4.5 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
|
|
}
|
|
|
|
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 {
|
|
AdminEmail string
|
|
AdminPassword string
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
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{
|
|
AdminEmail: os.Getenv("SEED_ADMIN_EMAIL"),
|
|
AdminPassword: os.Getenv("SEED_ADMIN_PASSWORD"),
|
|
},
|
|
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"),
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func getEnv(key, fallback string) string {
|
|
if v, ok := os.LookupEnv(key); ok && v != "" {
|
|
return v
|
|
}
|
|
return fallback
|
|
}
|