Skip to main content

doido_auth/
state.rs

1//! Process-global auth state installed at boot.
2
3use crate::config::AuthConfig;
4use crate::error::AuthError;
5use crate::jwt::JwtStrategy;
6use crate::oauth::{providers_from_config, OAuthProvider};
7use crate::registry::{all_custom_strategies, get_strategy};
8use crate::strategy::AuthStrategy;
9use doido_core::Result;
10use doido_model::sea_orm::DatabaseConnection;
11use std::collections::HashMap;
12use std::sync::{Arc, OnceLock, RwLock};
13
14/// Boot-time auth state: DB handle, config, and enabled strategies.
15pub struct AuthState {
16    pub db: DatabaseConnection,
17    pub config: AuthConfig,
18    pub strategies: Vec<Arc<dyn AuthStrategy>>,
19    pub jwt: Option<Arc<JwtStrategy>>,
20    pub oauth: HashMap<String, Arc<dyn OAuthProvider>>,
21}
22
23static AUTH_STATE: OnceLock<RwLock<Option<Arc<AuthState>>>> = OnceLock::new();
24
25fn slot() -> &'static RwLock<Option<Arc<AuthState>>> {
26    AUTH_STATE.get_or_init(|| RwLock::new(None))
27}
28
29impl AuthState {
30    pub(crate) fn build(db: DatabaseConnection, config: AuthConfig) -> Result<Self, AuthError> {
31        config.validate()?;
32        let mut strategies: Vec<Arc<dyn AuthStrategy>> = Vec::new();
33        let mut jwt = None;
34
35        for name in &config.strategies {
36            match name.as_str() {
37                "cookie" => {
38                    strategies.push(Arc::new(crate::session::from_config(&config)));
39                }
40                "jwt" => {
41                    let jwt_cfg = config
42                        .jwt
43                        .clone()
44                        .ok_or_else(|| AuthError::Config("missing auth.jwt".into()))?;
45                    let strategy = Arc::new(JwtStrategy::new(jwt_cfg)?);
46                    jwt = Some(strategy.clone());
47                    strategies.push(strategy);
48                }
49                custom => {
50                    let strategy = get_strategy(custom)
51                        .ok_or_else(|| AuthError::UnknownStrategy(custom.to_string()))?;
52                    strategies.push(strategy);
53                }
54            }
55        }
56
57        for custom in all_custom_strategies() {
58            if !strategies.iter().any(|s| s.name() == custom.name())
59                && config.strategies.iter().any(|n| n == custom.name())
60            {
61                strategies.push(custom);
62            }
63        }
64
65        let oauth = providers_from_config(&config.oauth);
66
67        Ok(Self {
68            db,
69            config,
70            strategies,
71            jwt,
72            oauth,
73        })
74    }
75}
76
77/// Initialise auth at boot. Idempotent: returns Ok when already initialised.
78pub async fn init(db: DatabaseConnection, config: &AuthConfig) -> Result<()> {
79    let mut guard = slot().write().expect("auth state lock");
80    if guard.is_some() {
81        return Ok(());
82    }
83    let state = AuthState::build(db, config.clone()).map_err(|e| doido_core::anyhow::anyhow!(e))?;
84    *guard = Some(Arc::new(state));
85    Ok(())
86}
87
88/// Install or replace auth state (tests and advanced boot scenarios).
89pub fn set_state(state: AuthState) {
90    *slot().write().expect("auth state lock") = Some(Arc::new(state));
91}
92
93/// Returns the global auth state, panicking if [`init`] was never called.
94pub fn global() -> Arc<AuthState> {
95    try_global().expect("auth not initialised; call doido_auth::init() at boot")
96}
97
98/// Returns the global auth state when installed.
99pub fn try_global() -> Option<Arc<AuthState>> {
100    slot().read().expect("auth state lock").clone()
101}
102
103/// Clear auth state (tests only).
104pub fn reset_state() {
105    *slot().write().expect("auth state lock") = None;
106}