Skip to main content

rustauth_plugins/magic_link/
options.rs

1use std::future::Future;
2use std::pin::Pin;
3use std::sync::Arc;
4
5use http::Request;
6use rustauth_core::context::AuthContext;
7use rustauth_core::error::RustAuthError;
8use rustauth_core::options::RateLimitRule;
9use rustauth_core::outbound::OutboundSendFuture;
10use serde_json::Value;
11use time::Duration;
12
13use super::token::TokenStorage;
14
15pub type MagicLinkFuture<'a, T> =
16    Pin<Box<dyn Future<Output = Result<T, RustAuthError>> + Send + 'a>>;
17
18#[derive(Debug, Clone, PartialEq)]
19pub struct MagicLinkEmail {
20    pub email: String,
21    pub url: String,
22    pub token: String,
23    pub metadata: Option<Value>,
24}
25
26#[derive(Clone, Copy)]
27pub struct MagicLinkSendContext<'a> {
28    pub context: &'a AuthContext,
29    pub request: &'a Request<Vec<u8>>,
30}
31
32pub type SendMagicLink = Arc<dyn Fn(MagicLinkEmail) -> MagicLinkFuture<'static, ()> + Send + Sync>;
33pub type SendMagicLinkWithContext = Arc<
34    dyn for<'a> Fn(MagicLinkEmail, MagicLinkSendContext<'a>) -> OutboundSendFuture + Send + Sync,
35>;
36pub type GenerateToken = Arc<dyn for<'a> Fn(&'a str) -> MagicLinkFuture<'a, String> + Send + Sync>;
37
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct MagicLinkRateLimit {
40    pub window: Duration,
41    pub max: u64,
42}
43
44impl Default for MagicLinkRateLimit {
45    fn default() -> Self {
46        Self {
47            window: Duration::minutes(1),
48            max: 5,
49        }
50    }
51}
52
53#[derive(Clone)]
54pub struct MagicLinkOptions {
55    pub(crate) expires_in: Duration,
56    pub(crate) allowed_attempts: AllowedAttempts,
57    pub(crate) send_magic_link: SendMagicLinkWithContext,
58    pub(crate) disable_sign_up: bool,
59    pub(crate) rate_limit: MagicLinkRateLimit,
60    pub(crate) generate_token: Option<GenerateToken>,
61    pub(crate) store_token: TokenStorage,
62}
63
64impl MagicLinkOptions {
65    pub fn new<F>(send_magic_link: F) -> Self
66    where
67        F: Fn(MagicLinkEmail) -> MagicLinkFuture<'static, ()> + Send + Sync + 'static,
68    {
69        let send_magic_link: SendMagicLink = Arc::new(send_magic_link);
70        Self::new_with_context(move |email, _ctx| {
71            let send_magic_link = Arc::clone(&send_magic_link);
72            send_magic_link(email)
73        })
74    }
75
76    pub fn new_with_context<F>(send_magic_link: F) -> Self
77    where
78        F: for<'a> Fn(MagicLinkEmail, MagicLinkSendContext<'a>) -> OutboundSendFuture
79            + Send
80            + Sync
81            + 'static,
82    {
83        Self {
84            expires_in: Duration::minutes(5),
85            allowed_attempts: AllowedAttempts::Limited(1),
86            send_magic_link: Arc::new(send_magic_link),
87            disable_sign_up: false,
88            rate_limit: MagicLinkRateLimit::default(),
89            generate_token: None,
90            store_token: TokenStorage::Plain,
91        }
92    }
93
94    #[must_use]
95    pub fn expires_in(mut self, expires_in: Duration) -> Self {
96        self.expires_in = expires_in;
97        self
98    }
99
100    #[must_use]
101    pub fn allowed_attempts(mut self, attempts: u64) -> Self {
102        self.allowed_attempts = AllowedAttempts::Limited(attempts);
103        self
104    }
105
106    #[must_use]
107    pub fn unlimited_attempts(mut self) -> Self {
108        self.allowed_attempts = AllowedAttempts::Unlimited;
109        self
110    }
111
112    #[must_use]
113    pub fn disable_sign_up(mut self, disabled: bool) -> Self {
114        self.disable_sign_up = disabled;
115        self
116    }
117
118    #[must_use]
119    pub fn rate_limit(mut self, rate_limit: MagicLinkRateLimit) -> Self {
120        self.rate_limit = rate_limit;
121        self
122    }
123
124    #[must_use]
125    pub fn generate_token<F>(mut self, generate_token: F) -> Self
126    where
127        F: for<'a> Fn(&'a str) -> MagicLinkFuture<'a, String> + Send + Sync + 'static,
128    {
129        self.generate_token = Some(Arc::new(generate_token));
130        self
131    }
132
133    #[must_use]
134    pub fn store_token(mut self, store_token: TokenStorage) -> Self {
135        self.store_token = store_token;
136        self
137    }
138
139    pub(crate) fn rate_limit_rule(&self) -> RateLimitRule {
140        RateLimitRule {
141            window: self.rate_limit.window,
142            max: self.rate_limit.max,
143        }
144    }
145
146    #[must_use]
147    pub fn builder() -> MagicLinkOptionsBuilder {
148        MagicLinkOptionsBuilder::default()
149    }
150}
151
152#[derive(Clone, Default)]
153pub struct MagicLinkOptionsBuilder {
154    send_magic_link: Option<SendMagicLinkWithContext>,
155    expires_in: Option<Duration>,
156    allowed_attempts: Option<AllowedAttempts>,
157    disable_sign_up: Option<bool>,
158    rate_limit: Option<MagicLinkRateLimit>,
159    generate_token: Option<GenerateToken>,
160    store_token: Option<TokenStorage>,
161}
162
163impl MagicLinkOptionsBuilder {
164    #[must_use]
165    pub fn send_magic_link<F>(mut self, send_magic_link: F) -> Self
166    where
167        F: for<'a> Fn(MagicLinkEmail, MagicLinkSendContext<'a>) -> OutboundSendFuture
168            + Send
169            + Sync
170            + 'static,
171    {
172        self.send_magic_link = Some(Arc::new(send_magic_link));
173        self
174    }
175
176    pub fn build(self) -> Result<MagicLinkOptions, rustauth_core::error::RustAuthError> {
177        let Some(send_magic_link) = self.send_magic_link else {
178            return Err(rustauth_core::error::RustAuthError::InvalidConfig(
179                "magic-link plugin requires a send_magic_link callback".to_owned(),
180            ));
181        };
182        Ok(MagicLinkOptions {
183            expires_in: self.expires_in.unwrap_or(Duration::minutes(5)),
184            allowed_attempts: self.allowed_attempts.unwrap_or(AllowedAttempts::Limited(1)),
185            send_magic_link,
186            disable_sign_up: self.disable_sign_up.unwrap_or(false),
187            rate_limit: self.rate_limit.unwrap_or_default(),
188            generate_token: self.generate_token,
189            store_token: self.store_token.unwrap_or(TokenStorage::Plain),
190        })
191    }
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq)]
195pub(crate) enum AllowedAttempts {
196    Limited(u64),
197    Unlimited,
198}
199
200impl AllowedAttempts {
201    pub(crate) fn exceeded(self, attempt: u64) -> bool {
202        match self {
203            Self::Limited(limit) => attempt >= limit,
204            Self::Unlimited => false,
205        }
206    }
207}