init push
This commit is contained in:
928
internal/config/config.go
Normal file
928
internal/config/config.go
Normal file
@@ -0,0 +1,928 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
AppPort string
|
||||
AppEnv string
|
||||
|
||||
SAMLCertFile string
|
||||
SAMLKeyFile string
|
||||
SAMLMetadataFile string
|
||||
SAMLRootURL string
|
||||
|
||||
RedisAddr string
|
||||
RedisPassword string
|
||||
RedisDB int
|
||||
|
||||
FrontendURL string
|
||||
|
||||
CookieDomain string
|
||||
CookieSecure bool
|
||||
SessionTTLHour int
|
||||
}
|
||||
|
||||
type SAMLConfig struct {
|
||||
CertFile string
|
||||
KeyFile string
|
||||
MetadataFile string
|
||||
RootURL string
|
||||
FrontendURL string
|
||||
CookieDomain string
|
||||
CookieSecure bool
|
||||
SessionTTLHr int
|
||||
}
|
||||
|
||||
func LoadEnvConfig() Config {
|
||||
return Config{
|
||||
AppPort: envString("APP_PORT", "3000"),
|
||||
AppEnv: envString("APP_ENV", "development"),
|
||||
SAMLCertFile: envString("SAML_CERT_FILE", "configs/saml.crt"),
|
||||
SAMLKeyFile: envString("SAML_KEY_FILE", "configs/saml.key"),
|
||||
SAMLMetadataFile: envString("SAML_METADATA_FILE", "configs/metadata.xml"),
|
||||
SAMLRootURL: envString("SAML_ROOT_URL", "https://api-wfm-dev.mybit.co.id"),
|
||||
RedisAddr: envString("REDIS_ADDR", "localhost:6379"),
|
||||
RedisPassword: envString("REDIS_PASSWORD", ""),
|
||||
RedisDB: envInt("REDIS_DB", 0),
|
||||
FrontendURL: envString("FRONTEND_URL", "https://wfm-dev.mybit.co.id"),
|
||||
CookieDomain: strings.TrimSpace(os.Getenv("COOKIE_DOMAIN")),
|
||||
CookieSecure: envBool("COOKIE_SECURE", true),
|
||||
SessionTTLHour: envInt("SESSION_TTL_HOUR", 8),
|
||||
}
|
||||
}
|
||||
|
||||
func envString(key, fallback string) string {
|
||||
v := strings.TrimSpace(os.Getenv(key))
|
||||
if v == "" {
|
||||
return fallback
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func envInt(key string, fallback int) int {
|
||||
v := strings.TrimSpace(os.Getenv(key))
|
||||
if v == "" {
|
||||
return fallback
|
||||
}
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type MySQLConfig struct {
|
||||
DSN string
|
||||
MaxOpenConns int
|
||||
MaxIdleConns int
|
||||
ConnMaxLifetime time.Duration
|
||||
ConnMaxIdleTime time.Duration
|
||||
}
|
||||
|
||||
type AuthConfig struct {
|
||||
PublicBaseURL string
|
||||
PasswordPepper string
|
||||
IPEncryptionKey string
|
||||
HashTime uint32
|
||||
HashMemoryKB uint32
|
||||
HashThreads uint8
|
||||
HashKeyLen uint32
|
||||
EmailVerifyTTL time.Duration
|
||||
EmailVerifyURL string
|
||||
InviteTTL time.Duration
|
||||
SetPasswordURL string
|
||||
PasswordResetTTL time.Duration
|
||||
PasswordResetURL string
|
||||
SecurityPINResetTTL time.Duration
|
||||
SecurityPINResetURL string
|
||||
ForgotSecurityPINCooldown time.Duration
|
||||
ForgotSecurityPINMinDuration time.Duration
|
||||
SecurityPINMaxAttempts int
|
||||
SecurityPINLockDuration time.Duration
|
||||
SecurityPINActionTokenTTL time.Duration
|
||||
ForgotPasswordEmailCooldown time.Duration
|
||||
ForgotPasswordMinDuration time.Duration
|
||||
ForgotPasswordIPRateMax int
|
||||
ForgotPasswordIPRateWindow time.Duration
|
||||
LoginIPRateMax int
|
||||
LoginIPRateWindow time.Duration
|
||||
TOTPChallengeTTL time.Duration
|
||||
EmailOTPChallengeTTL time.Duration
|
||||
EmailOTPLength int
|
||||
EmailOTPExpiry time.Duration
|
||||
EmailOTPMaxAttempts int
|
||||
EmailOTPResendCooldown time.Duration
|
||||
EmailOTPMaxResendPerHour int
|
||||
SSOStateTTL time.Duration
|
||||
TOTPIssuer string
|
||||
TOTPSecretKeyB64 string
|
||||
JWTAccessSecret string
|
||||
JWTRefreshSecret string
|
||||
JWTAccessTTL time.Duration
|
||||
JWTRefreshTTL time.Duration
|
||||
JWTRefreshTOTPTTL time.Duration
|
||||
JWTLocalRefreshTTL time.Duration
|
||||
JWTSSORefreshTTL time.Duration
|
||||
SessionIdleTTL time.Duration
|
||||
JWTCookieDomain string
|
||||
JWTCookieSecure bool
|
||||
JWTCookieSameSite string
|
||||
JWTAccessCookieName string
|
||||
JWTRefreshCookieName string
|
||||
WebAuthnRPID string
|
||||
WebAuthnRPDisplayName string
|
||||
WebAuthnRPOrigins []string
|
||||
WebAuthnChallengeTTL time.Duration
|
||||
SSOSuccessRedirectURL string
|
||||
DefaultRoleName string
|
||||
DisableRegister bool
|
||||
}
|
||||
|
||||
type SESConfig struct {
|
||||
Region string
|
||||
Endpoint string
|
||||
From string
|
||||
ConfigurationSetName string
|
||||
SendTimeout time.Duration
|
||||
AllowInsecureEndpoint bool
|
||||
}
|
||||
|
||||
type EmailDeliveryConfig struct {
|
||||
Provider string
|
||||
MockDir string
|
||||
}
|
||||
|
||||
type MicrosoftSSOConfig struct {
|
||||
TenantID string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
RedirectURL string
|
||||
Authority string
|
||||
Scopes []string
|
||||
}
|
||||
|
||||
type HTTPConfig struct {
|
||||
BodyLimit int
|
||||
ReadTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
IdleTimeout time.Duration
|
||||
RequestTimeout time.Duration
|
||||
RequestTimeoutByPath map[string]time.Duration
|
||||
ShutdownTimeout time.Duration
|
||||
CORSAllowOrigins string
|
||||
CORSAllowMethods string
|
||||
CORSAllowHeaders string
|
||||
CORSExposeHeaders string
|
||||
CORSAllowCredentials bool
|
||||
CORSMaxAge int
|
||||
RateLimitMax int
|
||||
RateLimitWindow time.Duration
|
||||
RateLimitByEndpoint map[string]RateLimitRule
|
||||
HSTSMaxAge int
|
||||
}
|
||||
|
||||
type RateLimitRule struct {
|
||||
Max int
|
||||
Window time.Duration
|
||||
}
|
||||
|
||||
type AuditConfig struct {
|
||||
QueueSize int
|
||||
Workers int
|
||||
}
|
||||
|
||||
type LoggingConfig struct {
|
||||
Level string
|
||||
Format string
|
||||
}
|
||||
|
||||
type CircuitBreakerPolicy struct {
|
||||
Enabled bool
|
||||
MaxRequests uint32
|
||||
Interval time.Duration
|
||||
BucketPeriod time.Duration
|
||||
Timeout time.Duration
|
||||
MinRequests uint32
|
||||
FailureRatio float64
|
||||
ConsecutiveFailures uint32
|
||||
}
|
||||
|
||||
type ResilienceConfig struct {
|
||||
Default CircuitBreakerPolicy
|
||||
MicrosoftSSO CircuitBreakerPolicy
|
||||
SES CircuitBreakerPolicy
|
||||
SQS CircuitBreakerPolicy
|
||||
}
|
||||
|
||||
type QueueConfig struct {
|
||||
MessageSchemaVersion string
|
||||
EnableLegacyPayloadFallback bool
|
||||
IdempotencyKeyPrefix string
|
||||
IdempotencyTTL time.Duration
|
||||
ProcessingTTL time.Duration
|
||||
WorkerConcurrency int
|
||||
WorkerMaxBatchSize int
|
||||
WorkerProcessTimeout time.Duration
|
||||
WorkerShutdownTimeout time.Duration
|
||||
WorkerBackoffMin time.Duration
|
||||
WorkerBackoffMax time.Duration
|
||||
WorkerPermanentFailureDelay time.Duration
|
||||
WorkerMonitorAddr string
|
||||
WorkerHealthErrorThreshold int
|
||||
WorkerDepthPollInterval time.Duration
|
||||
OutboxEnabled bool
|
||||
OutboxPollInterval time.Duration
|
||||
OutboxBatchSize int
|
||||
OutboxDispatchTimeout time.Duration
|
||||
OutboxLockTTL time.Duration
|
||||
OutboxMaxAttempts int
|
||||
}
|
||||
|
||||
type FileQueueConfig struct {
|
||||
MessageSchemaVersion string
|
||||
IdempotencyKeyPrefix string
|
||||
IdempotencyTTL time.Duration
|
||||
ProcessingTTL time.Duration
|
||||
WorkerConcurrency int
|
||||
WorkerMaxBatchSize int
|
||||
WorkerProcessTimeout time.Duration
|
||||
WorkerShutdownTimeout time.Duration
|
||||
WorkerBackoffMin time.Duration
|
||||
WorkerBackoffMax time.Duration
|
||||
WorkerPermanentFailureDelay time.Duration
|
||||
WorkerMonitorAddr string
|
||||
WorkerHealthErrorThreshold int
|
||||
WorkerDepthPollInterval time.Duration
|
||||
OutboxEnabled bool
|
||||
OutboxPollInterval time.Duration
|
||||
OutboxBatchSize int
|
||||
OutboxDispatchTimeout time.Duration
|
||||
OutboxLockTTL time.Duration
|
||||
OutboxMaxAttempts int
|
||||
}
|
||||
|
||||
type SQSConfig struct {
|
||||
Region string
|
||||
AccessKeyID string
|
||||
SecretAccessKey string
|
||||
Endpoint string
|
||||
QueueURL string
|
||||
QueueType string
|
||||
MessageGroupID string
|
||||
MaxMessages int
|
||||
WaitTimeSeconds int32
|
||||
VisibilityTimeout time.Duration
|
||||
AllowInsecureEndpoint bool
|
||||
}
|
||||
|
||||
type FileStorageConfig struct {
|
||||
Provider string
|
||||
Bucket string
|
||||
Region string
|
||||
Endpoint string
|
||||
AccessKeyID string
|
||||
SecretAccessKey string
|
||||
ForcePathStyle bool
|
||||
AllowInsecureEndpoint bool
|
||||
AutoCreateBucket bool
|
||||
ObjectKeyPrefix string
|
||||
PresignPutTTL time.Duration
|
||||
PresignGetTTL time.Duration
|
||||
}
|
||||
|
||||
type FileManagerUploadPolicyConfig struct {
|
||||
MaxFileSizeBytes int64
|
||||
AllowedMimeTypes []string
|
||||
AllowedExtensions []string
|
||||
AllowEmptyFile bool
|
||||
MaxFilenameLength int
|
||||
}
|
||||
|
||||
type FileManagerTrashPolicyConfig struct {
|
||||
AutoPurgeEnabled bool
|
||||
Retention time.Duration
|
||||
AutoPurgeInterval time.Duration
|
||||
AutoPurgeBatchSize int
|
||||
}
|
||||
|
||||
type FileManagerLifecycleConfig struct {
|
||||
EnableS3Tagging bool
|
||||
EnableCleanupJob bool
|
||||
TempUploadGraceHours int
|
||||
OrphanGraceDays int
|
||||
CleanupBatchSize int
|
||||
CleanupDryRun bool
|
||||
}
|
||||
|
||||
type WOPIConfig struct {
|
||||
TokenSecret string
|
||||
ProofSecret string
|
||||
DiscoveryURL string
|
||||
PublicBaseURL string
|
||||
EditActionURL string
|
||||
TokenTTL time.Duration
|
||||
MaxBodyBytes int
|
||||
RequireProof bool
|
||||
}
|
||||
|
||||
type GotenbergConfig struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
func LoadMySQLConfigFromEnv() MySQLConfig {
|
||||
return MySQLConfig{
|
||||
DSN: getenv("MYSQL_DSN", ""),
|
||||
MaxOpenConns: getenvInt("MYSQL_MAX_OPEN_CONNS", 25),
|
||||
MaxIdleConns: getenvInt("MYSQL_MAX_IDLE_CONNS", 10),
|
||||
ConnMaxLifetime: getenvDuration("MYSQL_CONN_MAX_LIFETIME", 5*time.Minute),
|
||||
ConnMaxIdleTime: getenvDuration("MYSQL_CONN_MAX_IDLE_TIME", 2*time.Minute),
|
||||
}
|
||||
}
|
||||
|
||||
func LoadAuthConfigFromEnv() AuthConfig {
|
||||
refreshTTL := getenvDuration("AUTH_JWT_REFRESH_TTL", time.Hour)
|
||||
refreshTOTPTTL := getenvDuration("AUTH_JWT_REFRESH_TOTP_TTL", refreshTTL)
|
||||
passwordPepper := getenv("AUTH_PASSWORD_PEPPER", "")
|
||||
ipEncryptionKey := getenv("AUTH_IP_ENCRYPTION_KEY", getenv("AUTH_IP_HMAC_KEY", passwordPepper))
|
||||
|
||||
return AuthConfig{
|
||||
PublicBaseURL: getenv("APP_PUBLIC_BASE_URL", ""),
|
||||
PasswordPepper: passwordPepper,
|
||||
IPEncryptionKey: ipEncryptionKey,
|
||||
HashTime: uint32(getenvInt("AUTH_HASH_TIME", 1)),
|
||||
HashMemoryKB: uint32(getenvInt("AUTH_HASH_MEMORY_KB", 64*1024)),
|
||||
HashThreads: uint8(getenvInt("AUTH_HASH_THREADS", 4)),
|
||||
HashKeyLen: uint32(getenvInt("AUTH_HASH_KEY_LEN", 32)),
|
||||
EmailVerifyTTL: getenvDuration("AUTH_EMAIL_VERIFY_TTL", 24*time.Hour),
|
||||
EmailVerifyURL: getenv("AUTH_EMAIL_VERIFY_URL", ""),
|
||||
InviteTTL: getenvDuration("AUTH_INVITE_TTL", 24*time.Hour),
|
||||
SetPasswordURL: getenv("AUTH_SET_PASSWORD_URL", getenv("AUTH_EMAIL_VERIFY_URL", "")),
|
||||
PasswordResetTTL: getenvDuration("AUTH_PASSWORD_RESET_TTL", 20*time.Minute),
|
||||
PasswordResetURL: getenv("AUTH_PASSWORD_RESET_URL", getenv("AUTH_SET_PASSWORD_URL", "")),
|
||||
SecurityPINResetTTL: getenvDuration("AUTH_SECURITY_PIN_RESET_TTL", 20*time.Minute),
|
||||
SecurityPINResetURL: getenv("AUTH_SECURITY_PIN_RESET_URL", getenv("AUTH_PASSWORD_RESET_URL", getenv("AUTH_SET_PASSWORD_URL", ""))),
|
||||
ForgotSecurityPINCooldown: getenvDuration("AUTH_FORGOT_SECURITY_PIN_EMAIL_COOLDOWN", 2*time.Minute),
|
||||
ForgotSecurityPINMinDuration: getenvDuration("AUTH_FORGOT_SECURITY_PIN_MIN_DURATION", 150*time.Millisecond),
|
||||
SecurityPINMaxAttempts: getenvInt("AUTH_SECURITY_PIN_MAX_ATTEMPTS", 5),
|
||||
SecurityPINLockDuration: getenvDuration("AUTH_SECURITY_PIN_LOCK_DURATION", 0),
|
||||
SecurityPINActionTokenTTL: getenvDuration("AUTH_SECURITY_PIN_ACTION_TOKEN_TTL", 5*time.Minute),
|
||||
ForgotPasswordEmailCooldown: getenvDuration("AUTH_FORGOT_PASSWORD_EMAIL_COOLDOWN", 2*time.Minute),
|
||||
ForgotPasswordMinDuration: getenvDuration("AUTH_FORGOT_PASSWORD_MIN_DURATION", 150*time.Millisecond),
|
||||
ForgotPasswordIPRateMax: getenvInt("AUTH_FORGOT_PASSWORD_IP_RATE_MAX", 10),
|
||||
ForgotPasswordIPRateWindow: getenvDuration("AUTH_FORGOT_PASSWORD_IP_RATE_WINDOW", 15*time.Minute),
|
||||
LoginIPRateMax: getenvInt("AUTH_LOGIN_IP_RATE_MAX", 5),
|
||||
LoginIPRateWindow: getenvDuration("AUTH_LOGIN_IP_RATE_WINDOW", time.Minute),
|
||||
TOTPChallengeTTL: getenvDuration("AUTH_TOTP_CHALLENGE_TTL", 5*time.Minute),
|
||||
EmailOTPChallengeTTL: getenvDuration("AUTH_EMAIL_OTP_CHALLENGE_TTL", 10*time.Minute),
|
||||
EmailOTPLength: getenvInt("AUTH_EMAIL_OTP_LENGTH", 6),
|
||||
EmailOTPExpiry: getenvDuration("AUTH_EMAIL_OTP_EXPIRY", 10*time.Minute),
|
||||
EmailOTPMaxAttempts: getenvInt("AUTH_EMAIL_OTP_MAX_ATTEMPTS", 5),
|
||||
EmailOTPResendCooldown: getenvDuration("AUTH_EMAIL_OTP_RESEND_COOLDOWN", 60*time.Second),
|
||||
EmailOTPMaxResendPerHour: getenvInt("AUTH_EMAIL_OTP_MAX_RESEND_PER_HOUR", 5),
|
||||
SSOStateTTL: getenvDuration("AUTH_SSO_STATE_TTL", 10*time.Minute),
|
||||
TOTPIssuer: getenv("TOTP_ISSUER", "Wucher"),
|
||||
TOTPSecretKeyB64: getenv("TOTP_SECRET_KEY", ""),
|
||||
JWTAccessSecret: getenv("AUTH_JWT_ACCESS_SECRET", ""),
|
||||
JWTRefreshSecret: getenv("AUTH_JWT_REFRESH_SECRET", ""),
|
||||
JWTAccessTTL: getenvDuration("AUTH_JWT_ACCESS_TTL", 10*time.Minute),
|
||||
JWTRefreshTTL: refreshTTL,
|
||||
JWTRefreshTOTPTTL: refreshTOTPTTL,
|
||||
JWTLocalRefreshTTL: getenvDuration("AUTH_JWT_LOCAL_REFRESH_TTL", 24*time.Hour),
|
||||
// Fallback lifetime for Microsoft/SSO sessions when Microsoft does not
|
||||
// report refresh_token_expires_in. Defaults to Microsoft's default
|
||||
// refresh-token lifetime (90 days); the actual session follows the
|
||||
// value Microsoft returns whenever it is present.
|
||||
JWTSSORefreshTTL: getenvDuration("AUTH_JWT_SSO_REFRESH_TTL", 90*24*time.Hour),
|
||||
SessionIdleTTL: getenvDuration("AUTH_SESSION_IDLE_TTL", 15*time.Minute),
|
||||
JWTCookieDomain: getenv("AUTH_JWT_COOKIE_DOMAIN", ""),
|
||||
JWTCookieSecure: getenvBool("AUTH_JWT_COOKIE_SECURE", false),
|
||||
JWTCookieSameSite: getenv("AUTH_JWT_COOKIE_SAMESITE", "Lax"),
|
||||
JWTAccessCookieName: getenv("AUTH_JWT_ACCESS_COOKIE", "wucher_at"),
|
||||
JWTRefreshCookieName: getenv("AUTH_JWT_REFRESH_COOKIE", "wucher_rt"),
|
||||
WebAuthnRPID: getenv("AUTH_WEBAUTHN_RP_ID", ""),
|
||||
WebAuthnRPDisplayName: getenv("AUTH_WEBAUTHN_RP_DISPLAY_NAME", "Wucher"),
|
||||
WebAuthnRPOrigins: getenvCSV("AUTH_WEBAUTHN_RP_ORIGINS", ""),
|
||||
WebAuthnChallengeTTL: getenvDuration("AUTH_WEBAUTHN_CHALLENGE_TTL", 5*time.Minute),
|
||||
SSOSuccessRedirectURL: getenv("AUTH_SSO_SUCCESS_REDIRECT", ""),
|
||||
DefaultRoleName: getenv("AUTH_DEFAULT_ROLE", "user"),
|
||||
DisableRegister: getenvBool("AUTH_DISABLE_REGISTER", false),
|
||||
}
|
||||
}
|
||||
|
||||
func LoadSESConfigFromEnv() SESConfig {
|
||||
return SESConfig{
|
||||
Region: getenv("SES_REGION", getenv("AWS_REGION", "")),
|
||||
Endpoint: getenv("SES_ENDPOINT", ""),
|
||||
From: getenv("SES_FROM", ""),
|
||||
ConfigurationSetName: getenv("SES_CONFIGURATION_SET", ""),
|
||||
SendTimeout: getenvDuration("SES_SEND_TIMEOUT", 10*time.Second),
|
||||
AllowInsecureEndpoint: getenvBool("SES_ALLOW_INSECURE_ENDPOINT", false),
|
||||
}
|
||||
}
|
||||
|
||||
func LoadEmailDeliveryConfigFromEnv() EmailDeliveryConfig {
|
||||
return EmailDeliveryConfig{
|
||||
Provider: strings.ToLower(strings.TrimSpace(getenv("EMAIL_PROVIDER", "ses"))),
|
||||
MockDir: strings.TrimSpace(getenv("EMAIL_MOCK_DIR", ".local/emails")),
|
||||
}
|
||||
}
|
||||
|
||||
func LoadMicrosoftSSOConfigFromEnv() MicrosoftSSOConfig {
|
||||
scopes := strings.Split(getenv("MS_ENTRA_SCOPES", "openid,profile,email,offline_access,User.Read"), ",")
|
||||
for i := range scopes {
|
||||
scopes[i] = strings.TrimSpace(scopes[i])
|
||||
}
|
||||
return MicrosoftSSOConfig{
|
||||
TenantID: getenv("MS_ENTRA_TENANT_ID", ""),
|
||||
ClientID: getenv("MS_ENTRA_CLIENT_ID", ""),
|
||||
ClientSecret: getenv("MS_ENTRA_CLIENT_SECRET", ""),
|
||||
RedirectURL: getenv("MS_ENTRA_REDIRECT_URL", ""),
|
||||
Authority: getenv("MS_ENTRA_AUTHORITY", "https://login.microsoftonline.com/"),
|
||||
Scopes: scopes,
|
||||
}
|
||||
}
|
||||
|
||||
func LoadHTTPConfigFromEnv() HTTPConfig {
|
||||
return HTTPConfig{
|
||||
BodyLimit: getenvInt("HTTP_BODY_LIMIT", 4*1024*1024),
|
||||
ReadTimeout: getenvDuration("HTTP_READ_TIMEOUT", 10*time.Second),
|
||||
WriteTimeout: getenvDuration("HTTP_WRITE_TIMEOUT", 15*time.Second),
|
||||
IdleTimeout: getenvDuration("HTTP_IDLE_TIMEOUT", 60*time.Second),
|
||||
RequestTimeout: getenvDuration("HTTP_REQUEST_TIMEOUT", 30*time.Second),
|
||||
RequestTimeoutByPath: parseHTTPTimeoutOverrides(getenv("HTTP_REQUEST_TIMEOUT_OVERRIDES", "")),
|
||||
ShutdownTimeout: getenvDuration("HTTP_SHUTDOWN_TIMEOUT", 30*time.Second),
|
||||
CORSAllowOrigins: getenv("HTTP_CORS_ALLOW_ORIGINS", "http://localhost:3000,http://127.0.0.1:3000,http://localhost:5173,http://127.0.0.1:5173"),
|
||||
CORSAllowMethods: getenv("HTTP_CORS_ALLOW_METHODS", "GET,POST,PUT,PATCH,DELETE,OPTIONS,HEAD"),
|
||||
CORSAllowHeaders: getenv("HTTP_CORS_ALLOW_HEADERS", "Origin,Content-Type,Accept,Authorization,X-Requested-With,X-Pin-Verification-Token,X-Device-Type"),
|
||||
CORSExposeHeaders: getenv("HTTP_CORS_EXPOSE_HEADERS", "X-Request-ID"),
|
||||
CORSAllowCredentials: getenvBool("HTTP_CORS_ALLOW_CREDENTIALS", true),
|
||||
CORSMaxAge: getenvInt("HTTP_CORS_MAX_AGE", 300),
|
||||
RateLimitMax: getenvInt("HTTP_RATE_LIMIT_MAX", 120),
|
||||
RateLimitWindow: getenvDuration("HTTP_RATE_LIMIT_WINDOW", time.Minute),
|
||||
RateLimitByEndpoint: parseHTTPEndpointRateLimitOverrides(getenv("HTTP_RATE_LIMIT_ENDPOINTS", "")),
|
||||
HSTSMaxAge: getenvInt("HTTP_SECURITY_HSTS_MAX_AGE", 0),
|
||||
}
|
||||
}
|
||||
|
||||
func parseHTTPTimeoutOverrides(raw string) map[string]time.Duration {
|
||||
parsed := make(map[string]time.Duration)
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return parsed
|
||||
}
|
||||
|
||||
entries := strings.Split(raw, ",")
|
||||
for _, entry := range entries {
|
||||
entry = strings.TrimSpace(entry)
|
||||
if entry == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(entry, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
keyParts := strings.Fields(strings.TrimSpace(parts[0]))
|
||||
if len(keyParts) < 2 {
|
||||
continue
|
||||
}
|
||||
method := strings.ToUpper(strings.TrimSpace(keyParts[0]))
|
||||
path := strings.Join(keyParts[1:], " ")
|
||||
path = strings.TrimSpace(path)
|
||||
if method == "" || path == "" {
|
||||
continue
|
||||
}
|
||||
key := method + " " + path
|
||||
|
||||
duration, err := time.ParseDuration(strings.TrimSpace(parts[1]))
|
||||
if err != nil || duration <= 0 {
|
||||
continue
|
||||
}
|
||||
parsed[key] = duration
|
||||
}
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
func parseHTTPEndpointRateLimitOverrides(raw string) map[string]RateLimitRule {
|
||||
parsed := make(map[string]RateLimitRule)
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return parsed
|
||||
}
|
||||
|
||||
entries := strings.Split(raw, ";")
|
||||
for _, entry := range entries {
|
||||
entry = strings.TrimSpace(entry)
|
||||
if entry == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(entry, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
keyParts := strings.Fields(strings.TrimSpace(parts[0]))
|
||||
if len(keyParts) < 2 {
|
||||
continue
|
||||
}
|
||||
method := strings.ToUpper(strings.TrimSpace(keyParts[0]))
|
||||
path := strings.Join(keyParts[1:], " ")
|
||||
path = strings.TrimSpace(path)
|
||||
if method == "" || path == "" {
|
||||
continue
|
||||
}
|
||||
key := method + " " + path
|
||||
|
||||
ruleParts := strings.SplitN(strings.TrimSpace(parts[1]), ",", 2)
|
||||
if len(ruleParts) != 2 {
|
||||
continue
|
||||
}
|
||||
max, err := strconv.Atoi(strings.TrimSpace(ruleParts[0]))
|
||||
if err != nil || max <= 0 {
|
||||
continue
|
||||
}
|
||||
window, err := time.ParseDuration(strings.TrimSpace(ruleParts[1]))
|
||||
if err != nil || window <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
parsed[key] = RateLimitRule{
|
||||
Max: max,
|
||||
Window: window,
|
||||
}
|
||||
}
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
func LoadAuditConfigFromEnv() AuditConfig {
|
||||
return AuditConfig{
|
||||
QueueSize: getenvInt("AUDIT_LOG_QUEUE_SIZE", 1024),
|
||||
Workers: getenvInt("AUDIT_LOG_WORKERS", 2),
|
||||
}
|
||||
}
|
||||
|
||||
func LoadLoggingConfigFromEnv() LoggingConfig {
|
||||
return LoggingConfig{
|
||||
Level: getenv("LOG_LEVEL", "info"),
|
||||
Format: getenv("LOG_FORMAT", "json"),
|
||||
}
|
||||
}
|
||||
|
||||
func LoadResilienceConfigFromEnv() ResilienceConfig {
|
||||
defaults := loadCircuitBreakerPolicyFromEnv("CB_DEFAULT_", CircuitBreakerPolicy{
|
||||
Enabled: true,
|
||||
MaxRequests: 1,
|
||||
Interval: time.Minute,
|
||||
BucketPeriod: 10 * time.Second,
|
||||
Timeout: 30 * time.Second,
|
||||
MinRequests: 5,
|
||||
FailureRatio: 0.6,
|
||||
ConsecutiveFailures: 5,
|
||||
})
|
||||
return ResilienceConfig{
|
||||
Default: defaults,
|
||||
MicrosoftSSO: loadCircuitBreakerPolicyFromEnv("CB_MICROSOFT_SSO_", defaults),
|
||||
SES: loadCircuitBreakerPolicyFromEnv("CB_SES_", defaults),
|
||||
SQS: loadCircuitBreakerPolicyFromEnv("CB_SQS_", defaults),
|
||||
}
|
||||
}
|
||||
|
||||
func LoadQueueConfigFromEnv() QueueConfig {
|
||||
return QueueConfig{
|
||||
MessageSchemaVersion: getenv("QUEUE_MESSAGE_SCHEMA_VERSION", "v2"),
|
||||
EnableLegacyPayloadFallback: getenvBool("QUEUE_ENABLE_LEGACY_PAYLOAD_FALLBACK", true),
|
||||
IdempotencyKeyPrefix: getenv("QUEUE_IDEMPOTENCY_KEY_PREFIX", "queue:idempotency:email:"),
|
||||
IdempotencyTTL: getenvDuration("QUEUE_IDEMPOTENCY_TTL", 24*time.Hour),
|
||||
ProcessingTTL: getenvDuration("QUEUE_PROCESSING_TTL", 15*time.Minute),
|
||||
WorkerConcurrency: getenvInt("QUEUE_WORKER_CONCURRENCY", 4),
|
||||
WorkerMaxBatchSize: getenvInt("QUEUE_WORKER_MAX_BATCH_SIZE", 10),
|
||||
WorkerProcessTimeout: getenvDuration("QUEUE_WORKER_PROCESS_TIMEOUT", 30*time.Second),
|
||||
WorkerShutdownTimeout: getenvDuration("QUEUE_WORKER_SHUTDOWN_TIMEOUT", 30*time.Second),
|
||||
WorkerBackoffMin: getenvDuration("QUEUE_WORKER_BACKOFF_MIN", time.Second),
|
||||
WorkerBackoffMax: getenvDuration("QUEUE_WORKER_BACKOFF_MAX", 5*time.Minute),
|
||||
WorkerPermanentFailureDelay: getenvDuration("QUEUE_WORKER_PERMANENT_FAILURE_DELAY", 30*time.Second),
|
||||
WorkerMonitorAddr: getenv("QUEUE_WORKER_MONITOR_ADDR", "127.0.0.1:9090"),
|
||||
WorkerHealthErrorThreshold: getenvInt("QUEUE_WORKER_HEALTH_ERROR_THRESHOLD", 3),
|
||||
WorkerDepthPollInterval: getenvDuration("QUEUE_WORKER_DEPTH_POLL_INTERVAL", 30*time.Second),
|
||||
OutboxEnabled: getenvBool("QUEUE_OUTBOX_ENABLED", true),
|
||||
OutboxPollInterval: getenvDuration("QUEUE_OUTBOX_POLL_INTERVAL", time.Second),
|
||||
OutboxBatchSize: getenvInt("QUEUE_OUTBOX_BATCH_SIZE", 10),
|
||||
OutboxDispatchTimeout: getenvDuration("QUEUE_OUTBOX_DISPATCH_TIMEOUT", 10*time.Second),
|
||||
OutboxLockTTL: getenvDuration("QUEUE_OUTBOX_LOCK_TTL", time.Minute),
|
||||
OutboxMaxAttempts: getenvInt("QUEUE_OUTBOX_MAX_ATTEMPTS", 20),
|
||||
}
|
||||
}
|
||||
|
||||
func LoadFileQueueConfigFromEnv() FileQueueConfig {
|
||||
return FileQueueConfig{
|
||||
MessageSchemaVersion: getenv("FILE_QUEUE_MESSAGE_SCHEMA_VERSION", "v1"),
|
||||
IdempotencyKeyPrefix: getenv("FILE_QUEUE_IDEMPOTENCY_KEY_PREFIX", "queue:idempotency:file:"),
|
||||
IdempotencyTTL: getenvDuration("FILE_QUEUE_IDEMPOTENCY_TTL", 24*time.Hour),
|
||||
ProcessingTTL: getenvDuration("FILE_QUEUE_PROCESSING_TTL", 15*time.Minute),
|
||||
WorkerConcurrency: getenvInt("FILE_QUEUE_WORKER_CONCURRENCY", 4),
|
||||
WorkerMaxBatchSize: getenvInt("FILE_QUEUE_WORKER_MAX_BATCH_SIZE", 10),
|
||||
WorkerProcessTimeout: getenvDuration("FILE_QUEUE_WORKER_PROCESS_TIMEOUT", 30*time.Second),
|
||||
WorkerShutdownTimeout: getenvDuration("FILE_QUEUE_WORKER_SHUTDOWN_TIMEOUT", 30*time.Second),
|
||||
WorkerBackoffMin: getenvDuration("FILE_QUEUE_WORKER_BACKOFF_MIN", time.Second),
|
||||
WorkerBackoffMax: getenvDuration("FILE_QUEUE_WORKER_BACKOFF_MAX", 5*time.Minute),
|
||||
WorkerPermanentFailureDelay: getenvDuration("FILE_QUEUE_WORKER_PERMANENT_FAILURE_DELAY", 30*time.Second),
|
||||
WorkerMonitorAddr: getenv("FILE_QUEUE_WORKER_MONITOR_ADDR", "127.0.0.1:9091"),
|
||||
WorkerHealthErrorThreshold: getenvInt("FILE_QUEUE_WORKER_HEALTH_ERROR_THRESHOLD", 3),
|
||||
WorkerDepthPollInterval: getenvDuration("FILE_QUEUE_WORKER_DEPTH_POLL_INTERVAL", 30*time.Second),
|
||||
OutboxEnabled: getenvBool("FILE_QUEUE_OUTBOX_ENABLED", true),
|
||||
OutboxPollInterval: getenvDuration("FILE_QUEUE_OUTBOX_POLL_INTERVAL", time.Second),
|
||||
OutboxBatchSize: getenvInt("FILE_QUEUE_OUTBOX_BATCH_SIZE", 10),
|
||||
OutboxDispatchTimeout: getenvDuration("FILE_QUEUE_OUTBOX_DISPATCH_TIMEOUT", 10*time.Second),
|
||||
OutboxLockTTL: getenvDuration("FILE_QUEUE_OUTBOX_LOCK_TTL", time.Minute),
|
||||
OutboxMaxAttempts: getenvInt("FILE_QUEUE_OUTBOX_MAX_ATTEMPTS", 20),
|
||||
}
|
||||
}
|
||||
|
||||
func LoadSQSConfigFromEnv() SQSConfig {
|
||||
return SQSConfig{
|
||||
Region: getenv("AWS_REGION", ""),
|
||||
AccessKeyID: getenv("AWS_ACCESS_KEY_ID", ""),
|
||||
SecretAccessKey: getenv("AWS_SECRET_ACCESS_KEY", ""),
|
||||
Endpoint: getenv("SQS_ENDPOINT", ""),
|
||||
QueueURL: getenv("SQS_QUEUE_URL", ""),
|
||||
QueueType: getenv("SQS_QUEUE_TYPE", "standard"),
|
||||
MessageGroupID: getenv("SQS_MESSAGE_GROUP_ID", "email"),
|
||||
MaxMessages: getenvInt("SQS_MAX_MESSAGES", 10),
|
||||
WaitTimeSeconds: int32(getenvInt("SQS_WAIT_TIME_SECONDS", 20)),
|
||||
VisibilityTimeout: getenvDuration("SQS_VISIBILITY_TIMEOUT", 60*time.Second),
|
||||
AllowInsecureEndpoint: getenvBool("SQS_ALLOW_INSECURE_ENDPOINT", false),
|
||||
}
|
||||
}
|
||||
|
||||
func LoadFileSQSConfigFromEnv() SQSConfig {
|
||||
return SQSConfig{
|
||||
Region: getenv("FILE_SQS_REGION", getenv("AWS_REGION", "")),
|
||||
AccessKeyID: getenv("AWS_ACCESS_KEY_ID", ""),
|
||||
SecretAccessKey: getenv("AWS_SECRET_ACCESS_KEY", ""),
|
||||
Endpoint: getenv("FILE_SQS_ENDPOINT", getenv("SQS_ENDPOINT", "")),
|
||||
QueueURL: getenv("FILE_SQS_QUEUE_URL", ""),
|
||||
QueueType: getenv("FILE_SQS_QUEUE_TYPE", "standard"),
|
||||
MessageGroupID: getenv("FILE_SQS_MESSAGE_GROUP_ID", "file-processing"),
|
||||
MaxMessages: getenvInt("FILE_SQS_MAX_MESSAGES", 10),
|
||||
WaitTimeSeconds: int32(getenvInt("FILE_SQS_WAIT_TIME_SECONDS", 20)),
|
||||
VisibilityTimeout: getenvDuration("FILE_SQS_VISIBILITY_TIMEOUT", 60*time.Second),
|
||||
AllowInsecureEndpoint: getenvBool("FILE_SQS_ALLOW_INSECURE_ENDPOINT", getenvBool("SQS_ALLOW_INSECURE_ENDPOINT", false)),
|
||||
}
|
||||
}
|
||||
|
||||
func ValidateDistinctQueueURLs(emailCfg, fileCfg SQSConfig) error {
|
||||
emailQueueURL := canonicalQueueURL(emailCfg.QueueURL)
|
||||
fileQueueURL := canonicalQueueURL(fileCfg.QueueURL)
|
||||
|
||||
if emailQueueURL == "" || fileQueueURL == "" {
|
||||
return nil
|
||||
}
|
||||
if emailQueueURL == fileQueueURL {
|
||||
return fmt.Errorf("invalid queue configuration: FILE_SQS_QUEUE_URL must be different from SQS_QUEUE_URL")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func canonicalQueueURL(raw string) string {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if trimmed == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(trimmed)
|
||||
if err != nil {
|
||||
return strings.TrimRight(strings.ToLower(trimmed), "/")
|
||||
}
|
||||
parsed.Host = strings.ToLower(parsed.Host)
|
||||
parsed.Path = strings.TrimRight(parsed.Path, "/")
|
||||
parsed.RawQuery = ""
|
||||
parsed.Fragment = ""
|
||||
return parsed.String()
|
||||
}
|
||||
|
||||
func LoadFileStorageConfigFromEnv() FileStorageConfig {
|
||||
provider := strings.ToLower(strings.TrimSpace(getenv("FILE_STORAGE_PROVIDER", "s3")))
|
||||
return FileStorageConfig{
|
||||
Provider: provider,
|
||||
Bucket: strings.TrimSpace(getenv("FILE_S3_BUCKET", "")),
|
||||
Region: strings.TrimSpace(getenv("FILE_S3_REGION", getenv("AWS_REGION", ""))),
|
||||
Endpoint: strings.TrimSpace(getenv("FILE_S3_ENDPOINT", "")),
|
||||
AccessKeyID: strings.TrimSpace(getenv("FILE_S3_ACCESS_KEY_ID", getenv("AWS_ACCESS_KEY_ID", ""))),
|
||||
SecretAccessKey: strings.TrimSpace(getenv("FILE_S3_SECRET_ACCESS_KEY", getenv("AWS_SECRET_ACCESS_KEY", ""))),
|
||||
ForcePathStyle: getenvBool("FILE_S3_FORCE_PATH_STYLE", false),
|
||||
AllowInsecureEndpoint: getenvBool("FILE_S3_ALLOW_INSECURE_ENDPOINT", false),
|
||||
AutoCreateBucket: getenvBool("FILE_S3_AUTO_CREATE_BUCKET", false),
|
||||
ObjectKeyPrefix: strings.TrimSpace(getenv("FILE_OBJECT_KEY_PREFIX", "fm/objects")),
|
||||
PresignPutTTL: getenvDuration("FILE_S3_PRESIGN_PUT_TTL", 15*time.Minute),
|
||||
PresignGetTTL: getenvDuration("FILE_S3_PRESIGN_GET_TTL", 15*time.Minute),
|
||||
}
|
||||
}
|
||||
|
||||
func LoadFileManagerUploadPolicyConfigFromEnv() FileManagerUploadPolicyConfig {
|
||||
return FileManagerUploadPolicyConfig{
|
||||
MaxFileSizeBytes: int64(getenvInt("FILE_MANAGER_UPLOAD_MAX_FILE_SIZE_BYTES", 4*1024*1024)),
|
||||
AllowedMimeTypes: normalizeMimeTypes(getenvCSV("FILE_MANAGER_UPLOAD_ALLOWED_MIME_TYPES", "")),
|
||||
AllowedExtensions: normalizeExtensions(getenvCSV("FILE_MANAGER_UPLOAD_ALLOWED_EXTENSIONS", "")),
|
||||
AllowEmptyFile: getenvBool("FILE_MANAGER_UPLOAD_ALLOW_EMPTY_FILE", false),
|
||||
MaxFilenameLength: getenvInt("FILE_MANAGER_UPLOAD_MAX_FILENAME_LENGTH", 255),
|
||||
}
|
||||
}
|
||||
|
||||
func LoadFileManagerTrashPolicyConfigFromEnv() FileManagerTrashPolicyConfig {
|
||||
retention := getenvDuration("FILE_MANAGER_TRASH_RETENTION", 30*24*time.Hour)
|
||||
if retention < 0 {
|
||||
retention = 0
|
||||
}
|
||||
interval := getenvDuration("FILE_MANAGER_TRASH_AUTO_PURGE_INTERVAL", time.Hour)
|
||||
if interval <= 0 {
|
||||
interval = time.Hour
|
||||
}
|
||||
batchSize := getenvInt("FILE_MANAGER_TRASH_AUTO_PURGE_BATCH_SIZE", 200)
|
||||
if batchSize <= 0 {
|
||||
batchSize = 200
|
||||
}
|
||||
|
||||
return FileManagerTrashPolicyConfig{
|
||||
AutoPurgeEnabled: getenvBool("FILE_MANAGER_TRASH_AUTO_PURGE_ENABLED", true),
|
||||
Retention: retention,
|
||||
AutoPurgeInterval: interval,
|
||||
AutoPurgeBatchSize: batchSize,
|
||||
}
|
||||
}
|
||||
|
||||
func LoadFileManagerLifecycleConfigFromEnv() FileManagerLifecycleConfig {
|
||||
tempHours := getenvInt("TEMP_UPLOAD_GRACE_HOURS", 2)
|
||||
if tempHours <= 0 {
|
||||
tempHours = 2
|
||||
}
|
||||
orphanDays := getenvInt("ORPHAN_GRACE_PERIOD_DAYS", 30)
|
||||
if orphanDays <= 0 {
|
||||
orphanDays = 30
|
||||
}
|
||||
batchSize := getenvInt("FILE_CLEANUP_BATCH_SIZE", 100)
|
||||
if batchSize <= 0 {
|
||||
batchSize = 100
|
||||
}
|
||||
return FileManagerLifecycleConfig{
|
||||
EnableS3Tagging: getenvBool("ENABLE_S3_TAGGING", true),
|
||||
EnableCleanupJob: getenvBool("ENABLE_FILE_CLEANUP_JOB", false),
|
||||
TempUploadGraceHours: tempHours,
|
||||
OrphanGraceDays: orphanDays,
|
||||
CleanupBatchSize: batchSize,
|
||||
CleanupDryRun: getenvBool("FILE_CLEANUP_DRY_RUN", true),
|
||||
}
|
||||
}
|
||||
|
||||
func getenv(key, def string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func getenvInt(key string, def int) int {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
if n, err := strconv.Atoi(v); err == nil {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func getenvUint32(key string, def uint32) uint32 {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
if n, err := strconv.ParseUint(v, 10, 32); err == nil {
|
||||
return uint32(n)
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func getenvFloat64(key string, def float64) float64 {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
if n, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func getenvDuration(key string, def time.Duration) time.Duration {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
if d, err := time.ParseDuration(v); err == nil {
|
||||
return d
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func getenvBool(key string, def bool) bool {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
if b, err := strconv.ParseBool(v); err == nil {
|
||||
return b
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func getenvCSV(key, def string) []string {
|
||||
raw := os.Getenv(key)
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
raw = def
|
||||
}
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
parts := strings.Split(raw, ",")
|
||||
out := make([]string, 0, len(parts))
|
||||
for i := range parts {
|
||||
part := strings.TrimSpace(parts[i])
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, part)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeMimeTypes(values []string) []string {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]string, 0, len(values))
|
||||
seen := make(map[string]struct{}, len(values))
|
||||
for i := range values {
|
||||
v := strings.ToLower(strings.TrimSpace(values[i]))
|
||||
if idx := strings.Index(v, ";"); idx >= 0 {
|
||||
v = strings.TrimSpace(v[:idx])
|
||||
}
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[v]; ok {
|
||||
continue
|
||||
}
|
||||
seen[v] = struct{}{}
|
||||
out = append(out, v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeExtensions(values []string) []string {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]string, 0, len(values))
|
||||
seen := make(map[string]struct{}, len(values))
|
||||
for i := range values {
|
||||
v := strings.ToLower(strings.TrimSpace(values[i]))
|
||||
v = strings.TrimPrefix(v, ".")
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[v]; ok {
|
||||
continue
|
||||
}
|
||||
seen[v] = struct{}{}
|
||||
out = append(out, v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func loadCircuitBreakerPolicyFromEnv(prefix string, defaults CircuitBreakerPolicy) CircuitBreakerPolicy {
|
||||
return CircuitBreakerPolicy{
|
||||
Enabled: getenvBool(prefix+"ENABLED", defaults.Enabled),
|
||||
MaxRequests: getenvUint32(prefix+"MAX_REQUESTS", defaults.MaxRequests),
|
||||
Interval: getenvDuration(prefix+"INTERVAL", defaults.Interval),
|
||||
BucketPeriod: getenvDuration(prefix+"BUCKET_PERIOD", defaults.BucketPeriod),
|
||||
Timeout: getenvDuration(prefix+"TIMEOUT", defaults.Timeout),
|
||||
MinRequests: getenvUint32(prefix+"MIN_REQUESTS", defaults.MinRequests),
|
||||
FailureRatio: getenvFloat64(prefix+"FAILURE_RATIO", defaults.FailureRatio),
|
||||
ConsecutiveFailures: getenvUint32(prefix+"CONSECUTIVE_FAILURES", defaults.ConsecutiveFailures),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user