a2eff1a984
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
137 lines
4.6 KiB
Rust
137 lines
4.6 KiB
Rust
//! 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<PathBuf>,
|
||
/// 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<String>,
|
||
pub turnstile_secret_key: Option<String>,
|
||
/// 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<Self, ConfigError> {
|
||
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<String, ConfigError> {
|
||
env::var(key)
|
||
.ok()
|
||
.filter(|v| !v.trim().is_empty())
|
||
.ok_or(ConfigError::Missing(key))
|
||
}
|