Skip to main content

alibi_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 optional genuine signup request.
8/// Errors are logged while the generic duplicate response remains successful.
9pub type ExistingUserSignupCallback = dyn Fn(UserView, Option<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: Option<&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.cloned();
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    parsed_fields: &alibi_core::field_policy::FieldValues,
50    config: &EmailPasswordConfig,
51    context: &AuthContext<impl AuthSchema>,
52) -> AuthResult<(SignUpResponse<Value>, Option<Vec<String>>)> {
53    let now = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
54    let core_fields = Map::from_iter([
55        ("name".into(), json!(body.name)),
56        ("email".into(), json!(body.email)),
57        ("emailVerified".into(), json!(false)),
58        ("image".into(), json!(body.image)),
59        ("createdAt".into(), json!(now)),
60        ("updatedAt".into(), json!(now)),
61    ]);
62    let input = SyntheticUserContext {
63        core_fields,
64        additional_fields: parsed_fields
65            .iter()
66            .filter(|(name, _)| context.config.user.additional_fields.contains_key(*name))
67            .map(|(name, value)| Ok((name.clone(), serde_json::to_value(value)?)))
68            .collect::<Result<_, serde_json::Error>>()?,
69        id: context
70            .config
71            .advanced
72            .database
73            .generated_id("user")?
74            .unwrap_or_else(|| uuid::Uuid::new_v4().simple().to_string()),
75    };
76    let mut candidate = if let Some(customize) = &config.custom_synthetic_user {
77        customize(input)?
78    } else {
79        let mut fields = input.core_fields;
80        _ = fields.insert("id".into(), json!(input.id));
81        for (name, value) in parsed_fields {
82            _ = fields.insert(name.clone(), serde_json::to_value(value)?);
83        }
84        if config.enable_username {
85            _ = fields
86                .entry("username")
87                .or_insert_with(|| json!(body.username));
88            _ = fields
89                .entry("displayUsername")
90                .or_insert_with(|| json!(body.display_username));
91        }
92        fields
93    };
94    let mut result = Map::new();
95    for field in ["id", "name", "email"] {
96        if let Some(value) = candidate.remove(field) {
97            _ = result.insert(field.into(), value);
98        }
99    }
100    for (field, default) in [
101        ("emailVerified", json!(false)),
102        ("image", Value::Null),
103        ("createdAt", json!(now)),
104        ("updatedAt", json!(now)),
105    ] {
106        _ = result.insert(field.into(), candidate.remove(field).unwrap_or(default));
107    }
108    let mut defaults = Vec::new();
109    let enabled = |key: &str| {
110        context
111            .get_metadata(key)
112            .and_then(Value::as_bool)
113            .unwrap_or(false)
114    };
115    if config.enable_username {
116        defaults.extend([("username", Value::Null), ("displayUsername", Value::Null)]);
117    }
118    if enabled("two_factor.enabled") {
119        defaults.push(("twoFactorEnabled", json!(false)));
120    }
121    if enabled("admin.enabled") {
122        let mut creation = alibi_core::CreateUser::new();
123        super::super::helpers::apply_default_role(context, &mut creation);
124        defaults.extend([
125            ("role", json!(creation.role)),
126            ("banned", json!(false)),
127            ("banReason", Value::Null),
128            ("banExpires", Value::Null),
129        ]);
130    }
131    if enabled("anonymous.enabled") {
132        defaults.push(("isAnonymous", json!(false)));
133    }
134    if enabled("phone-number.enabled") {
135        defaults.extend([
136            ("phoneNumber", Value::Null),
137            ("phoneNumberVerified", Value::Null),
138        ]);
139    }
140    if enabled("last-login-method.enabled") {
141        defaults.push(("lastLoginMethod", Value::Null));
142    }
143    for (field, default) in defaults {
144        _ = result.insert(field.into(), candidate.remove(field).unwrap_or(default));
145    }
146    let registered = context
147        .extensions
148        .get::<alibi_core::field_policy::UserFields>();
149    let fields = registered
150        .as_ref()
151        .map_or(&context.config.user.additional_fields, |fields| &fields.0.0);
152    for (name, field) in fields {
153        if !field.returned {
154            _ = result.remove(name);
155        } else if let Some(value) = candidate.remove(name) {
156            _ = result.insert(name.clone(), value);
157        } else if !result.contains_key(name) {
158            if let Some(default) = &field.default {
159                _ = result.insert(name.clone(), serde_json::to_value(default.value())?);
160            } else if !field.required {
161                _ = result.insert(name.clone(), Value::Null);
162            }
163        }
164    }
165    Ok((
166        SignUpResponse {
167            token: None,
168            user: Value::Object(result),
169        },
170        None,
171    ))
172}