alibi_plugins/magic_link/
endpoint.rs1use super::{MagicLinkPlugin, MagicLinkSignInInput};
2use crate::authentication_helpers::{JsonField, RequestBody};
3use crate::endpoint::{definition, validate_fields, validation};
4use alibi_core::endpoint::{
5 EndpointCall, EndpointDefinition, EndpointInput, EndpointResponse, ServerEndpoint,
6};
7use alibi_core::{AuthContext, AuthResult, AuthSchema, HttpMethod};
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Deserialize)]
12pub struct MagicLinkSent {
13 pub status: bool,
14}
15
16#[derive(Debug, Deserialize, Serialize)]
18pub struct MagicLinkVerifyInput {
19 pub token: String,
20 #[serde(rename = "callbackURL", skip_serializing_if = "Option::is_none")]
21 pub callback_url: Option<String>,
22 #[serde(rename = "newUserCallbackURL", skip_serializing_if = "Option::is_none")]
23 pub new_user_callback_url: Option<String>,
24 #[serde(rename = "errorCallbackURL", skip_serializing_if = "Option::is_none")]
25 pub error_callback_url: Option<String>,
26}
27
28#[derive(Debug, Deserialize)]
30pub struct MagicLinkVerified {
31 pub token: String,
32 pub user: Box<alibi_core::UserView>,
33 pub session: Box<alibi_core::wire::SessionView>,
34}
35
36const VERIFY_FIELDS: &[JsonField] = &[
37 JsonField::string("token", true),
38 JsonField::string("callbackURL", false),
39 JsonField::string("errorCallbackURL", false),
40 JsonField::string("newUserCallbackURL", false),
41];
42
43pub(super) fn definitions() -> Vec<EndpointDefinition> {
44 vec![
45 definition(
46 "signInMagicLink",
47 "signInWithMagicLink",
48 Some("/sign-in/magic-link"),
49 HttpMethod::Post,
50 ),
51 definition(
52 "magicLinkVerify",
53 "verifyMagicLink",
54 Some("/magic-link/verify"),
55 HttpMethod::Get,
56 ),
57 ]
58}
59
60pub(super) fn validate(call: &EndpointCall) -> AuthResult<EndpointInput> {
61 let input = if call.operation_id() == "verifyMagicLink" {
62 EndpointInput {
63 body: call.body().cloned(),
64 query: Some(validate_fields(call.query(), "query", VERIFY_FIELDS)?),
65 }
66 } else {
67 EndpointInput {
68 body: Some(validate_fields(
69 call.body(),
70 "body",
71 MagicLinkSignInInput::FIELDS,
72 )?),
73 query: call.query().cloned(),
74 }
75 };
76 if call.headers().is_none() {
77 return Err(validation("Headers is required"));
78 }
79 Ok(input)
80}
81
82impl MagicLinkPlugin {
83 pub fn verify_endpoint(
89 query: &MagicLinkVerifyInput,
90 ) -> AuthResult<ServerEndpoint<MagicLinkVerified>> {
91 ServerEndpoint::new("magic-link", "magicLinkVerify").with_query(query)
92 }
93
94 pub fn sign_in_endpoint(
99 input: &MagicLinkSignInInput,
100 ) -> AuthResult<ServerEndpoint<MagicLinkSent>> {
101 ServerEndpoint::new("magic-link", "signInMagicLink").with_body(input)
102 }
103
104 pub(super) async fn call_endpoint(
105 &self,
106 call: &EndpointCall,
107 ctx: &AuthContext<impl AuthSchema>,
108 ) -> AuthResult<EndpointResponse> {
109 let output = if call.operation_id() == "verifyMagicLink" {
110 self.verify_core(call, ctx, call.query_as()?).await?
111 } else {
112 if let Some(request) = call.request() {
113 alibi_core::middleware::CsrfMiddleware::new(
114 alibi_core::middleware::CsrfConfig::new(),
115 ctx.config.clone(),
116 )
117 .check_login_origin(request)?;
118 }
119 self.sign_in_core(call.request(), ctx, call.body_as()?)
120 .await?
121 };
122 if output.status == 302 {
123 let mut response = EndpointResponse::redirect(
124 output.headers.get("location").cloned().unwrap_or_default(),
125 );
126 response.merge_headers(output.headers);
127 return Ok(response);
128 }
129 let mut response = if output.status >= 400 {
130 EndpointResponse::error(crate::endpoint::error_response(output.clone()))
131 .without_error_body()
132 } else {
133 EndpointResponse::value(alibi_core::utils::json::from_slice(&output.body)?)
134 };
135 response.set_status(Some(output.status));
136 response.merge_headers(output.headers);
137 Ok(response)
138 }
139}