//! Ortam değişkenlerinden uygulama ayarlarını okur. use std::{env, path::PathBuf}; #[derive(Debug, Clone)] pub struct Config { pub mongodb_uri: String, pub jwt_secret: String, pub app_url: String, pub port: u16, pub registration: RegistrationMode, /// Derlenmiş frontend klasörü (`STATIC_DIR`). Tanımlıysa Axum arayüzü de sunar /// (canlıda tek servis). Geliştirmede boş: arayüzü Vite sunar. pub static_dir: Option, /// Otomatik kur yenileme aralığı (dakika, `RATES_REFRESH_MINUTES`, varsayılan 60); 0 → kapalı pub rates_refresh_minutes: u64, /// Cloudflare Turnstile (`TURNSTILE_SITE_KEY` + `TURNSTILE_SECRET_KEY`); ikisi de yoksa kapalı pub turnstile_site_key: Option, pub turnstile_secret_key: Option, /// Ters vekil arkasında (Dokploy/Traefik) gerçek IP `X-Forwarded-For`'dan (`TRUST_PROXY`, varsayılan açık) pub trust_proxy: bool, } /// Yeni hane kaydı politikası (`REGISTRATION` ortam değişkeni). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RegistrationMode { /// Varsayılan. Sistemde hiç hane yokken ilk kayıt yeni hane kurar; /// sonrasında yalnızca davet koduyla katılınır. InviteOnly, /// Herkes yeni hane kurabilir. Open, } #[derive(Debug, thiserror::Error)] pub enum ConfigError { #[error("{0} ortam değişkeni tanımlı değil (.env dosyasını kontrol edin)")] Missing(&'static str), #[error("{0} geçersiz: {1}")] Invalid(&'static str, &'static str), } impl Config { pub fn from_env() -> Result { let mongodb_uri = required("MONGODB_URI")?; let jwt_secret = required("JWT_SECRET")?; if jwt_secret.len() < 32 { return Err(ConfigError::Invalid( "JWT_SECRET", "en az 32 karakter olmalı (openssl rand -hex 32)", )); } let app_url = env::var("APP_URL").unwrap_or_else(|_| "http://localhost:5173".into()); let registration = match env::var("REGISTRATION").as_deref() { Err(_) | Ok("") | Ok("invite_only") => RegistrationMode::InviteOnly, Ok("open") => RegistrationMode::Open, Ok(_) => { return Err(ConfigError::Invalid( "REGISTRATION", "\"invite_only\" veya \"open\" olmalı", )); } }; let static_dir = env::var("STATIC_DIR") .ok() .filter(|v| !v.trim().is_empty()) .map(PathBuf::from); let port = match env::var("PORT") { Ok(p) => p .parse() .map_err(|_| ConfigError::Invalid("PORT", "sayı olmalı"))?, Err(_) => 8080, }; let rates_refresh_minutes = match env::var("RATES_REFRESH_MINUTES") { Ok(m) if !m.trim().is_empty() => m.trim().parse().map_err(|_| { ConfigError::Invalid("RATES_REFRESH_MINUTES", "dakika (sayı) olmalı") })?, _ => 60, }; let opt = |k: &str| { env::var(k) .ok() .map(|v| v.trim().to_string()) .filter(|v| !v.is_empty()) }; let turnstile_site_key = opt("TURNSTILE_SITE_KEY"); let turnstile_secret_key = opt("TURNSTILE_SECRET_KEY"); if turnstile_site_key.is_some() != turnstile_secret_key.is_some() { return Err(ConfigError::Invalid( "TURNSTILE_SITE_KEY / TURNSTILE_SECRET_KEY", "ikisi birlikte tanımlanmalı (ya da ikisi de boş)", )); } let trust_proxy = match env::var("TRUST_PROXY").as_deref().map(str::trim) { Err(_) | Ok("") | Ok("true") | Ok("1") => true, Ok("false") | Ok("0") => false, Ok(_) => { return Err(ConfigError::Invalid( "TRUST_PROXY", "\"true\" veya \"false\" olmalı", )); } }; Ok(Self { mongodb_uri, jwt_secret, app_url, port, registration, static_dir, rates_refresh_minutes, turnstile_site_key, turnstile_secret_key, trust_proxy, }) } /// Cookie'lerde `Secure` bayrağı yalnızca HTTPS'te açılır. pub fn is_https(&self) -> bool { self.app_url.starts_with("https://") } } fn required(key: &'static str) -> Result { env::var(key) .ok() .filter(|v| !v.trim().is_empty()) .ok_or(ConfigError::Missing(key)) }