use super::{
PhoneNumberPlugin,
types::{ResetRequest, SendRequest, SignInRequest, VerifyRequest},
};
use crate::authentication_helpers::JsonField;
use crate::endpoint::{definition, validate_fields};
use alibi_core::endpoint::{
EndpointCall, EndpointDefinition, EndpointInput, EndpointResponse, ServerEndpoint,
};
use alibi_core::utils::json::JsValue;
use alibi_core::{AuthContext, AuthResult, AuthSchema, HttpMethod};
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
pub struct PhoneOtpConsumption {
pub status: bool,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ConsumeInput {
phone_number: String,
code: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct PhoneOtpSent {
pub message: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct PhonePasswordResetRequested {
pub status: bool,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct PhonePasswordResetResult {
pub status: bool,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum PhoneSignInResponse {
Session {
token: String,
user: Box<alibi_core::UserView>,
},
TwoFactor {
#[serde(rename = "twoFactorRedirect")]
two_factor_redirect: bool,
#[serde(rename = "twoFactorMethods")]
two_factor_methods: Vec<String>,
},
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PhoneVerificationInput {
pub phone_number: String,
pub code: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub disable_session: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub update_phone_number: Option<bool>,
#[serde(flatten)]
pub additional_fields: serde_json::Map<String, serde_json::Value>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct PhoneVerificationResult {
pub status: bool,
pub token: Option<String>,
pub user: Box<alibi_core::UserView>,
}
pub(super) fn definitions() -> Vec<EndpointDefinition> {
vec![
definition(
"verifyPhoneNumber",
"verifyPhoneNumber",
Some("/phone-number/verify"),
HttpMethod::Post,
),
definition(
"signInPhoneNumber",
"signInPhoneNumber",
Some("/sign-in/phone-number"),
HttpMethod::Post,
),
definition(
"resetPasswordPhoneNumber",
"resetPasswordPhoneNumber",
Some("/phone-number/reset-password"),
HttpMethod::Post,
),
definition(
"requestPasswordResetPhoneNumber",
"requestPasswordResetPhoneNumber",
Some("/phone-number/request-password-reset"),
HttpMethod::Post,
),
definition(
"sendPhoneNumberOTP",
"sendPhoneNumberOTP",
Some("/phone-number/send-otp"),
HttpMethod::Post,
),
definition(
"consumePhoneNumberOTP",
"consumePhoneNumberOTP",
None,
HttpMethod::Post,
),
]
}
const SEND_FIELDS: &[JsonField] = &[JsonField::string("phoneNumber", true)];
const CONSUME_FIELDS: &[JsonField] = &[
JsonField::string("phoneNumber", true),
JsonField::string("code", true),
];
pub(super) fn validate(call: &EndpointCall) -> AuthResult<EndpointInput> {
if call.operation_id() == "verifyPhoneNumber" {
let _ = validate_fields(
call.body(),
"body",
<VerifyRequest as crate::authentication_helpers::RequestBody>::FIELDS,
)?;
return Ok(EndpointInput {
body: call.body().cloned(),
query: call.query().cloned(),
});
}
Ok(EndpointInput {
body: Some(validate_fields(
call.body(),
"body",
match call.operation_id() {
"consumePhoneNumberOTP" => CONSUME_FIELDS,
"signInPhoneNumber" => {
<SignInRequest as crate::authentication_helpers::RequestBody>::FIELDS
}
"resetPasswordPhoneNumber" => {
<ResetRequest as crate::authentication_helpers::RequestBody>::FIELDS
}
_ => SEND_FIELDS,
},
)?),
query: call.query().cloned(),
})
}
impl PhoneNumberPlugin {
pub fn verify_endpoint(
input: &PhoneVerificationInput,
) -> AuthResult<ServerEndpoint<PhoneVerificationResult>> {
ServerEndpoint::new("phone-number", "verifyPhoneNumber").with_body(input)
}
pub fn sign_in_endpoint(
input: &SignInRequest,
) -> AuthResult<ServerEndpoint<PhoneSignInResponse>> {
ServerEndpoint::new("phone-number", "signInPhoneNumber").with_body(input)
}
pub fn reset_password_endpoint(
input: &ResetRequest,
) -> AuthResult<ServerEndpoint<PhonePasswordResetResult>> {
ServerEndpoint::new("phone-number", "resetPasswordPhoneNumber").with_body(input)
}
#[must_use]
pub fn request_password_reset_endpoint(
phone_number: impl Into<String>,
) -> ServerEndpoint<PhonePasswordResetRequested> {
ServerEndpoint::new("phone-number", "requestPasswordResetPhoneNumber").with_body_value(
JsValue::Object(
[("phoneNumber".into(), JsValue::String(phone_number.into()))]
.into_iter()
.collect(),
),
)
}
#[must_use]
pub fn send_otp_endpoint(phone_number: impl Into<String>) -> ServerEndpoint<PhoneOtpSent> {
ServerEndpoint::new("phone-number", "sendPhoneNumberOTP").with_body_value(JsValue::Object(
[("phoneNumber".into(), JsValue::String(phone_number.into()))]
.into_iter()
.collect(),
))
}
#[must_use]
pub fn consume_otp_endpoint(
phone_number: impl Into<String>,
code: impl Into<String>,
) -> ServerEndpoint<PhoneOtpConsumption> {
ServerEndpoint::new("phone-number", "consumePhoneNumberOTP").with_body_value(
JsValue::Object(
[
("phoneNumber".into(), JsValue::String(phone_number.into())),
("code".into(), JsValue::String(code.into())),
]
.into_iter()
.collect(),
),
)
}
pub(super) async fn call_endpoint(
&self,
call: &EndpointCall,
ctx: &AuthContext<impl AuthSchema>,
) -> AuthResult<EndpointResponse> {
if call.operation_id() != "consumePhoneNumberOTP" {
let output = match call.operation_id() {
"verifyPhoneNumber" => self.verify_core(call, ctx, call.body_as()?).await?,
"signInPhoneNumber" => self.sign_in_core(call, ctx, call.body_as()?).await?,
"sendPhoneNumberOTP" => {
self.send_otp_core(ctx, call.request(), call.body_as::<SendRequest>()?)
.await?
}
"resetPasswordPhoneNumber" => {
self.reset_password_core(ctx, call.body_as()?).await?
}
_ => {
self.request_password_reset_core(
ctx,
call.request(),
call.body_as::<SendRequest>()?,
)
.await?
}
};
let mut response = if output.status >= 400 {
EndpointResponse::error(crate::endpoint::error_response(output.clone()))
.without_error_body()
} else {
EndpointResponse::value(alibi_core::utils::json::from_slice(&output.body)?)
};
response.set_status(Some(output.status));
response.merge_headers(output.headers);
return Ok(response);
}
let body: ConsumeInput = call.body_as()?;
self.verify_and_consume(ctx, call.request(), &body.phone_number, &body.code)
.await?;
EndpointResponse::json(&PhoneOtpConsumption { status: true })
}
}