oauth.rs
221 lignes · 7851 octets
use std::env; use sea_orm::DatabaseConnection; use crate::services::app_settings_service::AppSettingsService; use crate::services::crypto_service::CryptoService; /// Configuration for a single OAuth provider. #[derive(Debug, Clone)] pub struct OAuthProviderConfig { /// Provider identifier (e.g. `"google"`, `"github"`). pub name: String, /// OAuth client ID registered with the provider. pub client_id: String, /// OAuth client secret (may be AES-GCM encrypted with the `enc:` prefix). pub client_secret: String, /// Azure AD / Microsoft tenant ID (only used by the Microsoft provider). pub tenant: Option<String>, /// Whether this provider is enabled and available for login. pub enabled: bool, } /// Global OAuth configuration. #[derive(Debug, Clone)] pub struct OAuthConfig { /// Whether OAuth login is enabled globally. pub enabled: bool, /// Base URL used to build provider callback URLs (env: `OAUTH_REDIRECT_BASE_URL`). pub redirect_base_url: String, /// Automatically register a new user when an unknown OAuth identity logs in. pub auto_register: bool, /// Link an OAuth identity to an existing account with the same email. pub link_existing_account: bool, /// Configured OAuth providers (only providers with both `client_id` and `client_secret` are included). pub providers: Vec<OAuthProviderConfig>, } impl OAuthConfig { /// Names of all recognised OAuth providers. pub const KNOWN_PROVIDERS: &'static [&'static str] = &["google", "github", "discord", "microsoft"]; /// Load OAuth configuration from the database, falling back to environment /// variables for any value not set in the DB. pub async fn load(db: &DatabaseConnection) -> Self { let db_val = |key: &str| { let db = db.clone(); let key = key.to_string(); async move { AppSettingsService::get_setting(&db, &key) .await .ok() .flatten() .map(|m| m.value) .filter(|v| !v.is_empty()) } }; let enabled = match db_val("oauth_enabled").await { Some(v) => v == "true", None => env::var("OAUTH_ENABLED") .ok() .and_then(|v| v.parse::<bool>().ok()) .unwrap_or(false), }; let redirect_base_url = match db_val("oauth_redirect_base_url").await { Some(v) => v, None => env::var("OAUTH_REDIRECT_BASE_URL") .unwrap_or_else(|_| "http://localhost:3000".to_string()), }; let auto_register = match db_val("oauth_auto_register").await { Some(v) => v == "true", None => true, }; let link_existing_account = match db_val("oauth_link_existing_account").await { Some(v) => v == "true", None => true, }; let mut providers = Vec::new(); for &name in Self::KNOWN_PROVIDERS { let upper = name.to_uppercase(); let provider_enabled = match db_val(&format!("oauth_{}_enabled", name)).await { Some(v) => v == "true", None => env::var(format!("OAUTH_{}_ENABLED", upper)) .ok() .and_then(|v| v.parse::<bool>().ok()) .unwrap_or(true), }; let client_id = match db_val(&format!("oauth_{}_client_id", name)).await { Some(v) => Some(v), None => env::var(format!("OAUTH_{}_CLIENT_ID", upper)).ok(), }; let client_secret_raw = match db_val(&format!("oauth_{}_client_secret", name)).await { Some(v) => Some(v), None => env::var(format!("OAUTH_{}_CLIENT_SECRET", upper)).ok(), }; let client_secret = client_secret_raw.and_then(|raw| { if CryptoService::is_encrypted(&raw) { match CryptoService::decrypt(&raw) { Ok(plain) => Some(plain), Err(e) => { tracing::error!( provider = name, "Failed to decrypt OAuth client secret: {}", e ); None } } } else { Some(raw) } }); let tenant = match db_val(&format!("oauth_{}_tenant", name)).await { Some(v) => Some(v), None => env::var(format!("OAUTH_{}_TENANT", upper)).ok(), }; if let (Some(id), Some(secret)) = (client_id, client_secret) { providers.push(OAuthProviderConfig { name: name.to_string(), client_id: id, client_secret: secret, tenant, enabled: provider_enabled, }); } } if enabled && providers.is_empty() { tracing::warn!( "OAuth is enabled but no providers are configured (neither DB nor env vars)" ); } Self { enabled, redirect_base_url, auto_register, link_existing_account, providers, } } /// Load from environment variables only (no DB access). /// Kept as fallback when DB is not available. pub fn from_env() -> Self { let enabled = env::var("OAUTH_ENABLED") .ok() .and_then(|v| v.parse::<bool>().ok()) .unwrap_or(false); let redirect_base_url = env::var("OAUTH_REDIRECT_BASE_URL") .unwrap_or_else(|_| "http://localhost:3000".to_string()); let mut providers = Vec::new(); for &name in Self::KNOWN_PROVIDERS { let upper = name.to_uppercase(); let client_id = env::var(format!("OAUTH_{}_CLIENT_ID", upper)); let client_secret = env::var(format!("OAUTH_{}_CLIENT_SECRET", upper)); if let (Ok(id), Ok(secret)) = (client_id, client_secret) { let provider_enabled = env::var(format!("OAUTH_{}_ENABLED", upper)) .ok() .and_then(|v| v.parse::<bool>().ok()) .unwrap_or(true); let tenant = env::var(format!("OAUTH_{}_TENANT", upper)).ok(); providers.push(OAuthProviderConfig { name: name.to_string(), client_id: id, client_secret: secret, tenant, enabled: provider_enabled, }); } } if enabled && providers.is_empty() { tracing::warn!("OAUTH_ENABLED=true but no providers are configured"); } Self { enabled, redirect_base_url, auto_register: true, link_existing_account: true, providers, } } /// Return all providers that are currently enabled. pub fn enabled_providers(&self) -> Vec<&OAuthProviderConfig> { self.providers.iter().filter(|p| p.enabled).collect() } /// Look up an enabled provider by name. pub fn get_provider(&self, name: &str) -> Option<&OAuthProviderConfig> { self.providers.iter().find(|p| p.name == name && p.enabled) } /// Build the OAuth callback URL for the given provider name. pub fn callback_url(&self, provider_name: &str) -> String { format!( "{}/api/v1/auth/oauth/{}/callback", self.redirect_base_url, provider_name ) } }
GitRust