use super::*;
fn parse_bool(var: &str, default: bool) -> bool {
std::env::var(var)
.map(|v| v == "true" || v == "1")
.unwrap_or(default)
}
fn parse_u64(var: &str, default: fn() -> u64) -> u64 {
std::env::var(var)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or_else(default)
}
fn parse_u32(var: &str, default: fn() -> u32) -> u32 {
std::env::var(var)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or_else(default)
}
fn parse_u16(var: &str, default: fn() -> u16) -> u16 {
std::env::var(var)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or_else(default)
}
pub fn load_server_config() -> ServerConfig {
ServerConfig {
host: std::env::var("HOST").unwrap_or_else(|_| default_host()),
port: parse_u16("PORT", default_port),
auth_base_path: std::env::var("AUTH_BASE_PATH")
.unwrap_or_else(|_| default_auth_base_path()),
frontend_url: std::env::var("FRONTEND_URL").ok(),
sso_callback_url: std::env::var("SSO_CALLBACK_URL").ok(),
bootstrap_admin_email: std::env::var("BOOTSTRAP_ADMIN_EMAIL").ok(),
trust_proxy: std::env::var("TRUST_PROXY")
.map(|v| v == "true" || v == "1")
.unwrap_or(false),
}
}
pub fn load_jwt_config(secret: String) -> JwtConfig {
let rsa_private_key_pem = std::env::var("JWT_RSA_PRIVATE_KEY").ok();
JwtConfig {
secret,
rsa_private_key_pem,
issuer: std::env::var("JWT_ISSUER").unwrap_or_else(|_| default_issuer()),
audience: std::env::var("JWT_AUDIENCE").unwrap_or_else(|_| default_audience()),
access_token_expiry: parse_u64("JWT_ACCESS_EXPIRY", default_access_expiry),
refresh_token_expiry: parse_u64("JWT_REFRESH_EXPIRY", default_refresh_expiry),
}
}
pub fn load_email_config() -> EmailConfig {
EmailConfig {
enabled: parse_bool("EMAIL_ENABLED", true),
require_verification: parse_bool(
"EMAIL_REQUIRE_VERIFICATION",
email_require_verification_default(),
),
block_disposable_emails: parse_bool("EMAIL_BLOCK_DISPOSABLE", false),
custom_blocked_domains: std::env::var("EMAIL_CUSTOM_BLOCKED_DOMAINS")
.ok()
.map(|v| {
v.split(',')
.map(|s| s.trim().to_lowercase())
.filter(|s| !s.is_empty())
.collect()
})
.unwrap_or_default(),
}
}
pub fn load_google_config() -> GoogleConfig {
GoogleConfig {
enabled: parse_bool("GOOGLE_ENABLED", true),
client_id: std::env::var("GOOGLE_CLIENT_ID").ok(),
}
}
pub fn load_apple_config() -> AppleConfig {
AppleConfig {
enabled: parse_bool("APPLE_ENABLED", true),
client_id: std::env::var("APPLE_CLIENT_ID").ok(),
allowed_client_ids: std::env::var("APPLE_ALLOWED_CLIENT_IDS")
.map(|v| {
v.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
})
.unwrap_or_default(),
team_id: std::env::var("APPLE_TEAM_ID").ok(),
key_id: std::env::var("APPLE_KEY_ID").ok(),
private_key_pem: std::env::var("APPLE_PRIVATE_KEY_PEM").ok(),
}
}
pub fn load_solana_config() -> SolanaConfig {
SolanaConfig {
enabled: parse_bool("SOLANA_ENABLED", true),
challenge_expiry_seconds: parse_u64("SOLANA_CHALLENGE_EXPIRY", default_challenge_expiry),
}
}
pub fn load_cors_config() -> CorsConfig {
CorsConfig {
allowed_origins: std::env::var("CORS_ORIGINS")
.map(|v| v.split(',').map(|s| s.trim().to_string()).collect())
.unwrap_or_else(|_| CorsConfig::default().allowed_origins),
disabled: std::env::var("CORS_DISABLED")
.map(|v| v == "true" || v == "1")
.unwrap_or(false),
}
}
pub fn load_cookie_config() -> CookieConfig {
CookieConfig {
enabled: parse_bool("COOKIE_ENABLED", true),
domain: std::env::var("COOKIE_DOMAIN").ok(),
secure: parse_bool("COOKIE_SECURE", cookie_secure_default()),
same_site: std::env::var("COOKIE_SAME_SITE").unwrap_or_else(|_| default_same_site()),
access_cookie_name: std::env::var("COOKIE_ACCESS_NAME")
.unwrap_or_else(|_| default_access_cookie_name()),
refresh_cookie_name: std::env::var("COOKIE_REFRESH_NAME")
.unwrap_or_else(|_| default_refresh_cookie_name()),
path_prefix: std::env::var("COOKIE_PATH_PREFIX")
.or_else(|_| std::env::var("AUTH_BASE_PATH"))
.unwrap_or_else(|_| default_path_prefix()),
}
}
fn cookie_secure_default() -> bool {
matches!(
std::env::var("ENVIRONMENT").as_deref(),
Ok("production") | Ok("prod") | Ok("PRODUCTION") | Ok("PROD")
)
}
fn email_require_verification_default() -> bool {
matches!(
std::env::var("ENVIRONMENT").as_deref(),
Ok("production") | Ok("prod") | Ok("PRODUCTION") | Ok("PROD")
)
}
pub fn load_webhook_config() -> WebhookConfig {
WebhookConfig {
enabled: parse_bool("WEBHOOK_ENABLED", false),
url: std::env::var("WEBHOOK_URL").ok(),
secret: std::env::var("WEBHOOK_SECRET").ok(),
timeout_secs: parse_u64("WEBHOOK_TIMEOUT", default_webhook_timeout),
retry_attempts: parse_u32("WEBHOOK_RETRIES", default_webhook_retries),
}
}
pub fn load_rate_limit_config() -> RateLimitConfig {
RateLimitConfig {
enabled: parse_bool("RATE_LIMIT_ENABLED", true),
auth_limit: parse_u32("RATE_LIMIT_AUTH", default_auth_limit),
general_limit: parse_u32("RATE_LIMIT_GENERAL", default_general_limit),
credit_limit: parse_u32("RATE_LIMIT_CREDIT", default_credit_limit),
window_secs: parse_u64("RATE_LIMIT_WINDOW", default_window_secs),
store: std::env::var("RATE_LIMIT_STORE").unwrap_or_else(|_| default_rate_limit_store()),
redis_url: std::env::var("REDIS_URL").ok(),
}
}
pub fn load_database_config() -> DatabaseConfig {
DatabaseConfig {
url: std::env::var("DATABASE_URL").ok(),
max_connections: parse_u32("DATABASE_MAX_CONNECTIONS", default_max_connections),
min_connections: parse_u32("DATABASE_MIN_CONNECTIONS", default_min_connections),
connect_timeout_secs: parse_u64("DATABASE_CONNECT_TIMEOUT", default_connect_timeout),
idle_timeout_secs: parse_u64("DATABASE_IDLE_TIMEOUT", default_idle_timeout),
}
}
pub fn load_notification_config() -> NotificationConfig {
NotificationConfig {
discord_webhook_url: std::env::var("DISCORD_WEBHOOK_URL").ok(),
telegram_bot_token: std::env::var("TELEGRAM_BOT_TOKEN").ok(),
telegram_chat_id: std::env::var("TELEGRAM_CHAT_ID").ok(),
environment: std::env::var("ENVIRONMENT").unwrap_or_else(|_| default_environment()),
}
}
pub fn load_sso_config() -> SsoConfig {
SsoConfig {
enabled: parse_bool("SSO_ENABLED", false),
}
}
pub fn load_webauthn_config() -> WebAuthnConfig {
WebAuthnConfig {
enabled: parse_bool("WEBAUTHN_ENABLED", false),
rp_id: std::env::var("WEBAUTHN_RP_ID").ok(),
rp_name: std::env::var("WEBAUTHN_RP_NAME").ok(),
rp_origin: std::env::var("WEBAUTHN_RP_ORIGIN").ok(),
challenge_ttl_seconds: parse_u64("WEBAUTHN_CHALLENGE_TTL", default_challenge_ttl),
allow_platform: parse_bool("WEBAUTHN_ALLOW_PLATFORM", true),
allow_cross_platform: parse_bool("WEBAUTHN_ALLOW_CROSS_PLATFORM", true),
require_user_verification: parse_bool("WEBAUTHN_REQUIRE_UV", true),
reject_cloned_credentials: parse_bool("WEBAUTHN_REJECT_CLONED", false),
}
}
pub fn load_wallet_config() -> WalletConfig {
use super::WalletRecoveryMode;
WalletConfig {
enabled: parse_bool("WALLET_ENABLED", false),
recovery_mode: std::env::var("WALLET_RECOVERY_MODE")
.ok()
.and_then(|v| v.parse::<WalletRecoveryMode>().ok())
.unwrap_or_default(),
unlock_ttl_secs: parse_u64("WALLET_UNLOCK_TTL", default_wallet_unlock_ttl),
}
}
pub fn load_privacy_config() -> super::PrivacyConfig {
use super::privacy::*;
super::PrivacyConfig {
enabled: parse_bool("PRIVACY_CASH_ENABLED", false),
sidecar_url: std::env::var("PRIVACY_CASH_SIDECAR_URL")
.unwrap_or_else(|_| default_sidecar_url()),
sidecar_timeout_ms: parse_u64(
"PRIVACY_CASH_SIDECAR_TIMEOUT_MS",
default_sidecar_timeout_ms,
),
sidecar_api_key: std::env::var("SIDECAR_API_KEY").ok(),
note_encryption_key: std::env::var("NOTE_ENCRYPTION_KEY").ok(),
note_encryption_key_id: std::env::var("NOTE_ENCRYPTION_KEY_ID")
.unwrap_or_else(|_| "v1".to_string()),
min_deposit_lamports: parse_u64("DEPOSIT_MIN_LAMPORTS", default_min_deposit_lamports),
max_deposit_lamports: parse_u64("DEPOSIT_MAX_LAMPORTS", default_max_deposit_lamports),
max_spend_per_transaction_lamports: parse_u64(
"MAX_SPEND_PER_TRANSACTION_LAMPORTS",
default_max_spend_per_transaction_lamports,
),
session_ttl_secs: parse_u64("DEPOSIT_SESSION_TTL_SECS", default_session_ttl_secs),
company_wallet_address: std::env::var("COMPANY_WALLET_ADDRESS").ok(),
company_currency: std::env::var("COMPANY_CURRENCY")
.unwrap_or_else(|_| default_company_currency()),
deposit_webhook_secret: std::env::var("DEPOSIT_WEBHOOK_SECRET").ok(),
spl_token_whitelist: std::env::var("SPL_TOKEN_WHITELIST")
.ok()
.map(|s| s.split(',').map(|t| t.trim().to_string()).collect())
.unwrap_or_else(|| vec![USDC_MINT.to_string(), USDT_MINT.to_string()]),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_env::{clear_env, lock_env, set_env};
#[test]
fn test_cookie_secure_default_in_production() {
let _lock = lock_env();
let _env = set_env("ENVIRONMENT", Some("production"));
let _clear_secure = clear_env("COOKIE_SECURE");
let config = load_cookie_config();
assert!(config.secure);
}
#[test]
fn test_cookie_secure_default_in_development() {
let _lock = lock_env();
let _env = set_env("ENVIRONMENT", Some("development"));
let _clear_secure = clear_env("COOKIE_SECURE");
let config = load_cookie_config();
assert!(!config.secure);
}
#[test]
fn test_cookie_secure_env_override_in_production() {
let _lock = lock_env();
let _env = set_env("ENVIRONMENT", Some("production"));
let _secure = set_env("COOKIE_SECURE", Some("false"));
let config = load_cookie_config();
assert!(!config.secure);
}
#[test]
fn test_email_verification_default_in_production() {
let _lock = lock_env();
let _env = set_env("ENVIRONMENT", Some("production"));
let _clear = clear_env("EMAIL_REQUIRE_VERIFICATION");
let config = load_email_config();
assert!(config.require_verification);
}
#[test]
fn test_email_verification_default_in_development() {
let _lock = lock_env();
let _env = set_env("ENVIRONMENT", Some("development"));
let _clear = clear_env("EMAIL_REQUIRE_VERIFICATION");
let config = load_email_config();
assert!(!config.require_verification);
}
#[test]
fn test_cors_defaults_to_empty_when_unset() {
let _lock = lock_env();
let _clear = clear_env("CORS_ORIGINS");
let config = load_cors_config();
assert!(config.allowed_origins.is_empty());
}
#[test]
fn test_cookie_path_prefix_defaults_to_auth_base_path() {
let _lock = lock_env();
let _auth = set_env("AUTH_BASE_PATH", Some("/auth"));
let _clear = clear_env("COOKIE_PATH_PREFIX");
let config = load_cookie_config();
assert_eq!(config.path_prefix, "/auth");
}
#[test]
fn test_load_server_config_sso_callback_url() {
let _lock = lock_env();
let _env = set_env(
"SSO_CALLBACK_URL",
Some("https://auth.example.com/auth/sso/callback"),
);
let config = load_server_config();
assert_eq!(
config.sso_callback_url.as_deref(),
Some("https://auth.example.com/auth/sso/callback")
);
}
#[test]
fn test_load_server_config_auth_base_path() {
let _lock = lock_env();
let _env = set_env("AUTH_BASE_PATH", Some("/auth/v2"));
let config = load_server_config();
assert_eq!(config.auth_base_path, "/auth/v2");
}
}