Skip to main content

shared/domain/password_reset/
repository.rs

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