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