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
        )
    }
}