Skip to main content

rustauth_plugins/email_otp/
types.rs

1use 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}