Skip to main content

alibi_plugins/phone_number/
types.rs

1use crate::authentication_helpers::{JsonField, JsonFieldKind, RequestBody};
2use alibi_core::{AuthError, wire::UserView};
3use serde::{Deserialize, Serialize};
4
5#[derive(Clone)]
6pub struct PhoneOtpDelivery {
7    pub phone_number: String,
8    pub code: String,
9}
10
11impl std::fmt::Debug for PhoneOtpDelivery {
12    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
13        f.debug_struct("PhoneOtpDelivery").finish_non_exhaustive()
14    }
15}
16
17#[derive(Clone)]
18pub struct PhoneNumberVerification {
19    pub phone_number: String,
20    pub user: UserView,
21}
22
23impl std::fmt::Debug for PhoneNumberVerification {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        f.debug_struct("PhoneNumberVerification")
26            .finish_non_exhaustive()
27    }
28}
29
30#[derive(Deserialize)]
31pub(super) struct SendRequest {
32    #[serde(rename = "phoneNumber")]
33    pub phone_number: String,
34}
35
36/// Credential input for the public phone sign-in operation.
37#[derive(Debug, Deserialize, Serialize)]
38pub struct SignInRequest {
39    #[serde(rename = "phoneNumber")]
40    pub phone_number: String,
41    pub password: String,
42    #[serde(rename = "rememberMe", skip_serializing_if = "Option::is_none")]
43    pub remember_me: Option<bool>,
44}
45
46#[derive(Deserialize)]
47pub(super) struct VerifyRequest {
48    #[serde(rename = "phoneNumber")]
49    pub phone_number: String,
50    pub code: String,
51    #[serde(rename = "disableSession")]
52    pub disable_session: Option<bool>,
53    #[serde(rename = "updatePhoneNumber")]
54    pub update_phone_number: Option<bool>,
55    pub username: Option<alibi_core::utils::json::JsValue>,
56    #[serde(rename = "displayUsername")]
57    pub display_username: Option<alibi_core::utils::json::JsValue>,
58    pub image: Option<String>,
59    #[serde(rename = "phoneNumberVerified")]
60    pub phone_number_verified: Option<alibi_core::utils::json::JsValue>,
61}
62
63/// Proof and replacement credential for the public phone password-reset operation.
64#[derive(Debug, Deserialize, Serialize)]
65pub struct ResetRequest {
66    pub otp: String,
67    #[serde(rename = "phoneNumber")]
68    pub phone_number: String,
69    #[serde(rename = "newPassword")]
70    pub new_password: String,
71}
72
73impl RequestBody for SendRequest {
74    const FIELDS: &'static [JsonField] = &[JsonField::string("phoneNumber", true)];
75}
76
77impl RequestBody for SignInRequest {
78    const FIELDS: &'static [JsonField] = &[
79        JsonField::string("phoneNumber", true),
80        JsonField::string("password", true),
81        JsonField {
82            name: "rememberMe",
83            kind: JsonFieldKind::Boolean,
84            required: false,
85        },
86    ];
87}
88
89impl RequestBody for VerifyRequest {
90    const FIELDS: &'static [JsonField] = &[
91        JsonField::string("phoneNumber", true),
92        JsonField::string("code", true),
93        JsonField {
94            name: "disableSession",
95            kind: JsonFieldKind::Boolean,
96            required: false,
97        },
98        JsonField {
99            name: "updatePhoneNumber",
100            kind: JsonFieldKind::Boolean,
101            required: false,
102        },
103    ];
104}
105
106impl RequestBody for ResetRequest {
107    const FIELDS: &'static [JsonField] = &[
108        JsonField::string("otp", true),
109        JsonField::string("phoneNumber", true),
110        JsonField::string("newPassword", true),
111    ];
112}
113
114pub(super) const fn phone_error(
115    status: u16,
116    code: &'static str,
117    message: &'static str,
118) -> AuthError {
119    AuthError::Upstream {
120        status,
121        code,
122        message,
123    }
124}
125
126// Pinned parseUserInput applies the registered schema to signup's additional
127// fields. The verified field is input:false, whose guard uses JS truthiness.
128pub(crate) fn reject_verified_input(
129    input: Option<&alibi_core::utils::json::JsValue>,
130) -> Result<(), AuthError> {
131    use alibi_core::utils::json::JsValue as Value;
132    let truthy = match input {
133        None | Some(Value::Null) => false,
134        Some(Value::Bool(value)) => *value,
135        Some(Value::Number(value)) => *value != 0.0 && !value.is_nan(),
136        Some(Value::String(value)) => !value.is_empty(),
137        Some(Value::Array(_) | Value::Object(_)) => true,
138    };
139    if truthy {
140        return Err(phone_error(
141            400,
142            "FIELD_NOT_ALLOWED",
143            "phoneNumberVerified is not allowed to be set",
144        ));
145    }
146    Ok(())
147}
148
149#[expect(
150    clippy::as_conversions,
151    clippy::cast_possible_truncation,
152    reason = "JavaScript-compatible numbers deliberately retain IEEE754 rounding and guarded integer coercion at the wire or adapter boundary"
153)]
154pub(crate) async fn parse_signup_phone(
155    ctx: &alibi_core::AuthContext<impl alibi_core::AuthSchema>,
156    input: Option<&alibi_core::utils::json::JsValue>,
157) -> Result<Option<String>, AuthError> {
158    use alibi_core::utils::json::JsValue as Value;
159    match input {
160        None | Some(Value::Null) => Ok(None),
161        Some(Value::String(value)) => Ok(Some(value.clone())),
162        // The pinned SQLite adapter coerces these scalar inputs to its TEXT
163        // column. Arrays and objects fail user insertion, leaving no rows.
164        Some(Value::Bool(value)) => Ok(Some(if *value { "1" } else { "0" }.into())),
165        Some(Value::Number(value)) => {
166            use alibi_core::store::NumericTextInput;
167            // A valid JSON number can overflow the JavaScript f64 range.
168            // Preserve its number type through validation, then apply the same
169            // rounding/overflow before the adapter chooses INTEGER or REAL.
170            let number = *value;
171            // Bun's SQLite binding uses JavaScriptCore isAnyInt(), whose
172            // signed Int52 range excludes negative zero. Other values bind REAL.
173            let input_2 = if (-2_251_799_813_685_248.0..2_251_799_813_685_248.0).contains(&number)
174                && number.fract() == 0.0
175                && !(number == 0.0 && number.is_sign_negative())
176            {
177                NumericTextInput::Integer(number as i64)
178            } else {
179                NumericTextInput::Real(number)
180            };
181            Ok(Some(ctx.database.coerce_user_text_number(input_2).await?))
182        }
183        Some(Value::Array(_) | Value::Object(_)) => Err(phone_error(
184            422,
185            "FAILED_TO_CREATE_USER",
186            "Failed to create user",
187        )),
188    }
189}