Skip to main content

alibi_plugins/phone_number/
types.rs

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