Skip to main content

openauth_plugins/email_otp/
types.rs

1use std::fmt;
2use std::sync::Arc;
3
4use http::Request;
5use openauth_core::error::OpenAuthError;
6use openauth_core::options::RateLimitRule;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum EmailOtpType {
10    EmailVerification,
11    SignIn,
12    ForgetPassword,
13    ChangeEmail,
14}
15
16impl EmailOtpType {
17    pub fn as_str(self) -> &'static str {
18        match self {
19            Self::EmailVerification => "email-verification",
20            Self::SignIn => "sign-in",
21            Self::ForgetPassword => "forget-password",
22            Self::ChangeEmail => "change-email",
23        }
24    }
25}
26
27impl TryFrom<&str> for EmailOtpType {
28    type Error = ();
29
30    fn try_from(value: &str) -> Result<Self, Self::Error> {
31        match value {
32            "email-verification" => Ok(Self::EmailVerification),
33            "sign-in" => Ok(Self::SignIn),
34            "forget-password" => Ok(Self::ForgetPassword),
35            "change-email" => Ok(Self::ChangeEmail),
36            _ => Err(()),
37        }
38    }
39}
40
41pub trait EmailOtpHasher: Send + Sync + 'static {
42    fn hash_otp(&self, otp: &str) -> Result<String, OpenAuthError>;
43}
44
45impl<F> EmailOtpHasher for F
46where
47    F: Fn(&str) -> Result<String, OpenAuthError> + Send + Sync + 'static,
48{
49    fn hash_otp(&self, otp: &str) -> Result<String, OpenAuthError> {
50        self(otp)
51    }
52}
53
54pub trait EmailOtpEncryptor: Send + Sync + 'static {
55    fn encrypt_otp(&self, otp: &str) -> Result<String, OpenAuthError>;
56    fn decrypt_otp(&self, stored: &str) -> Result<String, OpenAuthError>;
57}
58
59#[derive(Clone, Default)]
60pub enum OtpStorage {
61    #[default]
62    Plain,
63    Hashed,
64    Encrypted,
65    CustomHash(Arc<dyn EmailOtpHasher>),
66    CustomEncrypt(Arc<dyn EmailOtpEncryptor>),
67}
68
69impl fmt::Debug for OtpStorage {
70    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
71        match self {
72            Self::Plain => formatter.write_str("Plain"),
73            Self::Hashed => formatter.write_str("Hashed"),
74            Self::Encrypted => formatter.write_str("Encrypted"),
75            Self::CustomHash(_) => formatter.write_str("CustomHash(<hasher>)"),
76            Self::CustomEncrypt(_) => formatter.write_str("CustomEncrypt(<encryptor>)"),
77        }
78    }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
82pub enum ResendStrategy {
83    #[default]
84    Rotate,
85    Reuse,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
89pub struct ChangeEmailOptions {
90    pub enabled: bool,
91    pub verify_current_email: bool,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct EmailOtpPayload {
96    pub email: String,
97    pub otp: String,
98    pub otp_type: EmailOtpType,
99}
100
101pub trait SendEmailOtp: Send + Sync + 'static {
102    fn send_email_otp(
103        &self,
104        payload: EmailOtpPayload,
105        request: Option<&Request<Vec<u8>>>,
106    ) -> Result<(), OpenAuthError>;
107}
108
109impl<F> SendEmailOtp for F
110where
111    F: for<'a> Fn(EmailOtpPayload, Option<&'a Request<Vec<u8>>>) -> Result<(), OpenAuthError>
112        + Send
113        + Sync
114        + 'static,
115{
116    fn send_email_otp(
117        &self,
118        payload: EmailOtpPayload,
119        request: Option<&Request<Vec<u8>>>,
120    ) -> Result<(), OpenAuthError> {
121        self(payload, request)
122    }
123}
124
125pub trait EmailOtpGenerator: Send + Sync + 'static {
126    fn generate_otp(&self, email: &str, otp_type: EmailOtpType, length: usize) -> String;
127}
128
129impl<F> EmailOtpGenerator for F
130where
131    F: Fn(&str, EmailOtpType, usize) -> String + Send + Sync + 'static,
132{
133    fn generate_otp(&self, email: &str, otp_type: EmailOtpType, length: usize) -> String {
134        self(email, otp_type, length)
135    }
136}
137
138#[derive(Clone)]
139pub struct EmailOtpOptions {
140    pub sender: Option<Arc<dyn SendEmailOtp>>,
141    pub generator: Option<Arc<dyn EmailOtpGenerator>>,
142    pub otp_length: usize,
143    pub expires_in: u64,
144    pub send_verification_on_sign_up: bool,
145    pub override_default_email_verification: bool,
146    pub disable_sign_up: bool,
147    pub allowed_attempts: u32,
148    pub store_otp: OtpStorage,
149    pub resend_strategy: ResendStrategy,
150    pub change_email: ChangeEmailOptions,
151    pub rate_limit: Option<RateLimitRule>,
152}
153
154impl Default for EmailOtpOptions {
155    fn default() -> Self {
156        Self {
157            sender: None,
158            generator: None,
159            otp_length: 6,
160            expires_in: 5 * 60,
161            send_verification_on_sign_up: false,
162            override_default_email_verification: false,
163            disable_sign_up: false,
164            allowed_attempts: 3,
165            store_otp: OtpStorage::Plain,
166            resend_strategy: ResendStrategy::Rotate,
167            change_email: ChangeEmailOptions::default(),
168            rate_limit: None,
169        }
170    }
171}
172
173impl fmt::Debug for EmailOtpOptions {
174    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
175        formatter
176            .debug_struct("EmailOtpOptions")
177            .field("sender", &self.sender.as_ref().map(|_| "<sender>"))
178            .field("generator", &self.generator.as_ref().map(|_| "<generator>"))
179            .field("otp_length", &self.otp_length)
180            .field("expires_in", &self.expires_in)
181            .field(
182                "send_verification_on_sign_up",
183                &self.send_verification_on_sign_up,
184            )
185            .field(
186                "override_default_email_verification",
187                &self.override_default_email_verification,
188            )
189            .field("disable_sign_up", &self.disable_sign_up)
190            .field("allowed_attempts", &self.allowed_attempts)
191            .field("store_otp", &self.store_otp)
192            .field("resend_strategy", &self.resend_strategy)
193            .field("change_email", &self.change_email)
194            .field("rate_limit", &self.rate_limit)
195            .finish()
196    }
197}