Skip to main content

rustauth_plugins/
schema_plugins.rs

1//! Default official plugin instances for CLI schema and migration planning.
2//!
3//! These builders use development-friendly defaults so `rustauth db` can derive the
4//! same database shape as a typical integration without custom app wiring.
5
6use rustauth_core::error::RustAuthError;
7use rustauth_core::plugin::AuthPlugin;
8
9use crate::{
10    admin::{admin, AdminOptions},
11    anonymous::{anonymous, AnonymousOptions},
12    api_key::{api_key, ApiKeyOptions},
13    device_authorization::{device_authorization, DeviceAuthorizationOptions},
14    jwt::{jwt, JwtOptions},
15    last_login_method::{last_login_method, LastLoginMethodOptions},
16    organization::{organization, DynamicAccessControlOptions, OrganizationOptions, TeamOptions},
17    phone_number::{phone_number, PhoneNumberOptions},
18    siwe::siwe_dev,
19    two_factor::{two_factor, TwoFactorOptions},
20    username::{username, UsernameOptions},
21    PLUGIN_IDS,
22};
23
24/// Plugin ids that may be enabled in an app but never contribute fixed database DDL.
25///
26/// CLI schema planning skips these intentionally — they rely on core tables, in-memory
27/// state, or app-specific wiring only.
28pub const NO_FIXED_SCHEMA_PLUGIN_IDS: &[&str] = &[
29    "bearer",
30    "captcha",
31    "custom-session",
32    "email-otp",
33    "generic-oauth",
34    "have-i-been-pwned",
35    "magic-link",
36    "multi-session",
37    "oauth-proxy",
38    "one-tap",
39    "one-time-token",
40    "open-api",
41];
42
43/// Plugin ids whose database shape depends on per-app field configuration.
44///
45/// [`official_schema_plugin`] cannot infer columns from the id alone; integrators must
46/// align migrations with their `additional_fields(...)` options.
47pub const APP_CONFIGURED_SCHEMA_PLUGIN_IDS: &[&str] = &["additional-fields"];
48
49fn schema_planning_organization_options() -> OrganizationOptions {
50    OrganizationOptions::builder()
51        .teams(TeamOptions {
52            enabled: true,
53            ..TeamOptions::default()
54        })
55        .dynamic_access_control(DynamicAccessControlOptions {
56            enabled: true,
57            ..DynamicAccessControlOptions::default()
58        })
59        .build()
60}
61
62/// Returns true when `plugin_id` is recognized by [`official_schema_plugin`].
63pub fn is_official_schema_plugin(plugin_id: &str) -> bool {
64    official_schema_plugin(plugin_id).is_some()
65}
66
67/// Build a schema-planning plugin instance with official defaults, when the plugin contributes database schema.
68pub fn official_schema_plugin(plugin_id: &str) -> Option<Result<AuthPlugin, RustAuthError>> {
69    match plugin_id {
70        "admin" => Some(admin(AdminOptions::default())),
71        "anonymous" => Some(Ok(anonymous(AnonymousOptions::default()))),
72        "api-key" => Some(api_key(ApiKeyOptions::default())),
73        "device-authorization" => Some(device_authorization(DeviceAuthorizationOptions::default())),
74        "jwt" => Some(jwt(JwtOptions::default())),
75        "last-login-method" => Some(Ok(last_login_method(
76            LastLoginMethodOptions::default().store_in_database(true),
77        ))),
78        "organization" => Some(Ok(organization(schema_planning_organization_options()))),
79        "phone-number" => Some(phone_number(PhoneNumberOptions::new(|_phone, _otp| Ok(())))),
80        "siwe" => Some(siwe_dev()),
81        "two-factor" => Some(Ok(two_factor(TwoFactorOptions::default()))),
82        "username" => Some(Ok(username(UsernameOptions::default()))),
83        _ => None,
84    }
85}
86
87/// Instantiate official schema plugins for the ids configured in `rustauth.toml`.
88pub fn configured_official_schema_plugins(
89    plugin_ids: &[String],
90) -> Result<Vec<AuthPlugin>, RustAuthError> {
91    let mut plugins = Vec::new();
92    for plugin_id in plugin_ids {
93        let Some(plugin) = official_schema_plugin(plugin_id) else {
94            continue;
95        };
96        plugins.push(plugin?);
97    }
98    Ok(plugins)
99}
100
101/// All plugin ids that [`official_schema_plugin`] can materialize for schema planning.
102pub fn official_schema_plugin_ids() -> Vec<&'static str> {
103    PLUGIN_IDS
104        .iter()
105        .copied()
106        .filter(|id| official_schema_plugin(id).is_some())
107        .collect()
108}
109
110#[cfg(test)]
111mod tests {
112    #![allow(clippy::expect_used)]
113
114    use rustauth_core::context::create_auth_context_with_adapter;
115    use rustauth_core::db::MemoryAdapter;
116    use rustauth_core::options::RustAuthOptions;
117    use std::sync::Arc;
118
119    use super::*;
120
121    fn is_catalog_plugin_id(plugin_id: &str) -> bool {
122        PLUGIN_IDS.contains(&plugin_id) || plugin_id == "have-i-been-pwned"
123    }
124
125    #[test]
126    fn schema_exemption_lists_are_official_plugin_ids() {
127        for id in NO_FIXED_SCHEMA_PLUGIN_IDS {
128            assert!(
129                is_catalog_plugin_id(id),
130                "{id} is not an official plugin id"
131            );
132            assert!(
133                official_schema_plugin(id).is_none(),
134                "{id} should not have a fixed schema factory"
135            );
136        }
137        for id in APP_CONFIGURED_SCHEMA_PLUGIN_IDS {
138            assert!(
139                is_catalog_plugin_id(id),
140                "{id} is not an official plugin id"
141            );
142            assert!(
143                official_schema_plugin(id).is_none(),
144                "{id} schema depends on app configuration"
145            );
146        }
147    }
148
149    #[test]
150    fn organization_schema_planning_includes_optional_tables() {
151        let plugin = official_schema_plugin("organization")
152            .expect("organization contributes schema")
153            .expect("organization defaults");
154        let context = create_auth_context_with_adapter(
155            RustAuthOptions::new().plugins(vec![plugin]),
156            Arc::new(MemoryAdapter::new()),
157        )
158        .expect("auth context");
159        assert!(context.db_schema.table("team").is_some());
160        assert!(context.db_schema.table("team_member").is_some());
161        assert!(context.db_schema.table("organization_role").is_some());
162    }
163}