This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/config/config.go
2026-03-07 17:11:32 +00:00

226 lines
8.3 KiB
Go

package config
import (
"fmt"
"os"
"strconv"
"github.com/joho/godotenv"
)
// Config holds all application configuration.
type Config struct {
Port string
DatabaseURL string
DBDriver string // "postgres" (default) or "sqlite"
JWTSecret string
Environment string
BasePath string // URL path prefix (e.g. "/dev", "/test", or "" for root)
// Admin bootstrap (optional — set via K8s secret)
AdminUsername string
AdminPassword string
AdminEmail string
// Seed users (dev/test only) — CSV: "user:pass:role,user2:pass2:role2"
SeedUsers string
// Seed providers (dev/test only) — CSV: "provider:api_key[:name]"
// Shortcuts: "openai:sk-xxx", "anthropic:sk-ant-xxx", "openrouter:sk-or-xxx"
// Skips if provider with same name exists (idempotent on restart).
SeedProviders string
// API key encryption (required for v0.9.4+)
// Used to derive AES-256 key for global/team provider API keys.
// Personal keys use per-user UEK (derived from password).
EncryptionKey string
// File storage (v0.12.0+)
// STORAGE_BACKEND: "pvc" or "s3". Empty = auto-detect (pvc if path writable).
// STORAGE_PATH: mount point for PVC backend (default /data/storage).
StorageBackend string
StoragePath string
// S3-compatible storage (v0.12.0+)
// Works with AWS S3, MinIO, Ceph RGW, or any S3-compatible API.
S3Endpoint string // custom endpoint URL (required for MinIO/Ceph, optional for AWS)
S3Bucket string // bucket name (required when STORAGE_BACKEND=s3)
S3Region string // AWS region (default "us-east-1")
S3AccessKey string // access key ID
S3SecretKey string // secret access key
S3Prefix string // optional key prefix within bucket (e.g. "switchboard/")
S3ForcePathStyle bool // use path-style URLs (required for MinIO, most self-hosted)
// Extraction pipeline (v0.12.0+)
// EXTRACTION_MODE: "inline" (in-process) or "sidecar" (shared PVC watcher).
// EXTRACTION_CONCURRENCY: max concurrent extraction jobs (default 3).
ExtractionMode string
ExtractionConcurrency int
// Workspace indexing (v0.21.2)
// WORKSPACE_INDEXING_ENABLED: global kill switch for workspace file indexing (default true).
// WORKSPACE_INDEX_CONCURRENCY: max concurrent indexing goroutines (default 2).
WorkspaceIndexingEnabled bool
WorkspaceIndexConcurrency int
// Provider auto-disable (v0.22.4)
// PROVIDER_AUTO_DISABLE_THRESHOLD: consecutive "down" hourly windows before a
// provider is automatically deactivated. Default 3. Set to 0 to disable.
ProviderAutoDisableThreshold int
// Auth mode (v0.24.0): "builtin" (default) | "mtls" | "oidc"
AuthMode string
// mTLS (v0.24.1)
// Headers injected by TLS-terminating reverse proxy.
MTLSHeaderDN string // default "X-SSL-Client-DN"
MTLSHeaderVerify string // default "X-SSL-Client-Verify"
MTLSHeaderFingerprint string // default "X-SSL-Client-Fingerprint"
MTLSAutoActivate bool // auto-activate new users (default true)
MTLSDefaultTeam string // team ID for auto-provisioned users (optional)
MTLSDefaultRole string // "user" (default) or "admin"
// OIDC (v0.24.1)
OIDCIssuerURL string // e.g. "https://keycloak.corp/realms/switchboard"
OIDCExternalIssuerURL string // OIDC_EXTERNAL_ISSUER_URL
OIDCClientID string
OIDCClientSecret string
OIDCRedirectURL string // e.g. "https://switchboard.corp/api/v1/auth/oidc/callback"
OIDCAutoActivate bool // auto-activate new users (default true)
OIDCDefaultTeam string // team ID for auto-provisioned users (optional)
OIDCDefaultRole string // "user" (default) or "admin"
OIDCRolesClaim string // JWT claim for role mapping (default "realm_access.roles")
OIDCGroupsClaim string // JWT claim for group sync (default "groups")
OIDCAdminRole string // IdP role value that maps to admin (default "admin")
}
// Load reads configuration from environment variables.
// A .env file is loaded if present but not required.
func Load() *Config {
// Best-effort .env load (not required in production)
_ = godotenv.Load()
return &Config{
Port: getEnv("PORT", "8080"),
DatabaseURL: resolveDatabaseURL(),
DBDriver: getEnv("DB_DRIVER", ""),
JWTSecret: getEnv("JWT_SECRET", "dev-secret-change-me"),
Environment: getEnv("ENVIRONMENT", "development"),
BasePath: sanitizeBasePath(getEnv("BASE_PATH", "")),
AdminUsername: getEnv("SWITCHBOARD_ADMIN_USERNAME", ""),
AdminPassword: getEnv("SWITCHBOARD_ADMIN_PASSWORD", ""),
AdminEmail: getEnv("SWITCHBOARD_ADMIN_EMAIL", ""),
SeedUsers: getEnv("SEED_USERS", ""),
SeedProviders: getEnv("SEED_PROVIDERS", ""),
EncryptionKey: getEnv("ENCRYPTION_KEY", ""),
StorageBackend: getEnv("STORAGE_BACKEND", ""),
StoragePath: getEnv("STORAGE_PATH", "/data/storage"),
S3Endpoint: getEnv("S3_ENDPOINT", ""),
S3Bucket: getEnv("S3_BUCKET", ""),
S3Region: getEnv("S3_REGION", "us-east-1"),
S3AccessKey: getEnv("S3_ACCESS_KEY", ""),
S3SecretKey: getEnv("S3_SECRET_KEY", ""),
S3Prefix: getEnv("S3_PREFIX", ""),
S3ForcePathStyle: getEnv("S3_FORCE_PATH_STYLE", "true") == "true",
ExtractionMode: getEnv("EXTRACTION_MODE", "inline"),
ExtractionConcurrency: getEnvInt("EXTRACTION_CONCURRENCY", 3),
WorkspaceIndexingEnabled: getEnvBool("WORKSPACE_INDEXING_ENABLED", true),
WorkspaceIndexConcurrency: getEnvInt("WORKSPACE_INDEX_CONCURRENCY", 2),
ProviderAutoDisableThreshold: getEnvInt("PROVIDER_AUTO_DISABLE_THRESHOLD", 3),
AuthMode: getEnv("AUTH_MODE", "builtin"),
// mTLS
MTLSHeaderDN: getEnv("MTLS_HEADER_DN", "X-SSL-Client-DN"),
MTLSHeaderVerify: getEnv("MTLS_HEADER_VERIFY", "X-SSL-Client-Verify"),
MTLSHeaderFingerprint: getEnv("MTLS_HEADER_FINGERPRINT", "X-SSL-Client-Fingerprint"),
MTLSAutoActivate: getEnvBool("MTLS_AUTO_ACTIVATE", true),
MTLSDefaultTeam: getEnv("MTLS_DEFAULT_TEAM", ""),
MTLSDefaultRole: getEnv("MTLS_DEFAULT_ROLE", "user"),
// OIDC
OIDCIssuerURL: getEnv("OIDC_ISSUER_URL", ""),
OIDCExternalIssuerURL: getEnv("OIDC_EXTERNAL_ISSUER_URL", ""),
OIDCClientID: getEnv("OIDC_CLIENT_ID", ""),
OIDCClientSecret: getEnv("OIDC_CLIENT_SECRET", ""),
OIDCRedirectURL: getEnv("OIDC_REDIRECT_URL", ""),
OIDCAutoActivate: getEnvBool("OIDC_AUTO_ACTIVATE", true),
OIDCDefaultTeam: getEnv("OIDC_DEFAULT_TEAM", ""),
OIDCDefaultRole: getEnv("OIDC_DEFAULT_ROLE", "user"),
OIDCRolesClaim: getEnv("OIDC_ROLES_CLAIM", "realm_access.roles"),
OIDCGroupsClaim: getEnv("OIDC_GROUPS_CLAIM", "groups"),
OIDCAdminRole: getEnv("OIDC_ADMIN_ROLE", "admin"),
}
}
// resolveDatabaseURL returns the database connection string.
//
// Resolution order:
// 1. DATABASE_URL — explicit DSN (K8s, .env, manual)
// 2. POSTGRES_* vars — assembled from individual env vars:
// POSTGRES_HOST, POSTGRES_PORT, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB
// (set by K8s manifest and docker-compose)
// 3. Empty string — backend runs without database
func resolveDatabaseURL() string {
if dsn := os.Getenv("DATABASE_URL"); dsn != "" {
return dsn
}
// Assemble from individual vars (K8s sets POSTGRES_*, docker-compose may too)
host := getEnv("POSTGRES_HOST", "")
if host == "" {
return ""
}
port := getEnv("POSTGRES_PORT", "5432")
user := getEnv("POSTGRES_USER", "")
pass := getEnv("POSTGRES_PASSWORD", "")
db := getEnv("POSTGRES_DB", "chat_switchboard")
ssl := getEnv("POSTGRES_SSLMODE", "disable")
return fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=%s", user, pass, host, port, db, ssl)
}
// sanitizeBasePath ensures the path starts with / and doesn't end with /.
// Empty string means root (no prefix).
func sanitizeBasePath(p string) string {
if p == "" || p == "/" {
return ""
}
// Ensure leading slash
if p[0] != '/' {
p = "/" + p
}
// Strip trailing slash
for len(p) > 1 && p[len(p)-1] == '/' {
p = p[:len(p)-1]
}
return p
}
func getEnv(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
func getEnvInt(key string, fallback int) int {
if v := os.Getenv(key); v != "" {
if n, err := strconv.Atoi(v); err == nil {
return n
}
}
return fallback
}
func getEnvBool(key string, fallback bool) bool {
if v := os.Getenv(key); v != "" {
b, err := strconv.ParseBool(v)
if err == nil {
return b
}
}
return fallback
}