alibi_plugins/phone_number/
types.rs1use 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#[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#[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
127pub(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 Some(Value::Bool(value)) => Ok(Some(if *value { "1" } else { "0" }.into())),
166 Some(Value::Number(value)) => {
167 use alibi_core::store::NumericTextInput;
168 let number = *value;
172 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}