package main import ( "os" "strconv" "strings" "time" ) type config struct { Port string WebDir string AppBaseURL string MCPResourceURL string CorsOrigins []string DatabaseURL string MinIOEndpoint string MinIOBucket string MinIOAccessKey string MinIOSecretKey string AssetEncryptionEnabled bool AuthDisabled bool KeycloakIssuer string KeycloakIssuerInternal string KeycloakAudience string StripeSecretKey string StripeAPIBaseURL string StripeWebhookSecret string StripeWebhookTolerance time.Duration StripeAllowedPriceIDs []string StripeCheckoutSuccessURL string StripeCheckoutCancelURL string BillingEventRetentionDays int EntitlementHistoryRetentionDays int EducationalRecordRetentionDays int DefaultTrialDays int DefaultFreeDays int KeycloakThemeDeployEnabled bool NotificationProvider string SendGridAPIKey string SendGridFromEmail string SendGridAPIBaseURL string ShutdownTimeout time.Duration } func loadConfig() config { return config{ Port: getEnv("PORT", "8080"), WebDir: strings.TrimSpace(getEnv("WEB_DIR", "")), AppBaseURL: strings.TrimRight(getEnv("APP_BASE_URL", "http://localhost:5173"), "/"), MCPResourceURL: strings.TrimRight(getEnv("MCP_RESOURCE_URL", "http://localhost:8080/api/mcp"), "/"), CorsOrigins: parseList(getEnv("CORS_ALLOWED_ORIGINS", "*")), DatabaseURL: getEnv("DATABASE_URL", "postgres://cms:cms@postgres:5432/cms?sslmode=disable"), MinIOEndpoint: getEnv("MINIO_ENDPOINT", ""), MinIOBucket: getEnv("MINIO_BUCKET", ""), MinIOAccessKey: getEnv("MINIO_ACCESS_KEY", ""), MinIOSecretKey: getEnv("MINIO_SECRET_KEY", ""), AssetEncryptionEnabled: getEnvBool("ASSET_ENCRYPTION_ENABLED", false), AuthDisabled: getEnvBool("AUTH_DISABLED", false), KeycloakIssuer: getEnv("KEYCLOAK_ISSUER", "http://localhost:8081/realms/cms"), KeycloakIssuerInternal: getEnv("KEYCLOAK_ISSUER_INTERNAL", "http://keycloak:8080/realms/cms"), KeycloakAudience: getEnv("KEYCLOAK_AUDIENCE", "cms-backend"), StripeSecretKey: getEnv("STRIPE_SECRET_KEY", ""), StripeAPIBaseURL: getEnv("STRIPE_API_BASE_URL", "https://api.stripe.com"), StripeWebhookSecret: getEnv("STRIPE_WEBHOOK_SECRET", ""), StripeWebhookTolerance: time.Duration(getEnvInt("STRIPE_WEBHOOK_TOLERANCE_SECONDS", 300)) * time.Second, StripeAllowedPriceIDs: parseOptionalList(getEnv("STRIPE_ALLOWED_PRICE_IDS", "")), StripeCheckoutSuccessURL: getEnv("STRIPE_CHECKOUT_SUCCESS_URL", "http://localhost:5173/learn/billing?checkout=success"), StripeCheckoutCancelURL: getEnv("STRIPE_CHECKOUT_CANCEL_URL", "http://localhost:5173/learn/billing?checkout=cancel"), BillingEventRetentionDays: getEnvInt("BILLING_EVENT_RETENTION_DAYS", 365), EntitlementHistoryRetentionDays: getEnvInt("ENTITLEMENT_HISTORY_RETENTION_DAYS", 730), EducationalRecordRetentionDays: getEnvInt("EDUCATIONAL_RECORD_RETENTION_DAYS", 2555), DefaultTrialDays: getEnvInt("DEFAULT_TRIAL_DAYS", 14), DefaultFreeDays: getEnvInt("DEFAULT_FREE_ACCESS_DAYS", 3650), KeycloakThemeDeployEnabled: getEnvBool("KEYCLOAK_THEME_DEPLOY_ENABLED", true), NotificationProvider: strings.ToLower(getEnv("NOTIFICATION_PROVIDER", "noop")), SendGridAPIKey: getEnv("SENDGRID_API_KEY", ""), SendGridFromEmail: getEnv("SENDGRID_FROM_EMAIL", ""), SendGridAPIBaseURL: getEnv("SENDGRID_API_BASE_URL", "https://api.sendgrid.com"), ShutdownTimeout: time.Duration(getEnvInt("SHUTDOWN_TIMEOUT_SECONDS", 10)) * time.Second, } } func getEnv(key, fallback string) string { value, ok := os.LookupEnv(key) if !ok || strings.TrimSpace(value) == "" { return fallback } return value } func getEnvInt(key string, fallback int) int { raw := strings.TrimSpace(getEnv(key, "")) if raw == "" { return fallback } value, err := strconv.Atoi(raw) if err != nil { return fallback } return value } func getEnvBool(key string, fallback bool) bool { raw := strings.TrimSpace(strings.ToLower(getEnv(key, ""))) if raw == "" { return fallback } return raw == "1" || raw == "true" || raw == "yes" || raw == "on" } func parseList(raw string) []string { if strings.TrimSpace(raw) == "" { return []string{"*"} } parts := strings.Split(raw, ",") out := make([]string, 0, len(parts)) for _, part := range parts { item := strings.TrimSpace(part) if item != "" { out = append(out, item) } } if len(out) == 0 { return []string{"*"} } return out } func parseOptionalList(raw string) []string { if strings.TrimSpace(raw) == "" { return []string{} } parts := strings.Split(raw, ",") out := make([]string, 0, len(parts)) for _, part := range parts { item := strings.TrimSpace(part) if item != "" { out = append(out, item) } } return out }