alibi_plugins/phone_number/
endpoint.rs1use super::{
3 PhoneNumberPlugin,
4 types::{ResetRequest, SendRequest, SignInRequest, VerifyRequest},
5};
6use crate::authentication_helpers::JsonField;
7use crate::endpoint::{definition, validate_fields};
8use alibi_core::endpoint::{
9 EndpointCall, EndpointDefinition, EndpointInput, EndpointResponse, ServerEndpoint,
10};
11use alibi_core::utils::json::JsValue;
12use alibi_core::{AuthContext, AuthResult, AuthSchema, HttpMethod};
13use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Deserialize, Serialize)]
17pub struct PhoneOtpConsumption {
18 pub status: bool,
19}
20
21#[derive(Deserialize)]
22#[serde(rename_all = "camelCase")]
23struct ConsumeInput {
24 phone_number: String,
25 code: String,
26}
27
28#[derive(Debug, Deserialize, Serialize)]
29pub struct PhoneOtpSent {
30 pub message: String,
31}
32
33#[derive(Debug, Deserialize, Serialize)]
35pub struct PhonePasswordResetRequested {
36 pub status: bool,
37}
38
39#[derive(Debug, Deserialize, Serialize)]
41pub struct PhonePasswordResetResult {
42 pub status: bool,
43}
44
45#[derive(Debug, Deserialize)]
47#[serde(untagged)]
48pub enum PhoneSignInResponse {
49 Session {
50 token: String,
51 user: Box<alibi_core::UserView>,
52 },
53 TwoFactor {
54 #[serde(rename = "twoFactorRedirect")]
55 two_factor_redirect: bool,
56 #[serde(rename = "twoFactorMethods")]
57 two_factor_methods: Vec<String>,
58 },
59}
60
61#[derive(Debug, Deserialize, Serialize)]
63#[serde(rename_all = "camelCase")]
64pub struct PhoneVerificationInput {
65 pub phone_number: String,
66 pub code: String,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub disable_session: Option<bool>,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub update_phone_number: Option<bool>,
71 #[serde(flatten)]
72 pub additional_fields: serde_json::Map<String, serde_json::Value>,
73}
74
75#[derive(Debug, Deserialize, Serialize)]
77pub struct PhoneVerificationResult {
78 pub status: bool,
79 pub token: Option<String>,
80 pub user: Box<alibi_core::UserView>,
81}
82
83pub(super) fn definitions() -> Vec<EndpointDefinition> {
84 vec![
85 definition(
86 "verifyPhoneNumber",
87 "verifyPhoneNumber",
88 Some("/phone-number/verify"),
89 HttpMethod::Post,
90 ),
91 definition(
92 "signInPhoneNumber",
93 "signInPhoneNumber",
94 Some("/sign-in/phone-number"),
95 HttpMethod::Post,
96 ),
97 definition(
98 "resetPasswordPhoneNumber",
99 "resetPasswordPhoneNumber",
100 Some("/phone-number/reset-password"),
101 HttpMethod::Post,
102 ),
103 definition(
104 "requestPasswordResetPhoneNumber",
105 "requestPasswordResetPhoneNumber",
106 Some("/phone-number/request-password-reset"),
107 HttpMethod::Post,
108 ),
109 definition(
110 "sendPhoneNumberOTP",
111 "sendPhoneNumberOTP",
112 Some("/phone-number/send-otp"),
113 HttpMethod::Post,
114 ),
115 definition(
116 "consumePhoneNumberOTP",
117 "consumePhoneNumberOTP",
118 None,
119 HttpMethod::Post,
120 ),
121 ]
122}
123
124const SEND_FIELDS: &[JsonField] = &[JsonField::string("phoneNumber", true)];
125const CONSUME_FIELDS: &[JsonField] = &[
126 JsonField::string("phoneNumber", true),
127 JsonField::string("code", true),
128];
129
130pub(super) fn validate(call: &EndpointCall) -> AuthResult<EndpointInput> {
131 if call.operation_id() == "verifyPhoneNumber" {
132 let _ = validate_fields(
133 call.body(),
134 "body",
135 <VerifyRequest as crate::authentication_helpers::RequestBody>::FIELDS,
136 )?;
137 return Ok(EndpointInput {
138 body: call.body().cloned(),
139 query: call.query().cloned(),
140 });
141 }
142 Ok(EndpointInput {
143 body: Some(validate_fields(
144 call.body(),
145 "body",
146 match call.operation_id() {
147 "consumePhoneNumberOTP" => CONSUME_FIELDS,
148 "signInPhoneNumber" => {
149 <SignInRequest as crate::authentication_helpers::RequestBody>::FIELDS
150 }
151 "resetPasswordPhoneNumber" => {
152 <ResetRequest as crate::authentication_helpers::RequestBody>::FIELDS
153 }
154 _ => SEND_FIELDS,
155 },
156 )?),
157 query: call.query().cloned(),
158 })
159}
160
161impl PhoneNumberPlugin {
162 pub fn verify_endpoint(
167 input: &PhoneVerificationInput,
168 ) -> AuthResult<ServerEndpoint<PhoneVerificationResult>> {
169 ServerEndpoint::new("phone-number", "verifyPhoneNumber").with_body(input)
170 }
171
172 pub fn sign_in_endpoint(
177 input: &SignInRequest,
178 ) -> AuthResult<ServerEndpoint<PhoneSignInResponse>> {
179 ServerEndpoint::new("phone-number", "signInPhoneNumber").with_body(input)
180 }
181
182 pub fn reset_password_endpoint(
187 input: &ResetRequest,
188 ) -> AuthResult<ServerEndpoint<PhonePasswordResetResult>> {
189 ServerEndpoint::new("phone-number", "resetPasswordPhoneNumber").with_body(input)
190 }
191
192 #[must_use]
194 pub fn request_password_reset_endpoint(
195 phone_number: impl Into<String>,
196 ) -> ServerEndpoint<PhonePasswordResetRequested> {
197 ServerEndpoint::new("phone-number", "requestPasswordResetPhoneNumber").with_body_value(
198 JsValue::Object(
199 [("phoneNumber".into(), JsValue::String(phone_number.into()))]
200 .into_iter()
201 .collect(),
202 ),
203 )
204 }
205
206 #[must_use]
208 pub fn send_otp_endpoint(phone_number: impl Into<String>) -> ServerEndpoint<PhoneOtpSent> {
209 ServerEndpoint::new("phone-number", "sendPhoneNumberOTP").with_body_value(JsValue::Object(
210 [("phoneNumber".into(), JsValue::String(phone_number.into()))]
211 .into_iter()
212 .collect(),
213 ))
214 }
215
216 #[must_use]
218 pub fn consume_otp_endpoint(
219 phone_number: impl Into<String>,
220 code: impl Into<String>,
221 ) -> ServerEndpoint<PhoneOtpConsumption> {
222 ServerEndpoint::new("phone-number", "consumePhoneNumberOTP").with_body_value(
223 JsValue::Object(
224 [
225 ("phoneNumber".into(), JsValue::String(phone_number.into())),
226 ("code".into(), JsValue::String(code.into())),
227 ]
228 .into_iter()
229 .collect(),
230 ),
231 )
232 }
233
234 pub(super) async fn call_endpoint(
235 &self,
236 call: &EndpointCall,
237 ctx: &AuthContext<impl AuthSchema>,
238 ) -> AuthResult<EndpointResponse> {
239 if call.operation_id() != "consumePhoneNumberOTP" {
240 let output = match call.operation_id() {
241 "verifyPhoneNumber" => self.verify_core(call, ctx, call.body_as()?).await?,
242 "signInPhoneNumber" => self.sign_in_core(call, ctx, call.body_as()?).await?,
243 "sendPhoneNumberOTP" => {
244 self.send_otp_core(ctx, call.request(), call.body_as::<SendRequest>()?)
245 .await?
246 }
247 "resetPasswordPhoneNumber" => {
248 self.reset_password_core(ctx, call.body_as()?).await?
249 }
250 _ => {
251 self.request_password_reset_core(
252 ctx,
253 call.request(),
254 call.body_as::<SendRequest>()?,
255 )
256 .await?
257 }
258 };
259 let mut response = if output.status >= 400 {
260 EndpointResponse::error(crate::endpoint::error_response(output.clone()))
261 .without_error_body()
262 } else {
263 EndpointResponse::value(alibi_core::utils::json::from_slice(&output.body)?)
264 };
265 response.set_status(Some(output.status));
266 response.merge_headers(output.headers);
267 return Ok(response);
268 }
269 let body: ConsumeInput = call.body_as()?;
270 self.verify_and_consume(ctx, call.request(), &body.phone_number, &body.code)
271 .await?;
272 EndpointResponse::json(&PhoneOtpConsumption { status: true })
273 }
274}