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