first
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user