rustauth_plugins/magic_link/
options.rs1use 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}