// Package config holds all runtime configuration for the backend. // Values are parsed from environment variables via struct tags. package config import ( "fmt" "time" "github.com/caarlos0/env/v11" ) // Config is the single source of runtime configuration. type Config struct { // HTTPAddr is the address the HTTP server listens on, e.g. ":8080". HTTPAddr string `env:"HTTP_ADDR" envDefault:":8080"` // AppEnv: "development" or "production". AppEnv string `env:"APP_ENV" envDefault:"development"` // LogLevel: debug | info | warn | error. LogLevel string `env:"LOG_LEVEL" envDefault:"info"` // PublicBaseURL is the externally reachable URL (scheme + host), no trailing slash. // Used e.g. for OIDC redirect URIs. Example: "https://mitbringsl.example.com". PublicBaseURL string `env:"PUBLIC_BASE_URL" envDefault:"http://localhost:8080"` // Database DatabaseURL string `env:"DATABASE_URL,required"` DBMaxOpenConns int32 `env:"DB_MAX_OPEN_CONNS" envDefault:"25"` DBMaxIdleConns int32 `env:"DB_MAX_IDLE_CONNS" envDefault:"5"` DBMaxLifetime time.Duration `env:"DB_MAX_CONN_LIFETIME" envDefault:"30m"` // Session token config (opaque tokens issued by this backend after login). SessionTokenBytes int `env:"SESSION_TOKEN_BYTES" envDefault:"32"` // 256-bit SessionTokenTTL time.Duration `env:"SESSION_TOKEN_TTL" envDefault:"720h"` // 30 days SessionCookieName string `env:"SESSION_COOKIE_NAME" envDefault:"mitbringsl_session"` // AuthPasswordEnabled controls whether email/password registration and // login are offered. Set to false to enforce OIDC-only login. AuthPasswordEnabled bool `env:"AUTH_PASSWORD_ENABLED" envDefault:"true"` // OIDC providers. Both optional; enable per provider. GoogleOIDC GoogleOIDCConfig GenericOIDC GenericOIDCConfig // CORS allowed origins (comma-separated). Empty = no CORS headers. CORSAllowedOrigins []string `env:"CORS_ALLOWED_ORIGINS" envSeparator:","` } // GoogleOIDCConfig for "Sign in with Google". type GoogleOIDCConfig struct { Enabled bool `env:"OIDC_GOOGLE_ENABLED" envDefault:"false"` ClientID string `env:"OIDC_GOOGLE_CLIENT_ID"` // the audience the verifier accepts Issuer string `env:"OIDC_GOOGLE_ISSUER" envDefault:"https://accounts.google.com"` } // GenericOIDCConfig for any standards-compliant OIDC IdP (Keycloak, Authentik, ...). type GenericOIDCConfig struct { Enabled bool `env:"OIDC_GENERIC_ENABLED" envDefault:"false"` Issuer string `env:"OIDC_GENERIC_ISSUER"` ClientID string `env:"OIDC_GENERIC_CLIENT_ID"` // DisplayName is shown to users in the app, e.g. "Authentik" or "Keycloak". // Defaults to "OIDC" when empty. DisplayName string `env:"OIDC_GENERIC_DISPLAY_NAME" envDefault:"OIDC"` } // Load reads configuration from environment variables and validates basic invariants. func Load() (*Config, error) { var cfg Config if err := env.Parse(&cfg); err != nil { return nil, fmt.Errorf("parse env: %w", err) } if err := cfg.validate(); err != nil { return nil, err } return &cfg, nil } // validate enforces invariants that env parsing alone can't express. func (c *Config) validate() error { if c.GoogleOIDC.Enabled { if c.GoogleOIDC.ClientID == "" { return fmt.Errorf("OIDC_GOOGLE_ENABLED=true requires OIDC_GOOGLE_CLIENT_ID") } if c.GoogleOIDC.Issuer == "" { return fmt.Errorf("OIDC_GOOGLE_ENABLED=true requires OIDC_GOOGLE_ISSUER") } } if c.GenericOIDC.Enabled { if c.GenericOIDC.ClientID == "" { return fmt.Errorf("OIDC_GENERIC_ENABLED=true requires OIDC_GENERIC_CLIENT_ID") } if c.GenericOIDC.Issuer == "" { return fmt.Errorf("OIDC_GENERIC_ENABLED=true requires OIDC_GENERIC_ISSUER") } } return nil } // IsProduction reports whether the app runs in production mode. func (c *Config) IsProduction() bool { return c.AppEnv == "production" }