// Package config loads the Control Service configuration from the process // environment. Every secret comes from the environment (environment.d); nothing // is compiled into the binary and nothing is read from a committed file. package config import ( "errors" "fmt" "log/slog" "os" "strconv" "strings" "time" ) // Config is the fully resolved runtime configuration. type Config struct { // Addr is the listen address, for example ":8791". Addr string // DBPath is the SQLite database file, for example "/data/control.db". DBPath string // SessionSecret signs the session cookie. Required, >= 16 bytes. SessionSecret []byte // AdminUsername is the single dashboard login name (Sam). AdminUsername string // AdminPassword is the plaintext password. Optional if AdminPasswordHash is set. AdminPassword string // AdminPasswordHash is a bcrypt hash. Optional if AdminPassword is set. AdminPasswordHash string // CookieSecure sets the Secure attribute on cookies. Keep true behind HTTPS. CookieSecure bool // SessionTTL is how long a session cookie stays valid. SessionTTL time.Duration // LogLevel is the slog level. LogLevel slog.Level // AppEnv is a label such as "production" or "development". AppEnv string // Version is injected at build time. Version string } // Load reads and validates the configuration. func Load() (*Config, error) { cfg := &Config{ Addr: env("CONTROL_ADDR", ":8791"), DBPath: env("CONTROL_DB_PATH", "/data/control.db"), AdminUsername: env("CONTROL_ADMIN_USERNAME", "sam"), AdminPassword: os.Getenv("CONTROL_ADMIN_PASSWORD"), AdminPasswordHash: os.Getenv("CONTROL_ADMIN_PASSWORD_HASH"), CookieSecure: envBool("CONTROL_COOKIE_SECURE", true), AppEnv: env("CONTROL_APP_ENV", "production"), } secret := os.Getenv("CONTROL_SESSION_SECRET") if len(secret) < 16 { return nil, errors.New("CONTROL_SESSION_SECRET must be set and at least 16 characters") } cfg.SessionSecret = []byte(secret) if cfg.AdminPassword == "" && cfg.AdminPasswordHash == "" { return nil, errors.New("set CONTROL_ADMIN_PASSWORD or CONTROL_ADMIN_PASSWORD_HASH") } if cfg.AdminUsername == "" { return nil, errors.New("CONTROL_ADMIN_USERNAME must not be empty") } ttl, err := time.ParseDuration(env("CONTROL_SESSION_TTL", "12h")) if err != nil { return nil, fmt.Errorf("CONTROL_SESSION_TTL: %w", err) } cfg.SessionTTL = ttl lvl, err := parseLevel(env("CONTROL_LOG_LEVEL", "info")) if err != nil { return nil, err } cfg.LogLevel = lvl return cfg, nil } func env(key, fallback string) string { if v := strings.TrimSpace(os.Getenv(key)); v != "" { return v } return fallback } func envBool(key string, fallback bool) bool { v := strings.TrimSpace(os.Getenv(key)) if v == "" { return fallback } b, err := strconv.ParseBool(v) if err != nil { return fallback } return b } func parseLevel(s string) (slog.Level, error) { switch strings.ToLower(strings.TrimSpace(s)) { case "debug": return slog.LevelDebug, nil case "info", "": return slog.LevelInfo, nil case "warn", "warning": return slog.LevelWarn, nil case "error": return slog.LevelError, nil default: return 0, fmt.Errorf("CONTROL_LOG_LEVEL: unknown level %q", s) } }