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