rustauth_plugins/email_otp/
types.rs1use std::fmt;
2use std::sync::Arc;
3
4use http::Request;
5use rustauth_core::error::RustAuthError;
6use rustauth_core::outbound::OutboundSendFuture;
7use time::Duration;
8
9use rustauth_core::options::RateLimitRule;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum EmailOtpType {
13 EmailVerification,
14 SignIn,
15 ForgetPassword,
16 ChangeEmail,
17}
18
19impl EmailOtpType {
20 pub fn as_str(self) -> &'static str {
21 match self {
22 Self::EmailVerification => "email-verification",
23 Self::SignIn => "sign-in",
24 Self::ForgetPassword => "forget-password",
25 Self::ChangeEmail => "change-email",
26 }
27 }
28}
29
30impl TryFrom<&str> for EmailOtpType {
31 type Error = ();
32
33 fn try_from(value: &str) -> Result<Self, Self::Error> {
34 match value {
35 "email-verification" => Ok(Self::EmailVerification),
36 "sign-in" => Ok(Self::SignIn),
37 "forget-password" => Ok(Self::ForgetPassword),
38 "change-email" => Ok(Self::ChangeEmail),
39 _ => Err(()),
40 }
41 }
42}
43
44pub trait EmailOtpHasher: Send + Sync + 'static {
45 fn hash_otp(&self, otp: &str) -> Result<String, RustAuthError>;
46}
47
48impl<F> EmailOtpHasher for F
49where
50 F: Fn(&str) -> Result<String, RustAuthError> + Send + Sync + 'static,
51{
52 fn hash_otp(&self, otp: &str) -> Result<String, RustAuthError> {
53 self(otp)
54 }
55}
56
57pub trait EmailOtpEncryptor: Send + Sync + 'static {
58 fn encrypt_otp(&self, otp: &str) -> Result<String, RustAuthError>;
59 fn decrypt_otp(&self, stored: &str) -> Result<String, RustAuthError>;
60}
61
62#[derive(Clone, Default)]
63pub enum OtpStorage {
64 #[default]
65 Plain,
66 Hashed,
67 Encrypted,
68 CustomHash(Arc<dyn EmailOtpHasher>),
69 CustomEncrypt(Arc<dyn EmailOtpEncryptor>),
70}
71
72impl fmt::Debug for OtpStorage {
73 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
74 match self {
75 Self::Plain => formatter.write_str("Plain"),
76 Self::Hashed => formatter.write_str("Hashed"),
77 Self::Encrypted => formatter.write_str("Encrypted"),
78 Self::CustomHash(_) => formatter.write_str("CustomHash(<hasher>)"),
79 Self::CustomEncrypt(_) => formatter.write_str("CustomEncrypt(<encryptor>)"),
80 }
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
85pub enum ResendStrategy {
86 #[default]
87 Rotate,
88 Reuse,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
92pub struct ChangeEmailOptions {
93 pub enabled: bool,
94 pub verify_current_email: bool,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct EmailOtpPayload {
99 pub email: String,
100 pub otp: String,
101 pub otp_type: EmailOtpType,
102}
103
104pub trait SendEmailOtp: Send + Sync + 'static {
105 fn send_email_otp(
106 &self,
107 payload: EmailOtpPayload,
108 request: Option<&Request<Vec<u8>>>,
109 ) -> OutboundSendFuture;
110}
111
112impl<F> SendEmailOtp for F
113where
114 F: for<'a> Fn(EmailOtpPayload, Option<&'a Request<Vec<u8>>>) -> OutboundSendFuture
115 + Send
116 + Sync
117 + 'static,
118{
119 fn send_email_otp(
120 &self,
121 payload: EmailOtpPayload,
122 request: Option<&Request<Vec<u8>>>,
123 ) -> OutboundSendFuture {
124 self(payload, request)
125 }
126}
127
128pub trait EmailOtpGenerator: Send + Sync + 'static {
129 fn generate_otp(&self, email: &str, otp_type: EmailOtpType, length: usize) -> String;
130}
131
132impl<F> EmailOtpGenerator for F
133where
134 F: Fn(&str, EmailOtpType, usize) -> String + Send + Sync + 'static,
135{
136 fn generate_otp(&self, email: &str, otp_type: EmailOtpType, length: usize) -> String {
137 self(email, otp_type, length)
138 }
139}
140
141#[derive(Clone)]
142pub struct EmailOtpOptions {
143 pub sender: Option<Arc<dyn SendEmailOtp>>,
144 pub generator: Option<Arc<dyn EmailOtpGenerator>>,
145 pub otp_length: usize,
146 pub expires_in: Duration,
147 pub send_verification_on_sign_up: bool,
148 pub override_default_email_verification: bool,
149 pub disable_sign_up: bool,
150 pub allowed_attempts: u32,
151 pub store_otp: OtpStorage,
152 pub resend_strategy: ResendStrategy,
153 pub change_email: ChangeEmailOptions,
154 pub rate_limit: Option<RateLimitRule>,
155}
156
157impl EmailOtpOptions {
158 #[must_use]
159 pub fn new(sender: Arc<dyn SendEmailOtp>) -> Self {
160 Self {
161 sender: Some(sender),
162 ..Self::default()
163 }
164 }
165
166 #[must_use]
167 pub fn builder() -> EmailOtpOptionsBuilder {
168 EmailOtpOptionsBuilder::default()
169 }
170
171 #[must_use]
172 pub fn expires_in(mut self, expires_in: Duration) -> Self {
173 self.expires_in = expires_in;
174 self
175 }
176
177 pub fn validate(&self) -> Result<(), RustAuthError> {
178 if self.sender.is_none() {
179 return Err(RustAuthError::InvalidConfig(
180 "email-otp plugin requires a sender callback".to_owned(),
181 ));
182 }
183 if self.otp_length == 0 {
184 return Err(RustAuthError::InvalidConfig(
185 "email-otp otp_length must be greater than zero".to_owned(),
186 ));
187 }
188 if self.allowed_attempts == 0 {
189 return Err(RustAuthError::InvalidConfig(
190 "email-otp allowed_attempts must be greater than zero".to_owned(),
191 ));
192 }
193 Ok(())
194 }
195}
196
197#[derive(Clone, Default)]
198pub struct EmailOtpOptionsBuilder {
199 sender: Option<Arc<dyn SendEmailOtp>>,
200 generator: Option<Arc<dyn EmailOtpGenerator>>,
201 otp_length: Option<usize>,
202 expires_in: Option<Duration>,
203 send_verification_on_sign_up: Option<bool>,
204 override_default_email_verification: Option<bool>,
205 disable_sign_up: Option<bool>,
206 allowed_attempts: Option<u32>,
207 store_otp: Option<OtpStorage>,
208 resend_strategy: Option<ResendStrategy>,
209 change_email: Option<ChangeEmailOptions>,
210 rate_limit: Option<Option<RateLimitRule>>,
211}
212
213impl EmailOtpOptionsBuilder {
214 #[must_use]
215 pub fn sender(mut self, sender: Arc<dyn SendEmailOtp>) -> Self {
216 self.sender = Some(sender);
217 self
218 }
219
220 pub fn build(self) -> Result<EmailOtpOptions, RustAuthError> {
221 let defaults = EmailOtpOptions::default();
222 let options = EmailOtpOptions {
223 sender: self.sender,
224 generator: self.generator,
225 otp_length: self.otp_length.unwrap_or(defaults.otp_length),
226 expires_in: self.expires_in.unwrap_or(defaults.expires_in),
227 send_verification_on_sign_up: self
228 .send_verification_on_sign_up
229 .unwrap_or(defaults.send_verification_on_sign_up),
230 override_default_email_verification: self
231 .override_default_email_verification
232 .unwrap_or(defaults.override_default_email_verification),
233 disable_sign_up: self.disable_sign_up.unwrap_or(defaults.disable_sign_up),
234 allowed_attempts: self.allowed_attempts.unwrap_or(defaults.allowed_attempts),
235 store_otp: self.store_otp.unwrap_or(defaults.store_otp),
236 resend_strategy: self.resend_strategy.unwrap_or(defaults.resend_strategy),
237 change_email: self.change_email.unwrap_or(defaults.change_email),
238 rate_limit: self.rate_limit.unwrap_or(defaults.rate_limit),
239 };
240 options.validate()?;
241 Ok(options)
242 }
243}
244
245impl Default for EmailOtpOptions {
246 fn default() -> Self {
247 Self {
248 sender: None,
249 generator: None,
250 otp_length: 6,
251 expires_in: Duration::minutes(5),
252 send_verification_on_sign_up: false,
253 override_default_email_verification: false,
254 disable_sign_up: false,
255 allowed_attempts: 3,
256 store_otp: OtpStorage::Plain,
257 resend_strategy: ResendStrategy::Rotate,
258 change_email: ChangeEmailOptions::default(),
259 rate_limit: None,
260 }
261 }
262}
263
264impl fmt::Debug for EmailOtpOptions {
265 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
266 formatter
267 .debug_struct("EmailOtpOptions")
268 .field("sender", &self.sender.as_ref().map(|_| "<sender>"))
269 .field("generator", &self.generator.as_ref().map(|_| "<generator>"))
270 .field("otp_length", &self.otp_length)
271 .field("expires_in", &self.expires_in)
272 .field(
273 "send_verification_on_sign_up",
274 &self.send_verification_on_sign_up,
275 )
276 .field(
277 "override_default_email_verification",
278 &self.override_default_email_verification,
279 )
280 .field("disable_sign_up", &self.disable_sign_up)
281 .field("allowed_attempts", &self.allowed_attempts)
282 .field("store_otp", &self.store_otp)
283 .field("resend_strategy", &self.resend_strategy)
284 .field("change_email", &self.change_email)
285 .field("rate_limit", &self.rate_limit)
286 .finish()
287 }
288}