1use rustauth_core::error::RustAuthError;
2use rustauth_core::plugin::{AuthPlugin, PluginMigration};
3#[cfg(feature = "oauth-provider")]
4use rustauth_oauth_provider::{oauth_provider, OAuthProviderOptions};
5#[cfg(feature = "passkey")]
6use rustauth_passkey::{passkey, PasskeyOptions};
7#[cfg(feature = "plugins")]
8use rustauth_plugins::official_schema_plugin;
9#[cfg(feature = "scim")]
10use rustauth_scim::{scim, ScimOptions};
11#[cfg(feature = "sso")]
12use rustauth_sso::{sso, SsoOptions};
13#[cfg(feature = "stripe")]
14use rustauth_stripe::{stripe, OrganizationStripeOptions, StripeOptions, SubscriptionOptions};
15use serde::Serialize;
16
17const CLI_SCHEMA_EXTENSION_PLUGIN_IDS: &[&str] =
18 &["oauth-provider", "passkey", "scim", "sso", "stripe"];
19
20#[cfg(not(feature = "plugins"))]
22const STATIC_OFFICIAL_PLUGIN_IDS: &[&str] = &[
23 "access",
24 "additional-fields",
25 "admin",
26 "anonymous",
27 "api-key",
28 "bearer",
29 "captcha",
30 "custom-session",
31 "device-authorization",
32 "email-otp",
33 "generic-oauth",
34 "have-i-been-pwned",
35 "jwt",
36 "last-login-method",
37 "magic-link",
38 "multi-session",
39 "oauth-proxy",
40 "one-tap",
41 "one-time-token",
42 "open-api",
43 "organization",
44 "phone-number",
45 "siwe",
46 "two-factor",
47 "username",
48];
49
50const STATIC_OFFICIAL_SCHEMA_PLUGIN_IDS: &[&str] = &[
52 "admin",
53 "anonymous",
54 "api-key",
55 "device-authorization",
56 "jwt",
57 "last-login-method",
58 "organization",
59 "phone-number",
60 "siwe",
61 "two-factor",
62 "username",
63];
64
65#[derive(Debug, Clone, Serialize)]
66pub struct PluginInfo {
67 pub id: &'static str,
68 pub official: bool,
69 pub schema_supported: bool,
70 pub snippet_supported: bool,
71 pub migration_impact: bool,
72}
73
74pub fn official_plugins() -> Vec<PluginInfo> {
75 official_plugin_ids()
76 .iter()
77 .map(|id| plugin_info(id))
78 .chain(
79 CLI_SCHEMA_EXTENSION_PLUGIN_IDS
80 .iter()
81 .map(|id| plugin_info(id)),
82 )
83 .collect()
84}
85
86fn official_plugin_ids() -> &'static [&'static str] {
87 #[cfg(feature = "plugins")]
88 {
89 rustauth_plugins::PLUGIN_IDS
90 }
91 #[cfg(not(feature = "plugins"))]
92 {
93 STATIC_OFFICIAL_PLUGIN_IDS
94 }
95}
96
97fn plugin_info(id: &'static str) -> PluginInfo {
98 let schema_supported = supports_schema_planning(id);
99 PluginInfo {
100 id,
101 official: true,
102 schema_supported,
103 snippet_supported: rust_snippet(id).is_some(),
104 migration_impact: schema_supported,
105 }
106}
107
108pub fn is_official_plugin(plugin: &str) -> bool {
109 official_plugin_ids().contains(&plugin) || CLI_SCHEMA_EXTENSION_PLUGIN_IDS.contains(&plugin)
110}
111
112pub fn schema_plugin(plugin: &str) -> Option<AuthPlugin> {
113 cli_schema_plugin(plugin).and_then(|result| result.ok())
114}
115
116pub fn supports_schema_planning(plugin: &str) -> bool {
118 matches!(cli_schema_plugin(plugin), Some(Ok(_)))
119}
120
121pub(crate) fn is_schema_planning_exception(plugin: &str) -> bool {
123 #[cfg(feature = "plugins")]
124 {
125 use rustauth_plugins::{APP_CONFIGURED_SCHEMA_PLUGIN_IDS, NO_FIXED_SCHEMA_PLUGIN_IDS};
126 NO_FIXED_SCHEMA_PLUGIN_IDS.contains(&plugin)
127 || APP_CONFIGURED_SCHEMA_PLUGIN_IDS.contains(&plugin)
128 }
129 #[cfg(not(feature = "plugins"))]
130 {
131 const NO_FIXED_SCHEMA_PLUGIN_IDS: &[&str] = &[
132 "bearer",
133 "captcha",
134 "custom-session",
135 "email-otp",
136 "generic-oauth",
137 "have-i-been-pwned",
138 "magic-link",
139 "multi-session",
140 "oauth-proxy",
141 "one-tap",
142 "one-time-token",
143 "open-api",
144 ];
145 const APP_CONFIGURED_SCHEMA_PLUGIN_IDS: &[&str] = &["additional-fields"];
146 NO_FIXED_SCHEMA_PLUGIN_IDS.contains(&plugin)
147 || APP_CONFIGURED_SCHEMA_PLUGIN_IDS.contains(&plugin)
148 }
149}
150
151fn cli_schema_plugin(plugin: &str) -> Option<Result<AuthPlugin, RustAuthError>> {
152 if let Err(error) = ensure_plugin_feature_enabled(plugin) {
153 return Some(Err(error));
154 }
155
156 #[cfg(feature = "plugins")]
157 if let Some(plugin) = official_schema_plugin(plugin) {
158 return Some(plugin);
159 }
160
161 match plugin {
162 #[cfg(feature = "oauth-provider")]
163 "oauth-provider" => Some(
164 oauth_provider(OAuthProviderOptions {
165 login_page: "/login".to_owned(),
166 consent_page: "/consent".to_owned(),
167 disable_jwt_plugin: true,
168 ..OAuthProviderOptions::default()
169 })
170 .map_err(|error| RustAuthError::InvalidConfig(error.to_string())),
171 ),
172 #[cfg(feature = "passkey")]
173 "passkey" => Some(Ok(passkey(PasskeyOptions::default()))),
174 #[cfg(feature = "scim")]
175 "scim" => Some(Ok(scim(ScimOptions::default()))),
176 #[cfg(feature = "sso")]
177 "sso" => Some(Ok(sso(SsoOptions::default()))),
178 #[cfg(feature = "stripe")]
179 "stripe" => Some(
180 stripe(
181 StripeOptions::dev()
182 .subscription(SubscriptionOptions::enabled(Vec::new()))
183 .organization(OrganizationStripeOptions::enabled()),
184 )
185 .map_err(|error| RustAuthError::InvalidConfig(error.to_string())),
186 ),
187 _ => None,
188 }
189}
190
191fn ensure_plugin_feature_enabled(plugin: &str) -> Result<(), RustAuthError> {
192 let Some(feature) = required_cargo_feature(plugin) else {
193 return Ok(());
194 };
195 if is_cargo_feature_enabled(feature) {
196 Ok(())
197 } else {
198 Err(RustAuthError::FeatureDisabled { feature })
199 }
200}
201
202pub(crate) fn required_cargo_feature(plugin: &str) -> Option<&'static str> {
203 if STATIC_OFFICIAL_SCHEMA_PLUGIN_IDS.contains(&plugin) {
204 return Some("plugins");
205 }
206 match plugin {
207 "oauth-provider" => Some("oauth-provider"),
208 "passkey" => Some("passkey"),
209 "scim" => Some("scim"),
210 "sso" => Some("sso"),
211 "stripe" => Some("stripe"),
212 _ => None,
213 }
214}
215
216#[allow(clippy::match_like_matches_macro)]
217pub(crate) fn is_cargo_feature_enabled(feature: &str) -> bool {
218 match feature {
219 "plugins" => cfg!(feature = "plugins"),
220 "oauth-provider" => cfg!(feature = "oauth-provider"),
221 "passkey" => cfg!(feature = "passkey"),
222 "scim" => cfg!(feature = "scim"),
223 "sso" => cfg!(feature = "sso"),
224 "stripe" => cfg!(feature = "stripe"),
225 _ => false,
226 }
227}
228
229pub fn schema_context_for_config(
230 plugin_ids: &[String],
231) -> Result<rustauth_core::context::AuthContext, RustAuthError> {
232 use std::sync::Arc;
233
234 use rustauth_core::context::create_auth_context_with_adapter;
235 use rustauth_core::db::MemoryAdapter;
236 use rustauth_core::options::RustAuthOptions;
237
238 let mut plugins = Vec::new();
239 for plugin_id in plugin_ids {
240 let Some(plugin) = cli_schema_plugin(plugin_id) else {
241 continue;
242 };
243 plugins.push(plugin?);
244 }
245 create_auth_context_with_adapter(
246 RustAuthOptions::new()
247 .development(true)
248 .secret("rustauth-cli-schema-planning-secret-32ch")
249 .plugins(plugins),
250 Arc::new(MemoryAdapter::new()),
251 )
252}
253
254pub fn plugin_migrations_for_config(
255 plugin_ids: &[String],
256) -> Result<Vec<PluginMigration>, RustAuthError> {
257 Ok(schema_context_for_config(plugin_ids)?.plugin_migrations)
258}
259
260pub fn rust_snippet(plugin: &str) -> Option<&'static str> {
261 match plugin {
262 "two-factor" => Some("rustauth::plugins::two_factor::two_factor(TwoFactorOptions::default())"),
263 "organization" => Some("rustauth::plugins::organization::organization(OrganizationOptions::default())"),
264 "username" => Some("rustauth::plugins::username::username(UsernameOptions::default())"),
265 "admin" => Some("rustauth::plugins::admin::admin(AdminOptions::default())?"),
266 "api-key" => Some("rustauth::plugins::api_key::api_key(ApiKeyOptions::default())?"),
267 "device-authorization" => {
268 Some("rustauth::plugins::device_authorization::device_authorization(DeviceAuthorizationOptions::default())?")
269 }
270 "anonymous" => Some("rustauth::plugins::anonymous::anonymous(AnonymousOptions::default())"),
271 "jwt" => Some("rustauth::plugins::jwt::jwt(JwtOptions::default())?"),
272 _ => None,
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 #[test]
281 fn official_plugins_lists_static_ids_without_plugins_feature() {
282 let plugins = official_plugins();
283 assert!(plugins.iter().any(|plugin| plugin.id == "admin"));
284 assert!(plugins.iter().any(|plugin| plugin.id == "passkey"));
285 assert!(plugins.iter().any(|plugin| plugin.id == "username"));
286 }
287
288 #[cfg(feature = "plugins")]
289 #[test]
290 fn official_schema_registry_covers_extension_plugins() {
291 for id in CLI_SCHEMA_EXTENSION_PLUGIN_IDS {
292 assert!(
293 cli_schema_plugin(id).is_some(),
294 "missing schema plugin for {id}"
295 );
296 }
297 }
298
299 #[cfg(feature = "plugins")]
300 #[test]
301 fn official_schema_plugin_ids_match_registry() {
302 use rustauth_plugins::{is_official_schema_plugin, official_schema_plugin_ids};
303
304 for id in official_schema_plugin_ids() {
305 assert!(is_official_schema_plugin(id));
306 }
307 }
308
309 #[cfg(not(feature = "passkey"))]
310 #[test]
311 fn passkey_schema_requires_passkey_feature() {
312 assert!(matches!(
313 cli_schema_plugin("passkey"),
314 Some(Err(RustAuthError::FeatureDisabled { feature: "passkey" }))
315 ));
316 assert!(!supports_schema_planning("passkey"));
317 }
318}