package app import ( "errors" "fmt" "net/url" "os" "strconv" "strings" "time" ) const ( defaultAppMode = "development" defaultAPIPort = "8080" defaultShutdownTimeout = 10 * time.Second ) var ( defaultLocalCORSOrigins = []string{ "http://localhost:3000", "http://127.0.0.1:3000", "http://localhost:3001", "http://127.0.0.1:3001", } insecureSecretPlaceholders = map[string]struct{}{ "": {}, "replace-me-with-a-long-random-secret": {}, "replace-me-with-a-dedicated-mail-secret": {}, "productier-local-mail-key": {}, "changeme": {}, "change-me": {}, "replace-me": {}, } ) type runtimeConfig struct { mode string apiPort string authServiceURL string shutdownTimeout time.Duration corsAllowOrigins []string mailSecret string metricsAuthToken string } func loadRuntimeConfig() (runtimeConfig, error) { mode, err := parseAppMode(os.Getenv("APP_ENV")) if err != nil { return runtimeConfig{}, err } apiPort, err := parsePort(os.Getenv("API_PORT"), defaultAPIPort, "API_PORT") if err != nil { return runtimeConfig{}, err } authServiceURL, err := parseAbsoluteHTTPURL( valueOrDefault(strings.TrimSpace(os.Getenv("AUTH_SERVICE_URL")), "http://localhost:3001"), "AUTH_SERVICE_URL", ) if err != nil { return runtimeConfig{}, err } shutdownTimeout, err := parseDuration( os.Getenv("API_SHUTDOWN_TIMEOUT"), defaultShutdownTimeout, "API_SHUTDOWN_TIMEOUT", ) if err != nil { return runtimeConfig{}, err } corsAllowOrigins, err := parseCORSAllowOrigins(mode, os.Getenv("CORS_ALLOW_ORIGINS")) if err != nil { return runtimeConfig{}, err } mailSecret, err := resolveMailSecret(mode) if err != nil { return runtimeConfig{}, err } metricsAuthToken, err := resolveMetricsAuthToken(mode) if err != nil { return runtimeConfig{}, err } return runtimeConfig{ mode: mode, apiPort: apiPort, authServiceURL: authServiceURL, shutdownTimeout: shutdownTimeout, corsAllowOrigins: corsAllowOrigins, mailSecret: mailSecret, metricsAuthToken: metricsAuthToken, }, nil } func parseAppMode(raw string) (string, error) { mode := strings.TrimSpace(strings.ToLower(raw)) if mode == "" { return defaultAppMode, nil } switch mode { case "development", "test", "staging", "production": return mode, nil default: return "", fmt.Errorf("unsupported APP_ENV %q (allowed: development, test, staging, production)", mode) } } func parsePort(raw string, fallback string, envName string) (string, error) { port := strings.TrimSpace(raw) if port == "" { port = fallback } numeric, err := strconv.Atoi(port) if err != nil || numeric < 1 || numeric > 65535 { return "", fmt.Errorf("%s must be a valid TCP port (1-65535)", envName) } return strconv.Itoa(numeric), nil } func parseDuration(raw string, fallback time.Duration, envName string) (time.Duration, error) { value := strings.TrimSpace(raw) if value == "" { return fallback, nil } duration, err := time.ParseDuration(value) if err != nil { return 0, fmt.Errorf("%s must be a valid duration (example: 10s): %w", envName, err) } if duration <= 0 { return 0, fmt.Errorf("%s must be greater than zero", envName) } return duration, nil } func parseAbsoluteHTTPURL(raw string, envName string) (string, error) { value := strings.TrimSpace(raw) if value == "" { return "", fmt.Errorf("%s is required", envName) } parsed, err := url.Parse(value) if err != nil { return "", fmt.Errorf("%s must be a valid URL: %w", envName, err) } if parsed.Scheme != "http" && parsed.Scheme != "https" { return "", fmt.Errorf("%s must use http or https", envName) } if parsed.Host == "" { return "", fmt.Errorf("%s must include a host", envName) } return strings.TrimRight(parsed.String(), "/"), nil } func parseCORSAllowOrigins(mode string, raw string) ([]string, error) { value := strings.TrimSpace(raw) if value == "" { if mode == "staging" || mode == "production" { return nil, errors.New("CORS_ALLOW_ORIGINS is required in staging/production (comma-separated origins)") } return append([]string(nil), defaultLocalCORSOrigins...), nil } parts := strings.Split(value, ",") origins := make([]string, 0, len(parts)) seen := make(map[string]struct{}, len(parts)) for _, part := range parts { origin := strings.TrimSpace(part) if origin == "" { continue } if origin == "*" { return nil, errors.New("CORS_ALLOW_ORIGINS cannot include '*' when credentials are enabled") } validated, err := parseOrigin(origin, "CORS_ALLOW_ORIGINS") if err != nil { return nil, err } if _, exists := seen[validated]; exists { continue } seen[validated] = struct{}{} origins = append(origins, validated) } if len(origins) == 0 { return nil, errors.New("CORS_ALLOW_ORIGINS must include at least one valid origin") } return origins, nil } func parseOrigin(raw string, envName string) (string, error) { parsed, err := url.Parse(strings.TrimSpace(raw)) if err != nil { return "", fmt.Errorf("%s must contain valid origins: %w", envName, err) } if parsed.Scheme != "http" && parsed.Scheme != "https" { return "", fmt.Errorf("%s origins must use http or https", envName) } if parsed.Host == "" { return "", fmt.Errorf("%s origins must include a host", envName) } if parsed.Path != "" && parsed.Path != "/" { return "", fmt.Errorf("%s origins cannot include URL paths", envName) } if parsed.RawQuery != "" || parsed.Fragment != "" { return "", fmt.Errorf("%s origins cannot include query or fragment components", envName) } return parsed.Scheme + "://" + parsed.Host, nil } func resolveMailSecret(mode string) (string, error) { mailSecret := strings.TrimSpace(os.Getenv("MAIL_ENCRYPTION_KEY")) if mailSecret == "" { mailSecret = strings.TrimSpace(os.Getenv("BETTER_AUTH_SECRET")) } if mode != "staging" && mode != "production" { return mailSecret, nil } if isInsecureSecret(mailSecret) { return "", errors.New("set a strong MAIL_ENCRYPTION_KEY (or BETTER_AUTH_SECRET fallback) for staging/production") } return mailSecret, nil } func resolveMetricsAuthToken(mode string) (string, error) { token := strings.TrimSpace(os.Getenv("METRICS_AUTH_TOKEN")) if token == "" { return "", nil } if mode == "production" && isInsecureSecret(token) { return "", errors.New("METRICS_AUTH_TOKEN must be a strong non-placeholder secret when set in production") } return token, nil } func isInsecureSecret(secret string) bool { normalized := strings.TrimSpace(strings.ToLower(secret)) if _, exists := insecureSecretPlaceholders[normalized]; exists { return true } return len(strings.TrimSpace(secret)) < 16 } func valueOrDefault(value string, fallback string) string { if strings.TrimSpace(value) == "" { return fallback } return value }