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),
|
||||
}
|
||||
}
|
||||
340
internal/config/config_test.go
Normal file
340
internal/config/config_test.go
Normal file
@@ -0,0 +1,340 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLoadFileManagerUploadPolicyConfigFromEnv_Defaults(t *testing.T) {
|
||||
t.Setenv("FILE_MANAGER_UPLOAD_MAX_FILE_SIZE_BYTES", "")
|
||||
t.Setenv("FILE_MANAGER_UPLOAD_ALLOWED_MIME_TYPES", "")
|
||||
t.Setenv("FILE_MANAGER_UPLOAD_ALLOWED_EXTENSIONS", "")
|
||||
t.Setenv("FILE_MANAGER_UPLOAD_ALLOW_EMPTY_FILE", "")
|
||||
t.Setenv("FILE_MANAGER_UPLOAD_MAX_FILENAME_LENGTH", "")
|
||||
|
||||
cfg := LoadFileManagerUploadPolicyConfigFromEnv()
|
||||
|
||||
if cfg.MaxFileSizeBytes != 4*1024*1024 {
|
||||
t.Fatalf("expected default max file size 4194304, got %d", cfg.MaxFileSizeBytes)
|
||||
}
|
||||
if cfg.AllowEmptyFile {
|
||||
t.Fatalf("expected default allow empty file false")
|
||||
}
|
||||
if cfg.MaxFilenameLength != 255 {
|
||||
t.Fatalf("expected default max filename length 255, got %d", cfg.MaxFilenameLength)
|
||||
}
|
||||
if len(cfg.AllowedMimeTypes) != 0 {
|
||||
t.Fatalf("expected empty default allowed mime types, got %v", cfg.AllowedMimeTypes)
|
||||
}
|
||||
if len(cfg.AllowedExtensions) != 0 {
|
||||
t.Fatalf("expected empty default allowed extensions, got %v", cfg.AllowedExtensions)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFileManagerUploadPolicyConfigFromEnv_Normalize(t *testing.T) {
|
||||
t.Setenv("FILE_MANAGER_UPLOAD_MAX_FILE_SIZE_BYTES", "1048576")
|
||||
t.Setenv("FILE_MANAGER_UPLOAD_ALLOWED_MIME_TYPES", " image/PNG ; charset=utf-8,application/pdf,IMAGE/PNG")
|
||||
t.Setenv("FILE_MANAGER_UPLOAD_ALLOWED_EXTENSIONS", " .PDF, png ,PNG")
|
||||
t.Setenv("FILE_MANAGER_UPLOAD_ALLOW_EMPTY_FILE", "true")
|
||||
t.Setenv("FILE_MANAGER_UPLOAD_MAX_FILENAME_LENGTH", "120")
|
||||
|
||||
cfg := LoadFileManagerUploadPolicyConfigFromEnv()
|
||||
|
||||
if cfg.MaxFileSizeBytes != 1048576 {
|
||||
t.Fatalf("expected max file size 1048576, got %d", cfg.MaxFileSizeBytes)
|
||||
}
|
||||
if !cfg.AllowEmptyFile {
|
||||
t.Fatalf("expected allow empty file true")
|
||||
}
|
||||
if cfg.MaxFilenameLength != 120 {
|
||||
t.Fatalf("expected max filename length 120, got %d", cfg.MaxFilenameLength)
|
||||
}
|
||||
|
||||
if len(cfg.AllowedMimeTypes) != 2 || cfg.AllowedMimeTypes[0] != "image/png" || cfg.AllowedMimeTypes[1] != "application/pdf" {
|
||||
t.Fatalf("unexpected normalized mime types: %v", cfg.AllowedMimeTypes)
|
||||
}
|
||||
if len(cfg.AllowedExtensions) != 2 || cfg.AllowedExtensions[0] != "pdf" || cfg.AllowedExtensions[1] != "png" {
|
||||
t.Fatalf("unexpected normalized extensions: %v", cfg.AllowedExtensions)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFileManagerTrashPolicyConfigFromEnv_Defaults(t *testing.T) {
|
||||
t.Setenv("FILE_MANAGER_TRASH_AUTO_PURGE_ENABLED", "")
|
||||
t.Setenv("FILE_MANAGER_TRASH_RETENTION", "")
|
||||
t.Setenv("FILE_MANAGER_TRASH_AUTO_PURGE_INTERVAL", "")
|
||||
t.Setenv("FILE_MANAGER_TRASH_AUTO_PURGE_BATCH_SIZE", "")
|
||||
|
||||
cfg := LoadFileManagerTrashPolicyConfigFromEnv()
|
||||
if !cfg.AutoPurgeEnabled {
|
||||
t.Fatalf("expected default auto purge enabled")
|
||||
}
|
||||
if cfg.Retention != 30*24*time.Hour {
|
||||
t.Fatalf("expected default retention 720h, got %s", cfg.Retention)
|
||||
}
|
||||
if cfg.AutoPurgeInterval != time.Hour {
|
||||
t.Fatalf("expected default auto purge interval 1h, got %s", cfg.AutoPurgeInterval)
|
||||
}
|
||||
if cfg.AutoPurgeBatchSize != 200 {
|
||||
t.Fatalf("expected default batch size 200, got %d", cfg.AutoPurgeBatchSize)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFileManagerTrashPolicyConfigFromEnv_Custom(t *testing.T) {
|
||||
t.Setenv("FILE_MANAGER_TRASH_AUTO_PURGE_ENABLED", "false")
|
||||
t.Setenv("FILE_MANAGER_TRASH_RETENTION", "168h")
|
||||
t.Setenv("FILE_MANAGER_TRASH_AUTO_PURGE_INTERVAL", "15m")
|
||||
t.Setenv("FILE_MANAGER_TRASH_AUTO_PURGE_BATCH_SIZE", "50")
|
||||
|
||||
cfg := LoadFileManagerTrashPolicyConfigFromEnv()
|
||||
if cfg.AutoPurgeEnabled {
|
||||
t.Fatalf("expected auto purge disabled")
|
||||
}
|
||||
if cfg.Retention != 168*time.Hour {
|
||||
t.Fatalf("expected retention 168h, got %s", cfg.Retention)
|
||||
}
|
||||
if cfg.AutoPurgeInterval != 15*time.Minute {
|
||||
t.Fatalf("expected auto purge interval 15m, got %s", cfg.AutoPurgeInterval)
|
||||
}
|
||||
if cfg.AutoPurgeBatchSize != 50 {
|
||||
t.Fatalf("expected batch size 50, got %d", cfg.AutoPurgeBatchSize)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFileManagerLifecycleConfigFromEnv_Defaults(t *testing.T) {
|
||||
t.Setenv("ENABLE_S3_TAGGING", "")
|
||||
t.Setenv("ENABLE_FILE_CLEANUP_JOB", "")
|
||||
t.Setenv("TEMP_UPLOAD_GRACE_HOURS", "")
|
||||
t.Setenv("ORPHAN_GRACE_PERIOD_DAYS", "")
|
||||
t.Setenv("FILE_CLEANUP_BATCH_SIZE", "")
|
||||
t.Setenv("FILE_CLEANUP_DRY_RUN", "")
|
||||
|
||||
cfg := LoadFileManagerLifecycleConfigFromEnv()
|
||||
if !cfg.EnableS3Tagging {
|
||||
t.Fatalf("expected s3 tagging enabled by default")
|
||||
}
|
||||
if cfg.EnableCleanupJob {
|
||||
t.Fatalf("expected cleanup job disabled by default")
|
||||
}
|
||||
if cfg.TempUploadGraceHours != 2 {
|
||||
t.Fatalf("expected temp upload grace 2 hours, got %d", cfg.TempUploadGraceHours)
|
||||
}
|
||||
if cfg.OrphanGraceDays != 30 {
|
||||
t.Fatalf("expected orphan grace 30 days, got %d", cfg.OrphanGraceDays)
|
||||
}
|
||||
if cfg.CleanupBatchSize != 100 {
|
||||
t.Fatalf("expected cleanup batch size 100, got %d", cfg.CleanupBatchSize)
|
||||
}
|
||||
if !cfg.CleanupDryRun {
|
||||
t.Fatalf("expected cleanup dry run enabled by default")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFileManagerLifecycleConfigFromEnv_Custom(t *testing.T) {
|
||||
t.Setenv("ENABLE_S3_TAGGING", "false")
|
||||
t.Setenv("ENABLE_FILE_CLEANUP_JOB", "true")
|
||||
t.Setenv("TEMP_UPLOAD_GRACE_HOURS", "6")
|
||||
t.Setenv("ORPHAN_GRACE_PERIOD_DAYS", "45")
|
||||
t.Setenv("FILE_CLEANUP_BATCH_SIZE", "250")
|
||||
t.Setenv("FILE_CLEANUP_DRY_RUN", "false")
|
||||
|
||||
cfg := LoadFileManagerLifecycleConfigFromEnv()
|
||||
if cfg.EnableS3Tagging {
|
||||
t.Fatalf("expected s3 tagging disabled")
|
||||
}
|
||||
if !cfg.EnableCleanupJob {
|
||||
t.Fatalf("expected cleanup job enabled")
|
||||
}
|
||||
if cfg.TempUploadGraceHours != 6 {
|
||||
t.Fatalf("expected temp upload grace 6 hours, got %d", cfg.TempUploadGraceHours)
|
||||
}
|
||||
if cfg.OrphanGraceDays != 45 {
|
||||
t.Fatalf("expected orphan grace 45 days, got %d", cfg.OrphanGraceDays)
|
||||
}
|
||||
if cfg.CleanupBatchSize != 250 {
|
||||
t.Fatalf("expected cleanup batch size 250, got %d", cfg.CleanupBatchSize)
|
||||
}
|
||||
if cfg.CleanupDryRun {
|
||||
t.Fatalf("expected cleanup dry run disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFileQueueConfigFromEnv_Defaults(t *testing.T) {
|
||||
t.Setenv("FILE_QUEUE_MESSAGE_SCHEMA_VERSION", "")
|
||||
t.Setenv("FILE_QUEUE_IDEMPOTENCY_KEY_PREFIX", "")
|
||||
t.Setenv("FILE_QUEUE_WORKER_MONITOR_ADDR", "")
|
||||
t.Setenv("FILE_QUEUE_OUTBOX_ENABLED", "")
|
||||
t.Setenv("FILE_QUEUE_OUTBOX_POLL_INTERVAL", "")
|
||||
t.Setenv("FILE_QUEUE_OUTBOX_BATCH_SIZE", "")
|
||||
t.Setenv("FILE_QUEUE_OUTBOX_DISPATCH_TIMEOUT", "")
|
||||
t.Setenv("FILE_QUEUE_OUTBOX_LOCK_TTL", "")
|
||||
t.Setenv("FILE_QUEUE_OUTBOX_MAX_ATTEMPTS", "")
|
||||
|
||||
cfg := LoadFileQueueConfigFromEnv()
|
||||
if cfg.MessageSchemaVersion != "v1" {
|
||||
t.Fatalf("expected default schema version v1, got %q", cfg.MessageSchemaVersion)
|
||||
}
|
||||
if cfg.IdempotencyKeyPrefix != "queue:idempotency:file:" {
|
||||
t.Fatalf("unexpected default idempotency prefix: %q", cfg.IdempotencyKeyPrefix)
|
||||
}
|
||||
if cfg.WorkerMonitorAddr != "127.0.0.1:9091" {
|
||||
t.Fatalf("unexpected default monitor addr: %q", cfg.WorkerMonitorAddr)
|
||||
}
|
||||
if !cfg.OutboxEnabled {
|
||||
t.Fatalf("expected default file outbox enabled")
|
||||
}
|
||||
if cfg.OutboxPollInterval != time.Second {
|
||||
t.Fatalf("expected default outbox poll interval 1s, got %s", cfg.OutboxPollInterval)
|
||||
}
|
||||
if cfg.OutboxBatchSize != 10 {
|
||||
t.Fatalf("expected default outbox batch size 10, got %d", cfg.OutboxBatchSize)
|
||||
}
|
||||
if cfg.OutboxDispatchTimeout != 10*time.Second {
|
||||
t.Fatalf("expected default outbox dispatch timeout 10s, got %s", cfg.OutboxDispatchTimeout)
|
||||
}
|
||||
if cfg.OutboxLockTTL != time.Minute {
|
||||
t.Fatalf("expected default outbox lock ttl 1m, got %s", cfg.OutboxLockTTL)
|
||||
}
|
||||
if cfg.OutboxMaxAttempts != 20 {
|
||||
t.Fatalf("expected default outbox max attempts 20, got %d", cfg.OutboxMaxAttempts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFileSQSConfigFromEnv(t *testing.T) {
|
||||
t.Setenv("AWS_REGION", "ap-southeast-1")
|
||||
t.Setenv("SQS_ENDPOINT", "http://localhost:4566")
|
||||
t.Setenv("SQS_ALLOW_INSECURE_ENDPOINT", "true")
|
||||
t.Setenv("FILE_SQS_REGION", "")
|
||||
t.Setenv("FILE_SQS_ENDPOINT", "")
|
||||
t.Setenv("FILE_SQS_QUEUE_URL", "http://localhost:4566/000000000000/app-file-queue")
|
||||
t.Setenv("FILE_SQS_QUEUE_TYPE", "standard")
|
||||
t.Setenv("FILE_SQS_MESSAGE_GROUP_ID", "file")
|
||||
t.Setenv("FILE_SQS_MAX_MESSAGES", "8")
|
||||
t.Setenv("FILE_SQS_WAIT_TIME_SECONDS", "15")
|
||||
t.Setenv("FILE_SQS_VISIBILITY_TIMEOUT", "90s")
|
||||
t.Setenv("FILE_SQS_ALLOW_INSECURE_ENDPOINT", "")
|
||||
|
||||
cfg := LoadFileSQSConfigFromEnv()
|
||||
if cfg.Region != "ap-southeast-1" {
|
||||
t.Fatalf("unexpected region: %q", cfg.Region)
|
||||
}
|
||||
if cfg.Endpoint != "http://localhost:4566" {
|
||||
t.Fatalf("unexpected endpoint: %q", cfg.Endpoint)
|
||||
}
|
||||
if cfg.QueueURL == "" {
|
||||
t.Fatalf("expected queue url")
|
||||
}
|
||||
if cfg.MaxMessages != 8 {
|
||||
t.Fatalf("unexpected max messages: %d", cfg.MaxMessages)
|
||||
}
|
||||
if cfg.WaitTimeSeconds != 15 {
|
||||
t.Fatalf("unexpected wait time seconds: %d", cfg.WaitTimeSeconds)
|
||||
}
|
||||
if !cfg.AllowInsecureEndpoint {
|
||||
t.Fatalf("expected insecure endpoint allowed from fallback")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDistinctQueueURLs(t *testing.T) {
|
||||
t.Run("different queue urls", func(t *testing.T) {
|
||||
err := ValidateDistinctQueueURLs(
|
||||
SQSConfig{QueueURL: "http://localhost:4566/000000000000/app-queue"},
|
||||
SQSConfig{QueueURL: "http://localhost:4566/000000000000/app-file-queue"},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("same queue urls with different case and slash", func(t *testing.T) {
|
||||
err := ValidateDistinctQueueURLs(
|
||||
SQSConfig{QueueURL: "HTTP://LOCALHOST:4566/000000000000/app-queue"},
|
||||
SQSConfig{QueueURL: "http://localhost:4566/000000000000/app-queue/"},
|
||||
)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for same queue urls")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("skip when one queue url empty", func(t *testing.T) {
|
||||
err := ValidateDistinctQueueURLs(
|
||||
SQSConfig{QueueURL: "http://localhost:4566/000000000000/app-queue"},
|
||||
SQSConfig{QueueURL: ""},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error when file queue url is empty, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseHTTPTimeoutOverrides(t *testing.T) {
|
||||
t.Run("invalid format ignored", func(t *testing.T) {
|
||||
parsed := parseHTTPTimeoutOverrides("GET /users/:id 100ms,foo")
|
||||
if len(parsed) != 0 {
|
||||
t.Fatalf("expected no parsed entries, got %v", parsed)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid duration ignored", func(t *testing.T) {
|
||||
parsed := parseHTTPTimeoutOverrides("GET /users/:id=not-a-duration")
|
||||
if len(parsed) != 0 {
|
||||
t.Fatalf("expected no parsed entries, got %v", parsed)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("zero and negative duration ignored", func(t *testing.T) {
|
||||
parsed := parseHTTPTimeoutOverrides("GET /users/:id=0s,POST /users=-1s")
|
||||
if len(parsed) != 0 {
|
||||
t.Fatalf("expected no parsed entries, got %v", parsed)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid entries parsed", func(t *testing.T) {
|
||||
parsed := parseHTTPTimeoutOverrides("GET /users/:id=120ms, POST /auth/login = 2s")
|
||||
if len(parsed) != 2 {
|
||||
t.Fatalf("expected 2 parsed entries, got %d", len(parsed))
|
||||
}
|
||||
if parsed["GET /users/:id"] != 120*time.Millisecond {
|
||||
t.Fatalf("unexpected GET /users/:id timeout: %s", parsed["GET /users/:id"])
|
||||
}
|
||||
if parsed["POST /auth/login"] != 2*time.Second {
|
||||
t.Fatalf("unexpected POST /auth/login timeout: %s", parsed["POST /auth/login"])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseHTTPEndpointRateLimitOverrides(t *testing.T) {
|
||||
t.Run("invalid entries ignored", func(t *testing.T) {
|
||||
parsed := parseHTTPEndpointRateLimitOverrides("POST /auth/login=abc,1m;foo;GET /x=1")
|
||||
if len(parsed) != 0 {
|
||||
t.Fatalf("expected no parsed entries, got %v", parsed)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("zero or negative values ignored", func(t *testing.T) {
|
||||
parsed := parseHTTPEndpointRateLimitOverrides("POST /auth/login=0,1m;POST /auth/totp/verify=5,0s")
|
||||
if len(parsed) != 0 {
|
||||
t.Fatalf("expected no parsed entries, got %v", parsed)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid entries parsed", func(t *testing.T) {
|
||||
parsed := parseHTTPEndpointRateLimitOverrides("POST /auth/login=5,1m; POST /auth/totp/verify = 3,30s")
|
||||
if len(parsed) != 2 {
|
||||
t.Fatalf("expected 2 parsed entries, got %d", len(parsed))
|
||||
}
|
||||
login, ok := parsed["POST /auth/login"]
|
||||
if !ok {
|
||||
t.Fatalf("expected POST /auth/login to be parsed")
|
||||
}
|
||||
if login.Max != 5 || login.Window != time.Minute {
|
||||
t.Fatalf("unexpected login rule: %+v", login)
|
||||
}
|
||||
totp, ok := parsed["POST /auth/totp/verify"]
|
||||
if !ok {
|
||||
t.Fatalf("expected POST /auth/totp/verify to be parsed")
|
||||
}
|
||||
if totp.Max != 3 || totp.Window != 30*time.Second {
|
||||
t.Fatalf("unexpected totp rule: %+v", totp)
|
||||
}
|
||||
})
|
||||
}
|
||||
638
internal/config/loader.go
Normal file
638
internal/config/loader.go
Normal file
@@ -0,0 +1,638 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Duration unmarshals a Go duration string (e.g. "30s", "5m", "150ms") from JSON.
|
||||
// An empty string is treated as zero duration.
|
||||
type Duration struct{ time.Duration }
|
||||
|
||||
func (d *Duration) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(b, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
if s == "" {
|
||||
d.Duration = 0
|
||||
return nil
|
||||
}
|
||||
dur, err := time.ParseDuration(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid duration %q: %w", s, err)
|
||||
}
|
||||
d.Duration = dur
|
||||
return nil
|
||||
}
|
||||
|
||||
// AppConfig is the unified config loaded from a JSON file.
|
||||
type AppConfig struct {
|
||||
HTTPPort int
|
||||
MySQL MySQLConfig
|
||||
Logging LoggingConfig
|
||||
Audit AuditConfig
|
||||
Auth AuthConfig
|
||||
SES SESConfig
|
||||
Email EmailDeliveryConfig
|
||||
MicrosoftSSO MicrosoftSSOConfig
|
||||
HTTP HTTPConfig
|
||||
Resilience ResilienceConfig
|
||||
Queue QueueConfig
|
||||
FileQueue FileQueueConfig
|
||||
SQS SQSConfig
|
||||
FileSQS SQSConfig
|
||||
FileStorage FileStorageConfig
|
||||
FileUpload FileManagerUploadPolicyConfig
|
||||
SAML SAMLConfig
|
||||
WOPI WOPIConfig
|
||||
Gotenberg GotenbergConfig
|
||||
}
|
||||
|
||||
// LoadFromFile reads path, parses it as JSON, and returns a populated AppConfig.
|
||||
// The file is typically produced by scripts/inject-secrets.sh.
|
||||
func LoadFromFile(path string) (AppConfig, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return AppConfig{}, fmt.Errorf("config: read %q: %w", path, err)
|
||||
}
|
||||
var raw jsonAppConfig
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return AppConfig{}, fmt.Errorf("config: parse %q: %w", path, err)
|
||||
}
|
||||
return raw.toAppConfig(), nil
|
||||
}
|
||||
|
||||
// ── JSON intermediate types ───────────────────────────────────────────────────
|
||||
|
||||
type jsonCircuitBreakerPolicy struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
MaxRequests uint32 `json:"maxRequests"`
|
||||
Interval Duration `json:"interval"`
|
||||
BucketPeriod Duration `json:"bucketPeriod"`
|
||||
Timeout Duration `json:"timeout"`
|
||||
MinRequests uint32 `json:"minRequests"`
|
||||
FailureRatio float64 `json:"failureRatio"`
|
||||
ConsecutiveFailures uint32 `json:"consecutiveFailures"`
|
||||
}
|
||||
|
||||
func (j jsonCircuitBreakerPolicy) toPolicy() CircuitBreakerPolicy {
|
||||
return CircuitBreakerPolicy{
|
||||
Enabled: j.Enabled,
|
||||
MaxRequests: j.MaxRequests,
|
||||
Interval: j.Interval.Duration,
|
||||
BucketPeriod: j.BucketPeriod.Duration,
|
||||
Timeout: j.Timeout.Duration,
|
||||
MinRequests: j.MinRequests,
|
||||
FailureRatio: j.FailureRatio,
|
||||
ConsecutiveFailures: j.ConsecutiveFailures,
|
||||
}
|
||||
}
|
||||
|
||||
type jsonWorker struct {
|
||||
Concurrency int `json:"concurrency"`
|
||||
MaxBatchSize int `json:"maxBatchSize"`
|
||||
ProcessTimeout Duration `json:"processTimeout"`
|
||||
ShutdownTimeout Duration `json:"shutdownTimeout"`
|
||||
BackoffMin Duration `json:"backoffMin"`
|
||||
BackoffMax Duration `json:"backoffMax"`
|
||||
PermanentFailureDelay Duration `json:"permanentFailureDelay"`
|
||||
MonitorAddr string `json:"monitorAddr"`
|
||||
HealthErrorThreshold int `json:"healthErrorThreshold"`
|
||||
DepthPollInterval Duration `json:"depthPollInterval"`
|
||||
}
|
||||
|
||||
type jsonOutbox struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
PollInterval Duration `json:"pollInterval"`
|
||||
BatchSize int `json:"batchSize"`
|
||||
DispatchTimeout Duration `json:"dispatchTimeout"`
|
||||
LockTTL Duration `json:"lockTTL"`
|
||||
MaxAttempts int `json:"maxAttempts"`
|
||||
}
|
||||
|
||||
type jsonSQS struct {
|
||||
Region string `json:"region"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
AllowInsecureEndpoint bool `json:"allowInsecureEndpoint"`
|
||||
QueueURL string `json:"queueUrl"`
|
||||
QueueType string `json:"queueType"`
|
||||
MessageGroupID string `json:"messageGroupId"`
|
||||
MaxMessages int `json:"maxMessages"`
|
||||
WaitTimeSeconds int32 `json:"waitTimeSeconds"`
|
||||
VisibilityTimeout Duration `json:"visibilityTimeout"`
|
||||
}
|
||||
|
||||
func (j jsonSQS) toSQSConfig(regionFallback, endpointFallback, accessKeyID, secretAccessKey string) SQSConfig {
|
||||
region := j.Region
|
||||
if region == "" {
|
||||
region = regionFallback
|
||||
}
|
||||
endpoint := j.Endpoint
|
||||
if endpoint == "" {
|
||||
endpoint = endpointFallback
|
||||
}
|
||||
return SQSConfig{
|
||||
Region: region,
|
||||
AccessKeyID: strings.TrimSpace(accessKeyID),
|
||||
SecretAccessKey: strings.TrimSpace(secretAccessKey),
|
||||
Endpoint: endpoint,
|
||||
QueueURL: j.QueueURL,
|
||||
QueueType: j.QueueType,
|
||||
MessageGroupID: j.MessageGroupID,
|
||||
MaxMessages: j.MaxMessages,
|
||||
WaitTimeSeconds: j.WaitTimeSeconds,
|
||||
VisibilityTimeout: j.VisibilityTimeout.Duration,
|
||||
AllowInsecureEndpoint: j.AllowInsecureEndpoint,
|
||||
}
|
||||
}
|
||||
|
||||
type jsonAppConfig struct {
|
||||
HTTP struct {
|
||||
Port int `json:"port"`
|
||||
BodyLimit int `json:"bodyLimit"`
|
||||
Timeouts struct {
|
||||
Read Duration `json:"read"`
|
||||
Write Duration `json:"write"`
|
||||
Idle Duration `json:"idle"`
|
||||
Request Duration `json:"request"`
|
||||
RequestOverrides string `json:"requestOverrides"`
|
||||
Shutdown Duration `json:"shutdown"`
|
||||
} `json:"timeouts"`
|
||||
CORS struct {
|
||||
AllowOrigins string `json:"allowOrigins"`
|
||||
AllowMethods string `json:"allowMethods"`
|
||||
AllowHeaders string `json:"allowHeaders"`
|
||||
ExposeHeaders string `json:"exposeHeaders"`
|
||||
AllowCredentials bool `json:"allowCredentials"`
|
||||
MaxAge int `json:"maxAge"`
|
||||
} `json:"cors"`
|
||||
RateLimit struct {
|
||||
Max int `json:"max"`
|
||||
Window Duration `json:"window"`
|
||||
Endpoints string `json:"endpoints"`
|
||||
} `json:"rateLimit"`
|
||||
Security struct {
|
||||
HSTSMaxAge int `json:"hstsMaxAge"`
|
||||
} `json:"security"`
|
||||
} `json:"http"`
|
||||
|
||||
Database struct {
|
||||
MySQL struct {
|
||||
DSN string `json:"dsn"`
|
||||
MaxOpenConns int `json:"maxOpenConns"`
|
||||
MaxIdleConns int `json:"maxIdleConns"`
|
||||
ConnMaxLifetime Duration `json:"connMaxLifetime"`
|
||||
ConnMaxIdleTime Duration `json:"connMaxIdleTime"`
|
||||
} `json:"mysql"`
|
||||
} `json:"database"`
|
||||
|
||||
Logging struct {
|
||||
Level string `json:"level"`
|
||||
Format string `json:"format"`
|
||||
} `json:"logging"`
|
||||
|
||||
Audit struct {
|
||||
QueueSize int `json:"queueSize"`
|
||||
Workers int `json:"workers"`
|
||||
} `json:"audit"`
|
||||
|
||||
Email struct {
|
||||
Provider string `json:"provider"`
|
||||
MockDir string `json:"mockDir"`
|
||||
} `json:"email"`
|
||||
|
||||
CircuitBreaker struct {
|
||||
Default jsonCircuitBreakerPolicy `json:"default"`
|
||||
MicrosoftSSO jsonCircuitBreakerPolicy `json:"microsoftSSO"`
|
||||
SES jsonCircuitBreakerPolicy `json:"ses"`
|
||||
SQS jsonCircuitBreakerPolicy `json:"sqs"`
|
||||
} `json:"circuitBreaker"`
|
||||
|
||||
Auth struct {
|
||||
PublicBaseURL string `json:"publicBaseURL"`
|
||||
PasswordPepper string `json:"passwordPepper"`
|
||||
IPEncryptionKey string `json:"ipEncryptionKey"`
|
||||
DefaultRole string `json:"defaultRole"`
|
||||
DisableRegister bool `json:"disableRegister"`
|
||||
Hash struct {
|
||||
Time uint32 `json:"time"`
|
||||
MemoryKB uint32 `json:"memoryKB"`
|
||||
Threads uint8 `json:"threads"`
|
||||
KeyLen uint32 `json:"keyLen"`
|
||||
} `json:"hash"`
|
||||
Email struct {
|
||||
VerifyTTL Duration `json:"verifyTTL"`
|
||||
VerifyURL string `json:"verifyURL"`
|
||||
InviteTTL Duration `json:"inviteTTL"`
|
||||
} `json:"email"`
|
||||
Password struct {
|
||||
SetURL string `json:"setURL"`
|
||||
ResetTTL Duration `json:"resetTTL"`
|
||||
ResetURL string `json:"resetURL"`
|
||||
ForgotEmailCooldown Duration `json:"forgotEmailCooldown"`
|
||||
ForgotMinDuration Duration `json:"forgotMinDuration"`
|
||||
ForgotIPRateMax int `json:"forgotIPRateMax"`
|
||||
ForgotIPRateWindow Duration `json:"forgotIPRateWindow"`
|
||||
LoginIPRateMax int `json:"loginIPRateMax"`
|
||||
LoginIPRateWindow Duration `json:"loginIPRateWindow"`
|
||||
} `json:"password"`
|
||||
SecurityPin struct {
|
||||
ResetTTL Duration `json:"resetTTL"`
|
||||
ResetURL string `json:"resetURL"`
|
||||
ForgotEmailCooldown Duration `json:"forgotEmailCooldown"`
|
||||
ForgotMinDuration Duration `json:"forgotMinDuration"`
|
||||
MaxAttempts int `json:"maxAttempts"`
|
||||
LockDuration Duration `json:"lockDuration"`
|
||||
ActionTokenTTL Duration `json:"actionTokenTTL"`
|
||||
} `json:"securityPin"`
|
||||
JWT struct {
|
||||
AccessSecret string `json:"accessSecret"`
|
||||
RefreshSecret string `json:"refreshSecret"`
|
||||
AccessTTL Duration `json:"accessTTL"`
|
||||
RefreshTTL Duration `json:"refreshTTL"`
|
||||
RefreshTotpTTL Duration `json:"refreshTotpTTL"`
|
||||
LocalRefreshTTL Duration `json:"localRefreshTTL"`
|
||||
SessionIdleTTL Duration `json:"sessionIdleTTL"`
|
||||
Cookie struct {
|
||||
Domain string `json:"domain"`
|
||||
Secure bool `json:"secure"`
|
||||
SameSite string `json:"sameSite"`
|
||||
AccessName string `json:"accessName"`
|
||||
RefreshName string `json:"refreshName"`
|
||||
} `json:"cookie"`
|
||||
} `json:"jwt"`
|
||||
TOTP struct {
|
||||
ChallengeTTL Duration `json:"challengeTTL"`
|
||||
} `json:"totp"`
|
||||
SSO struct {
|
||||
StateTTL Duration `json:"stateTTL"`
|
||||
SuccessRedirect string `json:"successRedirect"`
|
||||
} `json:"sso"`
|
||||
WebAuthn struct {
|
||||
RPID string `json:"rpId"`
|
||||
RPDisplayName string `json:"rpDisplayName"`
|
||||
RPOrigins string `json:"rpOrigins"`
|
||||
ChallengeTTL Duration `json:"challengeTTL"`
|
||||
} `json:"webauthn"`
|
||||
} `json:"auth"`
|
||||
|
||||
TOTP struct {
|
||||
Issuer string `json:"issuer"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
} `json:"totp"`
|
||||
|
||||
MicrosoftSSO struct {
|
||||
TenantID string `json:"tenantId"`
|
||||
ClientID string `json:"clientId"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
RedirectURL string `json:"redirectUrl"`
|
||||
Authority string `json:"authority"`
|
||||
Scopes string `json:"scopes"`
|
||||
} `json:"microsoftSSO"`
|
||||
SAML struct {
|
||||
CertFile string `json:"certFile"`
|
||||
KeyFile string `json:"keyFile"`
|
||||
MetadataFile string `json:"metadataFile"`
|
||||
RootURL string `json:"rootUrl"`
|
||||
FrontendURL string `json:"frontendUrl"`
|
||||
CookieDomain string `json:"cookieDomain"`
|
||||
CookieSecure bool `json:"cookieSecure"`
|
||||
SessionTTLHr int `json:"sessionTTLHour"`
|
||||
} `json:"saml"`
|
||||
|
||||
Queue struct {
|
||||
MessageSchemaVersion string `json:"messageSchemaVersion"`
|
||||
EnableLegacyPayloadFallback bool `json:"enableLegacyPayloadFallback"`
|
||||
IdempotencyKeyPrefix string `json:"idempotencyKeyPrefix"`
|
||||
IdempotencyTTL Duration `json:"idempotencyTTL"`
|
||||
ProcessingTTL Duration `json:"processingTTL"`
|
||||
Worker jsonWorker `json:"worker"`
|
||||
Outbox jsonOutbox `json:"outbox"`
|
||||
} `json:"queue"`
|
||||
|
||||
FileQueue struct {
|
||||
MessageSchemaVersion string `json:"messageSchemaVersion"`
|
||||
IdempotencyKeyPrefix string `json:"idempotencyKeyPrefix"`
|
||||
IdempotencyTTL Duration `json:"idempotencyTTL"`
|
||||
ProcessingTTL Duration `json:"processingTTL"`
|
||||
Worker jsonWorker `json:"worker"`
|
||||
Outbox jsonOutbox `json:"outbox"`
|
||||
} `json:"fileQueue"`
|
||||
|
||||
AWS struct {
|
||||
Region string `json:"region"`
|
||||
AccessKeyID string `json:"accessKeyId"`
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
SQS jsonSQS `json:"sqs"`
|
||||
FileSQS jsonSQS `json:"fileSqs"`
|
||||
SES struct {
|
||||
Region string `json:"region"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
From string `json:"from"`
|
||||
ConfigurationSet string `json:"configurationSet"`
|
||||
SendTimeout Duration `json:"sendTimeout"`
|
||||
AllowInsecureEndpoint bool `json:"allowInsecureEndpoint"`
|
||||
} `json:"ses"`
|
||||
} `json:"aws"`
|
||||
|
||||
File struct {
|
||||
Provider string `json:"provider"`
|
||||
S3 struct {
|
||||
Bucket string `json:"bucket"`
|
||||
Region string `json:"region"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
ForcePathStyle bool `json:"forcePathStyle"`
|
||||
AllowInsecureEndpoint bool `json:"allowInsecureEndpoint"`
|
||||
AutoCreateBucket bool `json:"autoCreateBucket"`
|
||||
ObjectKeyPrefix string `json:"objectKeyPrefix"`
|
||||
PresignPutTTL Duration `json:"presignPutTTL"`
|
||||
PresignGetTTL Duration `json:"presignGetTTL"`
|
||||
} `json:"s3"`
|
||||
Upload struct {
|
||||
MaxSize int64 `json:"maxSize"`
|
||||
AllowedMimeTypes string `json:"allowedMimeTypes"`
|
||||
AllowedExtensions string `json:"allowedExtensions"`
|
||||
AllowEmptyFile bool `json:"allowEmptyFile"`
|
||||
MaxFilenameLength int `json:"maxFilenameLength"`
|
||||
} `json:"upload"`
|
||||
} `json:"file"`
|
||||
|
||||
WOPI struct {
|
||||
TokenSecret string `json:"tokenSecret"`
|
||||
ProofSecret string `json:"proofSecret"`
|
||||
DiscoveryURL string `json:"discoveryUrl"`
|
||||
PublicBaseURL string `json:"publicBaseUrl"`
|
||||
EditActionURL string `json:"editActionUrl"`
|
||||
TokenTTL Duration `json:"tokenTTL"`
|
||||
MaxBodyBytes int `json:"maxBodyBytes"`
|
||||
RequireProof bool `json:"requireProof"`
|
||||
} `json:"wopi"`
|
||||
|
||||
Gotenberg struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"gotenberg"`
|
||||
}
|
||||
|
||||
func (r *jsonAppConfig) toAppConfig() AppConfig {
|
||||
// IPEncryptionKey falls back to PasswordPepper when not set (mirrors env loader behaviour)
|
||||
ipKey := r.Auth.IPEncryptionKey
|
||||
if ipKey == "" {
|
||||
ipKey = r.Auth.PasswordPepper
|
||||
}
|
||||
|
||||
// RefreshTotpTTL falls back to RefreshTTL when not set
|
||||
refreshTotpTTL := r.Auth.JWT.RefreshTotpTTL.Duration
|
||||
if refreshTotpTTL == 0 {
|
||||
refreshTotpTTL = r.Auth.JWT.RefreshTTL.Duration
|
||||
}
|
||||
|
||||
// SES region falls back to aws.region
|
||||
sesRegion := r.AWS.SES.Region
|
||||
if sesRegion == "" {
|
||||
sesRegion = r.AWS.Region
|
||||
}
|
||||
|
||||
return AppConfig{
|
||||
HTTPPort: r.HTTP.Port,
|
||||
|
||||
MySQL: MySQLConfig{
|
||||
DSN: r.Database.MySQL.DSN,
|
||||
MaxOpenConns: r.Database.MySQL.MaxOpenConns,
|
||||
MaxIdleConns: r.Database.MySQL.MaxIdleConns,
|
||||
ConnMaxLifetime: r.Database.MySQL.ConnMaxLifetime.Duration,
|
||||
ConnMaxIdleTime: r.Database.MySQL.ConnMaxIdleTime.Duration,
|
||||
},
|
||||
|
||||
Logging: LoggingConfig{
|
||||
Level: r.Logging.Level,
|
||||
Format: r.Logging.Format,
|
||||
},
|
||||
|
||||
Audit: AuditConfig{
|
||||
QueueSize: r.Audit.QueueSize,
|
||||
Workers: r.Audit.Workers,
|
||||
},
|
||||
|
||||
Email: EmailDeliveryConfig{
|
||||
Provider: strings.ToLower(strings.TrimSpace(r.Email.Provider)),
|
||||
MockDir: strings.TrimSpace(r.Email.MockDir),
|
||||
},
|
||||
|
||||
SES: SESConfig{
|
||||
Region: sesRegion,
|
||||
Endpoint: r.AWS.SES.Endpoint,
|
||||
From: r.AWS.SES.From,
|
||||
ConfigurationSetName: r.AWS.SES.ConfigurationSet,
|
||||
SendTimeout: r.AWS.SES.SendTimeout.Duration,
|
||||
AllowInsecureEndpoint: r.AWS.SES.AllowInsecureEndpoint,
|
||||
},
|
||||
|
||||
Auth: AuthConfig{
|
||||
PublicBaseURL: r.Auth.PublicBaseURL,
|
||||
PasswordPepper: r.Auth.PasswordPepper,
|
||||
IPEncryptionKey: ipKey,
|
||||
HashTime: r.Auth.Hash.Time,
|
||||
HashMemoryKB: r.Auth.Hash.MemoryKB,
|
||||
HashThreads: r.Auth.Hash.Threads,
|
||||
HashKeyLen: r.Auth.Hash.KeyLen,
|
||||
EmailVerifyTTL: r.Auth.Email.VerifyTTL.Duration,
|
||||
EmailVerifyURL: r.Auth.Email.VerifyURL,
|
||||
InviteTTL: r.Auth.Email.InviteTTL.Duration,
|
||||
SetPasswordURL: r.Auth.Password.SetURL,
|
||||
PasswordResetTTL: r.Auth.Password.ResetTTL.Duration,
|
||||
PasswordResetURL: r.Auth.Password.ResetURL,
|
||||
SecurityPINResetTTL: r.Auth.SecurityPin.ResetTTL.Duration,
|
||||
SecurityPINResetURL: r.Auth.SecurityPin.ResetURL,
|
||||
ForgotSecurityPINCooldown: r.Auth.SecurityPin.ForgotEmailCooldown.Duration,
|
||||
ForgotSecurityPINMinDuration: r.Auth.SecurityPin.ForgotMinDuration.Duration,
|
||||
SecurityPINMaxAttempts: r.Auth.SecurityPin.MaxAttempts,
|
||||
SecurityPINLockDuration: r.Auth.SecurityPin.LockDuration.Duration,
|
||||
SecurityPINActionTokenTTL: r.Auth.SecurityPin.ActionTokenTTL.Duration,
|
||||
ForgotPasswordEmailCooldown: r.Auth.Password.ForgotEmailCooldown.Duration,
|
||||
ForgotPasswordMinDuration: r.Auth.Password.ForgotMinDuration.Duration,
|
||||
ForgotPasswordIPRateMax: r.Auth.Password.ForgotIPRateMax,
|
||||
ForgotPasswordIPRateWindow: r.Auth.Password.ForgotIPRateWindow.Duration,
|
||||
LoginIPRateMax: r.Auth.Password.LoginIPRateMax,
|
||||
LoginIPRateWindow: r.Auth.Password.LoginIPRateWindow.Duration,
|
||||
TOTPChallengeTTL: r.Auth.TOTP.ChallengeTTL.Duration,
|
||||
SSOStateTTL: r.Auth.SSO.StateTTL.Duration,
|
||||
TOTPIssuer: r.TOTP.Issuer,
|
||||
TOTPSecretKeyB64: r.TOTP.SecretKey,
|
||||
JWTAccessSecret: r.Auth.JWT.AccessSecret,
|
||||
JWTRefreshSecret: r.Auth.JWT.RefreshSecret,
|
||||
JWTAccessTTL: r.Auth.JWT.AccessTTL.Duration,
|
||||
JWTRefreshTTL: r.Auth.JWT.RefreshTTL.Duration,
|
||||
JWTRefreshTOTPTTL: refreshTotpTTL,
|
||||
JWTLocalRefreshTTL: r.Auth.JWT.LocalRefreshTTL.Duration,
|
||||
SessionIdleTTL: r.Auth.JWT.SessionIdleTTL.Duration,
|
||||
JWTCookieDomain: r.Auth.JWT.Cookie.Domain,
|
||||
JWTCookieSecure: r.Auth.JWT.Cookie.Secure,
|
||||
JWTCookieSameSite: r.Auth.JWT.Cookie.SameSite,
|
||||
JWTAccessCookieName: r.Auth.JWT.Cookie.AccessName,
|
||||
JWTRefreshCookieName: r.Auth.JWT.Cookie.RefreshName,
|
||||
WebAuthnRPID: r.Auth.WebAuthn.RPID,
|
||||
WebAuthnRPDisplayName: r.Auth.WebAuthn.RPDisplayName,
|
||||
WebAuthnRPOrigins: splitCSV(r.Auth.WebAuthn.RPOrigins),
|
||||
WebAuthnChallengeTTL: r.Auth.WebAuthn.ChallengeTTL.Duration,
|
||||
SSOSuccessRedirectURL: r.Auth.SSO.SuccessRedirect,
|
||||
DefaultRoleName: r.Auth.DefaultRole,
|
||||
DisableRegister: r.Auth.DisableRegister,
|
||||
},
|
||||
|
||||
MicrosoftSSO: MicrosoftSSOConfig{
|
||||
TenantID: r.MicrosoftSSO.TenantID,
|
||||
ClientID: r.MicrosoftSSO.ClientID,
|
||||
ClientSecret: r.MicrosoftSSO.ClientSecret,
|
||||
RedirectURL: r.MicrosoftSSO.RedirectURL,
|
||||
Authority: r.MicrosoftSSO.Authority,
|
||||
Scopes: splitCSV(r.MicrosoftSSO.Scopes),
|
||||
},
|
||||
SAML: SAMLConfig{
|
||||
CertFile: strings.TrimSpace(r.SAML.CertFile),
|
||||
KeyFile: strings.TrimSpace(r.SAML.KeyFile),
|
||||
MetadataFile: strings.TrimSpace(r.SAML.MetadataFile),
|
||||
RootURL: strings.TrimSpace(r.SAML.RootURL),
|
||||
FrontendURL: strings.TrimSpace(r.SAML.FrontendURL),
|
||||
CookieDomain: strings.TrimSpace(r.SAML.CookieDomain),
|
||||
CookieSecure: r.SAML.CookieSecure,
|
||||
SessionTTLHr: r.SAML.SessionTTLHr,
|
||||
},
|
||||
|
||||
HTTP: HTTPConfig{
|
||||
BodyLimit: r.HTTP.BodyLimit,
|
||||
ReadTimeout: r.HTTP.Timeouts.Read.Duration,
|
||||
WriteTimeout: r.HTTP.Timeouts.Write.Duration,
|
||||
IdleTimeout: r.HTTP.Timeouts.Idle.Duration,
|
||||
RequestTimeout: r.HTTP.Timeouts.Request.Duration,
|
||||
RequestTimeoutByPath: parseHTTPTimeoutOverrides(r.HTTP.Timeouts.RequestOverrides),
|
||||
ShutdownTimeout: r.HTTP.Timeouts.Shutdown.Duration,
|
||||
CORSAllowOrigins: r.HTTP.CORS.AllowOrigins,
|
||||
CORSAllowMethods: r.HTTP.CORS.AllowMethods,
|
||||
CORSAllowHeaders: r.HTTP.CORS.AllowHeaders,
|
||||
CORSExposeHeaders: r.HTTP.CORS.ExposeHeaders,
|
||||
CORSAllowCredentials: r.HTTP.CORS.AllowCredentials,
|
||||
CORSMaxAge: r.HTTP.CORS.MaxAge,
|
||||
RateLimitMax: r.HTTP.RateLimit.Max,
|
||||
RateLimitWindow: r.HTTP.RateLimit.Window.Duration,
|
||||
RateLimitByEndpoint: parseHTTPEndpointRateLimitOverrides(r.HTTP.RateLimit.Endpoints),
|
||||
HSTSMaxAge: r.HTTP.Security.HSTSMaxAge,
|
||||
},
|
||||
|
||||
Resilience: ResilienceConfig{
|
||||
Default: r.CircuitBreaker.Default.toPolicy(),
|
||||
MicrosoftSSO: r.CircuitBreaker.MicrosoftSSO.toPolicy(),
|
||||
SES: r.CircuitBreaker.SES.toPolicy(),
|
||||
SQS: r.CircuitBreaker.SQS.toPolicy(),
|
||||
},
|
||||
|
||||
Queue: QueueConfig{
|
||||
MessageSchemaVersion: r.Queue.MessageSchemaVersion,
|
||||
EnableLegacyPayloadFallback: r.Queue.EnableLegacyPayloadFallback,
|
||||
IdempotencyKeyPrefix: r.Queue.IdempotencyKeyPrefix,
|
||||
IdempotencyTTL: r.Queue.IdempotencyTTL.Duration,
|
||||
ProcessingTTL: r.Queue.ProcessingTTL.Duration,
|
||||
WorkerConcurrency: r.Queue.Worker.Concurrency,
|
||||
WorkerMaxBatchSize: r.Queue.Worker.MaxBatchSize,
|
||||
WorkerProcessTimeout: r.Queue.Worker.ProcessTimeout.Duration,
|
||||
WorkerShutdownTimeout: r.Queue.Worker.ShutdownTimeout.Duration,
|
||||
WorkerBackoffMin: r.Queue.Worker.BackoffMin.Duration,
|
||||
WorkerBackoffMax: r.Queue.Worker.BackoffMax.Duration,
|
||||
WorkerPermanentFailureDelay: r.Queue.Worker.PermanentFailureDelay.Duration,
|
||||
WorkerMonitorAddr: r.Queue.Worker.MonitorAddr,
|
||||
WorkerHealthErrorThreshold: r.Queue.Worker.HealthErrorThreshold,
|
||||
WorkerDepthPollInterval: r.Queue.Worker.DepthPollInterval.Duration,
|
||||
OutboxEnabled: r.Queue.Outbox.Enabled,
|
||||
OutboxPollInterval: r.Queue.Outbox.PollInterval.Duration,
|
||||
OutboxBatchSize: r.Queue.Outbox.BatchSize,
|
||||
OutboxDispatchTimeout: r.Queue.Outbox.DispatchTimeout.Duration,
|
||||
OutboxLockTTL: r.Queue.Outbox.LockTTL.Duration,
|
||||
OutboxMaxAttempts: r.Queue.Outbox.MaxAttempts,
|
||||
},
|
||||
|
||||
FileQueue: FileQueueConfig{
|
||||
MessageSchemaVersion: r.FileQueue.MessageSchemaVersion,
|
||||
IdempotencyKeyPrefix: r.FileQueue.IdempotencyKeyPrefix,
|
||||
IdempotencyTTL: r.FileQueue.IdempotencyTTL.Duration,
|
||||
ProcessingTTL: r.FileQueue.ProcessingTTL.Duration,
|
||||
WorkerConcurrency: r.FileQueue.Worker.Concurrency,
|
||||
WorkerMaxBatchSize: r.FileQueue.Worker.MaxBatchSize,
|
||||
WorkerProcessTimeout: r.FileQueue.Worker.ProcessTimeout.Duration,
|
||||
WorkerShutdownTimeout: r.FileQueue.Worker.ShutdownTimeout.Duration,
|
||||
WorkerBackoffMin: r.FileQueue.Worker.BackoffMin.Duration,
|
||||
WorkerBackoffMax: r.FileQueue.Worker.BackoffMax.Duration,
|
||||
WorkerPermanentFailureDelay: r.FileQueue.Worker.PermanentFailureDelay.Duration,
|
||||
WorkerMonitorAddr: r.FileQueue.Worker.MonitorAddr,
|
||||
WorkerHealthErrorThreshold: r.FileQueue.Worker.HealthErrorThreshold,
|
||||
WorkerDepthPollInterval: r.FileQueue.Worker.DepthPollInterval.Duration,
|
||||
OutboxEnabled: r.FileQueue.Outbox.Enabled,
|
||||
OutboxPollInterval: r.FileQueue.Outbox.PollInterval.Duration,
|
||||
OutboxBatchSize: r.FileQueue.Outbox.BatchSize,
|
||||
OutboxDispatchTimeout: r.FileQueue.Outbox.DispatchTimeout.Duration,
|
||||
OutboxLockTTL: r.FileQueue.Outbox.LockTTL.Duration,
|
||||
OutboxMaxAttempts: r.FileQueue.Outbox.MaxAttempts,
|
||||
},
|
||||
|
||||
// SQS email queue: region falls back to aws.region
|
||||
SQS: r.AWS.SQS.toSQSConfig(r.AWS.Region, "", r.AWS.AccessKeyID, r.AWS.SecretAccessKey),
|
||||
|
||||
// FileSQS: region + endpoint fall back to aws.region / aws.sqs.endpoint
|
||||
FileSQS: r.AWS.FileSQS.toSQSConfig(r.AWS.Region, r.AWS.SQS.Endpoint, r.AWS.AccessKeyID, r.AWS.SecretAccessKey),
|
||||
|
||||
FileStorage: FileStorageConfig{
|
||||
Provider: strings.ToLower(strings.TrimSpace(r.File.Provider)),
|
||||
Bucket: strings.TrimSpace(r.File.S3.Bucket),
|
||||
Region: strings.TrimSpace(r.File.S3.Region),
|
||||
Endpoint: strings.TrimSpace(r.File.S3.Endpoint),
|
||||
AccessKeyID: strings.TrimSpace(r.AWS.AccessKeyID),
|
||||
SecretAccessKey: strings.TrimSpace(r.AWS.SecretAccessKey),
|
||||
ForcePathStyle: r.File.S3.ForcePathStyle,
|
||||
AllowInsecureEndpoint: r.File.S3.AllowInsecureEndpoint,
|
||||
AutoCreateBucket: r.File.S3.AutoCreateBucket,
|
||||
ObjectKeyPrefix: strings.TrimSpace(r.File.S3.ObjectKeyPrefix),
|
||||
PresignPutTTL: r.File.S3.PresignPutTTL.Duration,
|
||||
PresignGetTTL: r.File.S3.PresignGetTTL.Duration,
|
||||
},
|
||||
|
||||
FileUpload: FileManagerUploadPolicyConfig{
|
||||
MaxFileSizeBytes: r.File.Upload.MaxSize,
|
||||
AllowedMimeTypes: normalizeMimeTypes(splitCSV(r.File.Upload.AllowedMimeTypes)),
|
||||
AllowedExtensions: normalizeExtensions(splitCSV(r.File.Upload.AllowedExtensions)),
|
||||
AllowEmptyFile: r.File.Upload.AllowEmptyFile,
|
||||
MaxFilenameLength: r.File.Upload.MaxFilenameLength,
|
||||
},
|
||||
|
||||
WOPI: WOPIConfig{
|
||||
TokenSecret: strings.TrimSpace(r.WOPI.TokenSecret),
|
||||
ProofSecret: strings.TrimSpace(r.WOPI.ProofSecret),
|
||||
DiscoveryURL: strings.TrimSpace(r.WOPI.DiscoveryURL),
|
||||
PublicBaseURL: strings.TrimSpace(r.WOPI.PublicBaseURL),
|
||||
EditActionURL: strings.TrimSpace(r.WOPI.EditActionURL),
|
||||
TokenTTL: r.WOPI.TokenTTL.Duration,
|
||||
MaxBodyBytes: r.WOPI.MaxBodyBytes,
|
||||
RequireProof: r.WOPI.RequireProof,
|
||||
},
|
||||
|
||||
Gotenberg: GotenbergConfig{
|
||||
URL: strings.TrimSpace(r.Gotenberg.URL),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func splitCSV(s string) []string {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(s, ",")
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
if p = strings.TrimSpace(p); p != "" {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user