Skip to main content

shared/domain/email/
repository.rs

1use chrono::Utc;
2use std::sync::Arc;
3
4use crate::error::InternalError;
5use crate::error::{CoreError, Result, TokenErrorType};
6
7use super::model::EmailVerificationToken;
8use super::ports::database::DatabaseAdapter;
9use super::ports::query::QueryBuilder;
10
11#[derive(Clone)]
12pub struct EmailVerificationTokenRepository {
13    adapter: Arc<dyn DatabaseAdapter<EmailVerificationToken>>,
14}
15
16impl EmailVerificationTokenRepository {
17    pub fn new(adapter: Arc<dyn DatabaseAdapter<EmailVerificationToken>>) -> Self {
18        Self { adapter }
19    }
20
21    #[tracing::instrument(name = "db.email.insert", skip(self, otp))]
22    pub async fn insert(&self, otp: EmailVerificationToken) -> Result<()> {
23        match self.adapter.insert(otp).await {
24            Ok(_id) => Ok(()),
25            Err(e) => {
26                tracing::error!("Failed to insert user to database - {e}");
27                Err(CoreError::Internal(InternalError::Database(e.to_string())))
28            }
29        }
30    }
31
32    #[tracing::instrument(name = "db.email.consume", skip(self, hash))]
33    pub async fn consume(&self, hash: &str) -> Result<EmailVerificationToken> {
34        let filter = QueryBuilder::default()
35            .eq("hash", hash)
36            .gt("expiresAt", Utc::now())
37            .is_null("usedAt");
38        let update = QueryBuilder::default().set("usedAt", Utc::now());
39
40        match self.adapter.find_one_and_update(filter, update).await {
41            Ok(Some(token)) => Ok(token),
42            Ok(None) => Err(CoreError::Unauthenticated(
43                crate::error::AuthError::TokenInvalid {
44                    token_type: TokenErrorType::EmailVerificationToken,
45                },
46            )),
47            Err(err) => {
48                tracing::error!(error_code = "InternalError::Database", error = %err, "Database query failed");
49                Err(err)
50            }
51        }
52    }
53
54    #[tracing::instrument(name = "db.email.invalidate", skip(self, id))]
55    pub async fn invalidate(&self, id: &str) -> Result<EmailVerificationToken> {
56        let filter = QueryBuilder::default().eq("id", id);
57        let update = QueryBuilder::default().set("usedAt", Utc::now());
58
59        match self.adapter.find_one_and_update(filter, update).await {
60            Ok(Some(token)) => Ok(token),
61            Ok(None) => Err(CoreError::Unauthenticated(
62                crate::error::AuthError::TokenInvalid {
63                    token_type: TokenErrorType::EmailVerificationToken,
64                },
65            )),
66            Err(err) => {
67                tracing::error!(error_code = "InternalError::Database", error = %err, "Database query failed");
68                Err(err)
69            }
70        }
71    }
72
73    #[tracing::instrument(name = "db.email.revoke", skip(self), fields(user.id = user_id))]
74    pub async fn revoke(&self, user_id: &str) -> Result<()> {
75        let filter = QueryBuilder::default().eq("userId", user_id);
76        // FIXME delete tokens not update
77        let update = QueryBuilder::default().set("usedAt", Utc::now());
78
79        match self.adapter.update_many(filter, update).await {
80            Ok(_) => Ok(()),
81            Err(err) => {
82                tracing::error!(error_code = "InternalError::Database", error = %err, "Database query failed");
83                Err(err)
84            }
85        }
86    }
87}