1use super::{EmailOtpPlugin, EmailOtpType};
2use crate::authentication_helpers::{JsonField, JsonFieldKind};
3use crate::endpoint::{definition, validate_fields};
4use alibi_core::endpoint::{
5 EndpointCall, EndpointDefinition, EndpointInput, EndpointResponse, ServerEndpoint,
6};
7use alibi_core::utils::json::JsValue;
8use alibi_core::{AuthContext, AuthResult, AuthSchema, HttpMethod};
9use serde::Deserialize;
10
11#[derive(Debug, Deserialize)]
12pub struct EmailOtpRead {
13 pub otp: Option<String>,
14}
15
16#[derive(Debug, Deserialize)]
18pub struct EmailOtpSent {
19 pub success: bool,
20}
21
22#[derive(Debug, Deserialize)]
24pub struct EmailOtpCheck {
25 pub success: bool,
26}
27
28#[derive(Debug, Deserialize)]
30pub struct EmailOtpVerification {
31 pub status: bool,
32 pub token: Option<String>,
33 pub user: Box<alibi_core::UserView>,
34}
35
36#[derive(Debug, Deserialize, serde::Serialize)]
38pub struct EmailOtpSignInInput {
39 pub email: String,
40 pub otp: String,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub name: Option<String>,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub image: Option<String>,
45 #[serde(flatten)]
46 pub additional_fields: serde_json::Map<String, serde_json::Value>,
47}
48
49#[derive(Debug, Deserialize)]
51pub struct EmailOtpSignInResult {
52 pub token: String,
53 pub user: Box<alibi_core::UserView>,
54}
55
56#[derive(Debug, Deserialize)]
58pub struct EmailOtpResetRequested {
59 pub success: bool,
60}
61
62#[derive(Debug, Deserialize, serde::Serialize)]
64pub struct EmailOtpPasswordResetInput {
65 pub email: String,
66 pub otp: String,
67 pub password: String,
68}
69
70#[derive(Debug, Deserialize)]
72pub struct EmailOtpPasswordReset {
73 pub success: bool,
74}
75
76#[derive(Debug, Deserialize, serde::Serialize)]
78#[serde(rename_all = "camelCase")]
79pub struct EmailOtpChangeRequestInput {
80 pub new_email: String,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub otp: Option<String>,
83}
84
85#[derive(Debug, Deserialize)]
87pub struct EmailOtpChangeRequested {
88 pub success: bool,
89}
90
91#[derive(Debug, Deserialize, serde::Serialize)]
93#[serde(rename_all = "camelCase")]
94pub struct EmailOtpChangeInput {
95 pub new_email: String,
96 pub otp: String,
97}
98
99#[derive(Debug, Deserialize)]
101pub struct EmailOtpChanged {
102 pub success: bool,
103}
104
105const FIELDS: &[JsonField] = &[
106 JsonField::string("email", true),
107 JsonField {
108 name: "type",
109 kind: JsonFieldKind::OneOf(&[
110 "email-verification",
111 "sign-in",
112 "forget-password",
113 "change-email",
114 ]),
115 required: true,
116 },
117];
118
119pub(super) fn definitions() -> Vec<EndpointDefinition> {
120 vec![
121 definition(
122 "changeEmailEmailOTP",
123 "changeEmailWithEmailOTP",
124 Some("/email-otp/change-email"),
125 HttpMethod::Post,
126 ),
127 definition(
128 "requestEmailChangeEmailOTP",
129 "requestEmailChangeWithEmailOTP",
130 Some("/email-otp/request-email-change"),
131 HttpMethod::Post,
132 ),
133 definition(
134 "resetPasswordEmailOTP",
135 "resetPasswordWithEmailOTP",
136 Some("/email-otp/reset-password"),
137 HttpMethod::Post,
138 ),
139 definition(
140 "requestPasswordResetEmailOTP",
141 "requestPasswordResetWithEmailOTP",
142 Some("/email-otp/request-password-reset"),
143 HttpMethod::Post,
144 ),
145 definition(
146 "forgetPasswordEmailOTP",
147 "forgetPasswordWithEmailOTP",
148 Some("/forget-password/email-otp"),
149 HttpMethod::Post,
150 ),
151 definition(
152 "signInEmailOTP",
153 "signInWithEmailOTP",
154 Some("/sign-in/email-otp"),
155 HttpMethod::Post,
156 ),
157 definition(
158 "verifyEmailOTP",
159 "verifyEmailOTP",
160 Some("/email-otp/verify-email"),
161 HttpMethod::Post,
162 ),
163 definition(
164 "checkVerificationOTP",
165 "verifyEmailWithOTP",
166 Some("/email-otp/check-verification-otp"),
167 HttpMethod::Post,
168 ),
169 definition(
170 "sendVerificationOTP",
171 "sendEmailVerificationOTP",
172 Some("/email-otp/send-verification-otp"),
173 HttpMethod::Post,
174 ),
175 definition(
176 "createVerificationOTP",
177 "createEmailVerificationOTP",
178 None,
179 HttpMethod::Post,
180 ),
181 definition(
182 "getVerificationOTP",
183 "getEmailVerificationOTP",
184 None,
185 HttpMethod::Get,
186 ),
187 ]
188}
189
190pub(super) fn validate(call: &EndpointCall) -> AuthResult<EndpointInput> {
191 if call.operation_id() == "changeEmailWithEmailOTP" {
192 return Ok(EndpointInput {
193 body: Some(validate_fields(
194 call.body(),
195 "body",
196 <super::types::ConfirmChangeRequest as crate::authentication_helpers::RequestBody>::FIELDS,
197 )?),
198 query: call.query().cloned(),
199 });
200 }
201 if call.operation_id() == "requestEmailChangeWithEmailOTP" {
202 return Ok(EndpointInput {
203 body:Some(validate_fields(call.body(),"body",<super::types::ChangeEmailRequest as crate::authentication_helpers::RequestBody>::FIELDS)?),
204 query:call.query().cloned(),
205 });
206 }
207 if call.operation_id() == "resetPasswordWithEmailOTP" {
208 return Ok(EndpointInput {
209 body:Some(validate_fields(call.body(),"body",<super::types::PasswordRequest as crate::authentication_helpers::RequestBody>::FIELDS)?),
210 query:call.query().cloned(),
211 });
212 }
213 if matches!(
214 call.operation_id(),
215 "requestPasswordResetWithEmailOTP" | "forgetPasswordWithEmailOTP"
216 ) {
217 return Ok(EndpointInput {
218 body: Some(validate_fields(
219 call.body(),
220 "body",
221 <super::types::EmailRequest as crate::authentication_helpers::RequestBody>::FIELDS,
222 )?),
223 query: call.query().cloned(),
224 });
225 }
226 if call.operation_id() == "signInWithEmailOTP" {
227 let _ = validate_fields(
228 call.body(),
229 "body",
230 <super::types::SignInRequest as crate::authentication_helpers::RequestBody>::FIELDS,
231 )?;
232 return Ok(EndpointInput {
233 body: call.body().cloned(),
234 query: call.query().cloned(),
235 });
236 }
237 if call.operation_id() == "verifyEmailOTP" {
238 return Ok(EndpointInput {
239 body: Some(validate_fields(
240 call.body(),
241 "body",
242 <super::types::VerifyRequest as crate::authentication_helpers::RequestBody>::FIELDS,
243 )?),
244 query: call.query().cloned(),
245 });
246 }
247 if call.operation_id() == "verifyEmailWithOTP" {
248 return Ok(EndpointInput {
249 body: Some(validate_fields(
250 call.body(),
251 "body",
252 <super::types::CheckRequest as crate::authentication_helpers::RequestBody>::FIELDS,
253 )?),
254 query: call.query().cloned(),
255 });
256 }
257 if call.operation_id() == "getEmailVerificationOTP" {
258 Ok(EndpointInput {
259 body: call.body().cloned(),
260 query: Some(validate_fields(call.query(), "query", FIELDS)?),
261 })
262 } else {
263 Ok(EndpointInput {
264 body: Some(validate_fields(call.body(), "body", FIELDS)?),
265 query: call.query().cloned(),
266 })
267 }
268}
269
270impl EmailOtpPlugin {
271 pub fn change_email_email_otp_endpoint(
276 input: &EmailOtpChangeInput,
277 ) -> AuthResult<ServerEndpoint<EmailOtpChanged>> {
278 ServerEndpoint::new("email-otp", "changeEmailEmailOTP").with_body(input)
279 }
280
281 pub fn request_email_change_email_otp_endpoint(
286 input: &EmailOtpChangeRequestInput,
287 ) -> AuthResult<ServerEndpoint<EmailOtpChangeRequested>> {
288 ServerEndpoint::new("email-otp", "requestEmailChangeEmailOTP").with_body(input)
289 }
290
291 pub fn reset_password_email_otp_endpoint(
296 input: &EmailOtpPasswordResetInput,
297 ) -> AuthResult<ServerEndpoint<EmailOtpPasswordReset>> {
298 ServerEndpoint::new("email-otp", "resetPasswordEmailOTP").with_body(input)
299 }
300
301 #[must_use]
303 pub fn request_password_reset_email_otp_endpoint(
304 email: impl Into<String>,
305 ) -> ServerEndpoint<EmailOtpResetRequested> {
306 reset_request("requestPasswordResetEmailOTP", email.into())
307 }
308 #[must_use]
310 pub fn forget_password_email_otp_endpoint(
311 email: impl Into<String>,
312 ) -> ServerEndpoint<EmailOtpResetRequested> {
313 reset_request("forgetPasswordEmailOTP", email.into())
314 }
315
316 pub fn sign_in_email_otp_endpoint(
321 input: &EmailOtpSignInInput,
322 ) -> AuthResult<ServerEndpoint<EmailOtpSignInResult>> {
323 ServerEndpoint::new("email-otp", "signInEmailOTP").with_body(input)
324 }
325
326 #[must_use]
328 pub fn verify_email_otp_endpoint(
329 email: impl Into<String>,
330 otp: impl Into<String>,
331 ) -> ServerEndpoint<EmailOtpVerification> {
332 ServerEndpoint::new("email-otp", "verifyEmailOTP").with_body_value(JsValue::Object(
333 [
334 ("email".into(), JsValue::String(email.into())),
335 ("otp".into(), JsValue::String(otp.into())),
336 ]
337 .into_iter()
338 .collect(),
339 ))
340 }
341
342 #[must_use]
344 pub fn check_verification_otp_endpoint(
345 email: impl Into<String>,
346 otp_type: EmailOtpType,
347 otp: impl Into<String>,
348 ) -> ServerEndpoint<EmailOtpCheck> {
349 let mut body = input(email.into(), otp_type);
350 if let JsValue::Object(fields) = &mut body {
351 let _ = fields.insert("otp".into(), JsValue::String(otp.into()));
352 }
353 ServerEndpoint::new("email-otp", "checkVerificationOTP").with_body_value(body)
354 }
355
356 #[must_use]
358 pub fn send_verification_otp_endpoint(
359 email: impl Into<String>,
360 otp_type: EmailOtpType,
361 ) -> ServerEndpoint<EmailOtpSent> {
362 ServerEndpoint::new("email-otp", "sendVerificationOTP")
363 .with_body_value(input(email.into(), otp_type))
364 }
365
366 #[must_use]
368 pub fn create_verification_otp_endpoint(
369 email: impl Into<String>,
370 otp_type: EmailOtpType,
371 ) -> ServerEndpoint<String> {
372 ServerEndpoint::new("email-otp", "createVerificationOTP")
373 .with_body_value(input(email.into(), otp_type))
374 }
375
376 #[must_use]
378 pub fn get_verification_otp_endpoint(
379 email: impl Into<String>,
380 otp_type: EmailOtpType,
381 ) -> ServerEndpoint<EmailOtpRead> {
382 ServerEndpoint::new("email-otp", "getVerificationOTP")
383 .with_query_value(input(email.into(), otp_type))
384 }
385
386 pub(super) async fn call_endpoint(
387 &self,
388 call: &EndpointCall,
389 ctx: &AuthContext<impl AuthSchema>,
390 ) -> AuthResult<EndpointResponse> {
391 if call.operation_id() == "verifyEmailWithOTP" {
392 let output = self.check_verification_core(ctx, call.body_as()?).await?;
393 return EndpointResponse::json(&alibi_core::utils::json::from_slice::<JsValue>(
394 &output.body,
395 )?);
396 }
397 if matches!(
398 call.operation_id(),
399 "sendEmailVerificationOTP"
400 | "verifyEmailOTP"
401 | "signInWithEmailOTP"
402 | "requestPasswordResetWithEmailOTP"
403 | "forgetPasswordWithEmailOTP"
404 | "resetPasswordWithEmailOTP"
405 | "requestEmailChangeWithEmailOTP"
406 | "changeEmailWithEmailOTP"
407 ) {
408 if call.operation_id() == "sendEmailVerificationOTP"
409 && let Some(request) = call.request()
410 {
411 alibi_core::middleware::CsrfMiddleware::new(
412 alibi_core::middleware::CsrfConfig::new(),
413 ctx.config.clone(),
414 )
415 .check_login_origin(request)?;
416 }
417 let output = match call.operation_id() {
418 "sendEmailVerificationOTP" => {
419 self.send_verification_core(call.request(), ctx, call.body_as()?)
420 .await?
421 }
422 "signInWithEmailOTP" => {
423 let raw = call.body().cloned().unwrap_or(JsValue::Null);
424 let mut parsed = raw.clone();
425 if let JsValue::Object(fields) = &mut parsed {
426 let _ = fields.shift_remove("username");
427 let _ = fields.shift_remove("displayUsername");
428 }
429 let body = alibi_core::utils::json::from_value(parsed)?;
430 self.sign_in_core(call, ctx, body, raw).await?
431 }
432 "requestPasswordResetWithEmailOTP" | "forgetPasswordWithEmailOTP" => {
433 self.request_password_reset_core(call.request(), ctx, call.body_as()?)
434 .await?
435 }
436 "resetPasswordWithEmailOTP" => {
437 self.reset_password_core(ctx, call.body_as()?).await?
438 }
439 "changeEmailWithEmailOTP" => {
440 self.change_email_core(call, ctx, call.body_as()?).await?
441 }
442 "requestEmailChangeWithEmailOTP" => {
443 self.request_email_change_core(call, ctx, call.body_as()?)
444 .await?
445 }
446 _ => self.verify_email_core(call, ctx, call.body_as()?).await?,
447 };
448 let mut response = if output.status >= 400 {
449 EndpointResponse::error(crate::endpoint::error_response(output.clone()))
450 .without_error_body()
451 } else {
452 EndpointResponse::value(alibi_core::utils::json::from_slice(&output.body)?)
453 };
454 response.set_status(Some(output.status));
455 response.merge_headers(output.headers);
456 return Ok(response);
457 }
458 let (email, otp_type) = if call.operation_id() == "createEmailVerificationOTP" {
459 let body: super::types::SendRequest = call.body_as()?;
460 (body.email, body.otp_type)
461 } else {
462 let query: super::types::SendRequest = call.query_as()?;
463 (query.email, query.otp_type)
464 };
465 if call.operation_id() == "createEmailVerificationOTP" {
466 EndpointResponse::json(&self.create_verification_otp(ctx, &email, otp_type).await?)
467 } else {
468 EndpointResponse::json(
469 &serde_json::json!({"otp": self.get_verification_otp(ctx, &email, otp_type).await?}),
470 )
471 }
472 }
473}
474
475fn input(email: String, otp_type: EmailOtpType) -> JsValue {
476 JsValue::Object(
477 [
478 ("email".into(), JsValue::String(email)),
479 ("type".into(), JsValue::String(otp_type.as_str().into())),
480 ]
481 .into_iter()
482 .collect(),
483 )
484}
485
486fn reset_request(name: &'static str, email: String) -> ServerEndpoint<EmailOtpResetRequested> {
487 ServerEndpoint::new("email-otp", name).with_body_value(JsValue::Object(
488 [("email".into(), JsValue::String(email))]
489 .into_iter()
490 .collect(),
491 ))
492}