Skip to main content

alibi_api/plugins/email_password/
signup.rs

1//! Enumeration-safe duplicate responses and application callbacks.
2use super::{EmailPasswordConfig, SignUpRequest, SignUpResponse};
3use alibi_core::{AuthContext, AuthRequest, AuthResult, AuthSchema, wire::UserView};
4use serde_json::{Map, Value, json};
5use std::{future::Future, pin::Pin};
6
7/// Observes the existing physical user and the real signup request.
8/// Errors are logged while the generic duplicate response remains successful.
9pub type ExistingUserSignupCallback = dyn Fn(UserView, AuthRequest) -> Pin<Box<dyn Future<Output = AuthResult<()>> + Send>>
10    + Send
11    + Sync;
12
13/// Inputs to synthetic-user customization. These values are never persisted.
14#[derive(Clone, Debug)]
15pub struct SyntheticUserContext {
16    pub core_fields: Map<String, Value>,
17    pub additional_fields: Map<String, Value>,
18    pub id: String,
19}
20
21/// Customizes a duplicate response before the registered public schema filters it.
22/// Unknown and disabled-plugin fields are omitted; callback errors propagate.
23pub type CustomSyntheticUserCallback =
24    dyn Fn(SyntheticUserContext) -> AuthResult<Map<String, Value>> + Send + Sync;
25
26pub(super) async fn notify_existing(
27    user: UserView,
28    request: &AuthRequest,
29    config: &EmailPasswordConfig,
30    context: &AuthContext<impl AuthSchema>,
31) -> AuthResult<()> {
32    let Some(callback) = config.on_existing_user_signup.clone() else {
33        return Ok(());
34    };
35    let request = request.clone();
36    super::super::authentication_helpers::run_owned_notification(
37        context,
38        async move {
39            super::super::authentication_helpers::run_notification(callback(user, request)).await;
40            Ok(())
41        },
42        context.config.awaited_notification_errors,
43    )
44    .await
45}
46
47pub(super) fn synthetic_response(
48    body: &SignUpRequest,
49    config: &EmailPasswordConfig,
50    context: &AuthContext<impl AuthSchema>,
51) -> AuthResult<(SignUpResponse<Value>, Option<Vec<String>>)> {
52    let now = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
53    let core_fields = Map::from_iter([
54        ("name".into(), json!(body.name)),
55        ("email".into(), json!(body.email)),
56        ("emailVerified".into(), json!(false)),
57        ("image".into(), json!(body.image)),
58        ("createdAt".into(), json!(now)),
59        ("updatedAt".into(), json!(now)),
60    ]);
61    let input = SyntheticUserContext {
62        core_fields,
63        additional_fields: Map::new(),
64        id: uuid::Uuid::new_v4().simple().to_string(),
65    };
66    let mut candidate = if let Some(customize) = &config.custom_synthetic_user {
67        customize(input)?
68    } else {
69        let mut fields = input.core_fields;
70        drop(fields.insert("id".into(), json!(input.id)));
71        if config.enable_username {
72            drop(fields.insert("username".into(), json!(body.username)));
73            drop(fields.insert("displayUsername".into(), json!(body.display_username)));
74        }
75        fields
76    };
77    let mut result = Map::new();
78    for field in ["id", "name", "email"] {
79        if let Some(value) = candidate.remove(field) {
80            drop(result.insert(field.into(), value));
81        }
82    }
83    for (field, default) in [
84        ("emailVerified", json!(false)),
85        ("image", Value::Null),
86        ("createdAt", json!(now)),
87        ("updatedAt", json!(now)),
88    ] {
89        drop(result.insert(field.into(), candidate.remove(field).unwrap_or(default)));
90    }
91    let mut defaults = Vec::new();
92    let enabled = |key: &str| {
93        context
94            .get_metadata(key)
95            .and_then(Value::as_bool)
96            .unwrap_or(false)
97    };
98    if config.enable_username {
99        defaults.extend([("username", Value::Null), ("displayUsername", Value::Null)]);
100    }
101    if enabled("two_factor.enabled") {
102        defaults.push(("twoFactorEnabled", json!(false)));
103    }
104    if enabled("admin.enabled") {
105        let mut creation = alibi_core::CreateUser::new();
106        super::super::helpers::apply_default_role(context, &mut creation);
107        defaults.extend([
108            ("role", json!(creation.role)),
109            ("banned", json!(false)),
110            ("banReason", Value::Null),
111            ("banExpires", Value::Null),
112        ]);
113    }
114    if enabled("anonymous.enabled") {
115        defaults.push(("isAnonymous", json!(false)));
116    }
117    if enabled("phone-number.enabled") {
118        defaults.extend([
119            ("phoneNumber", Value::Null),
120            ("phoneNumberVerified", Value::Null),
121        ]);
122    }
123    if enabled("last-login-method.enabled") {
124        defaults.push(("lastLoginMethod", Value::Null));
125    }
126    for (field, default) in defaults {
127        drop(result.insert(field.into(), candidate.remove(field).unwrap_or(default)));
128    }
129    Ok((
130        SignUpResponse {
131            token: None,
132            user: Value::Object(result),
133        },
134        None,
135    ))
136}