use anyhow::{Context, Result}; #[derive(Clone, Debug)] pub struct Settings { pub port: u16, pub database_url: String, pub oidc_client_id: String, pub oidc_client_secret: String, pub oidc_authorize_url: String, pub oidc_token_url: String, pub oidc_userinfo_url: String, pub oidc_redirect_url: String, pub oidc_scopes: String, pub session_secret: String, pub cookie_secure: bool, pub stun_urls: Vec, pub turn_urls: Vec, pub turn_username: Option, pub turn_password: Option, pub media: Option, } #[derive(Clone, Debug)] pub struct MediaSettings { pub account_id: String, pub access_key_id: String, pub secret_access_key: String, pub bucket: String, pub public_base_url: String, } impl Settings { pub fn from_env() -> Result { Ok(Self { port: std::env::var("PORT") .unwrap_or_else(|_| "3000".into()) .parse() .context("PORT must be a valid u16")?, database_url: required("DATABASE_URL")?, oidc_client_id: required("OIDC_CLIENT_ID")?, oidc_client_secret: required("OIDC_CLIENT_SECRET")?, oidc_authorize_url: required("OIDC_AUTHORIZE_URL")?, oidc_token_url: required("OIDC_TOKEN_URL")?, oidc_userinfo_url: required("OIDC_USERINFO_URL")?, oidc_redirect_url: required("OIDC_REDIRECT_URL")?, oidc_scopes: std::env::var("OIDC_SCOPES") .unwrap_or_else(|_| "openid profile email".to_string()), session_secret: required("SESSION_SECRET")?, cookie_secure: std::env::var("COOKIE_SECURE") .unwrap_or_else(|_| "false".into()) .parse() .context("COOKIE_SECURE must be true/false")?, stun_urls: parse_csv_env("STUN_URLS", "stun:stun.l.google.com:19302"), turn_urls: parse_csv_env("TURN_URLS", ""), turn_username: optional("TURN_USERNAME"), turn_password: optional("TURN_PASSWORD"), media: MediaSettings::from_env()?, }) } } impl MediaSettings { fn from_env() -> Result> { let account_id = optional("R2_ACCOUNT_ID"); let access_key_id = optional("R2_ACCESS_KEY_ID"); let secret_access_key = optional("R2_SECRET_ACCESS_KEY"); let bucket = optional("R2_BUCKET"); let public_base_url = optional("R2_PUBLIC_BASE_URL"); if account_id.is_none() && access_key_id.is_none() && secret_access_key.is_none() && bucket.is_none() && public_base_url.is_none() { return Ok(None); } Ok(Some(Self { account_id: account_id.context("missing env var R2_ACCOUNT_ID")?, access_key_id: access_key_id.context("missing env var R2_ACCESS_KEY_ID")?, secret_access_key: secret_access_key.context("missing env var R2_SECRET_ACCESS_KEY")?, bucket: bucket.context("missing env var R2_BUCKET")?, public_base_url: public_base_url.context("missing env var R2_PUBLIC_BASE_URL")?, })) } pub fn endpoint_url(&self) -> String { format!("https://{}.r2.cloudflarestorage.com", self.account_id) } } fn required(name: &str) -> Result { std::env::var(name).with_context(|| format!("missing env var {name}")) } fn optional(name: &str) -> Option { std::env::var(name) .ok() .map(|v| v.trim().to_string()) .filter(|v| !v.is_empty()) } fn parse_csv_env(name: &str, default_value: &str) -> Vec { let raw = std::env::var(name).unwrap_or_else(|_| default_value.to_string()); raw.split(',') .map(str::trim) .filter(|s| !s.is_empty()) .map(ToString::to_string) .collect() }