Skip to main content

shared/domain/user/
repository.rs

1use std::sync::Arc;
2
3use crate::error::{CoreError, InternalError, ResourceKind, Result};
4
5use super::model::User;
6use super::ports::cache::CacheAdapter;
7use super::ports::database::DatabaseAdapter;
8use super::ports::query::QueryBuilder;
9
10#[derive(Clone)]
11pub struct UserRepository {
12    adapter: Arc<dyn DatabaseAdapter<User>>,
13    cache: Arc<dyn CacheAdapter>,
14}
15
16impl UserRepository {
17    pub fn new(adapter: Arc<dyn DatabaseAdapter<User>>, cache: Arc<dyn CacheAdapter>) -> Self {
18        Self { adapter, cache }
19    }
20}
21
22impl UserRepository {
23    // Cache
24    #[tracing::instrument(name = "cache.user.insert", skip(self))]
25    pub async fn add(&self, key: &str, expiration: u64) -> Result<()> {
26        self.cache.insert(key, "1", expiration).await
27    }
28    #[tracing::instrument(name = "cache.user.increment", skip(self))]
29    pub async fn increment(&self, key: &str, expiration: u64) -> u8 {
30        match self.cache.find_one(key).await {
31            Ok(Some(_)) => self
32                .cache
33                .increment(key)
34                .await
35                .map(|v| v as u8)
36                .unwrap_or(1),
37            Ok(None) | Err(_) => self.add(key, expiration).await.map(|_| 1).unwrap_or(1),
38        }
39    }
40    #[tracing::instrument(name = "cache.user.get_increment", skip(self))]
41    pub async fn get_attempts(&self, key: &str) -> u8 {
42        if let Ok(Some(val)) = self.cache.find_one(key).await {
43            return val.parse::<u8>().unwrap_or(0);
44        }
45        0
46    }
47    #[tracing::instrument(name = "cache.user.insert", skip(self))]
48    pub async fn put_cookie_in_lockout(&self, key: &str, expiration: u64) -> Result<()> {
49        self.cache
50            .insert(&format!("lockout:{}", key), "locked", expiration)
51            .await
52    }
53    #[tracing::instrument(name = "cache.user.islocked", skip(self))]
54    pub async fn is_locked(&self, key: &str) -> bool {
55        self.cache.find_one(key).await.is_ok_and(|v| v.is_some())
56    }
57    #[tracing::instrument(name = "cache.user.reset", skip(self))]
58    pub async fn reset_attempts(&self, key: &str, expiration: u64) -> Result<()> {
59        self.cache.update(key, "0", expiration).await
60    }
61    #[tracing::instrument(name = "cache.user.clear", skip(self))]
62    pub async fn clear_key(&self, key: &str) -> Result<()> {
63        self.cache.delete_one(key).await
64    }
65
66    // Database
67    #[tracing::instrument(
68        name = "db.user.insert",
69        skip(self, user),
70        fields(db.table = "users", db.operation = "INSERT")
71    )]
72    pub async fn insert(&self, user: &User) -> Result<String> {
73        match self.adapter.insert(user.to_owned()).await {
74            Ok(id) => Ok(id),
75            Err(e) => {
76                tracing::error!("Failed to insert user to database - {e}");
77                Err(CoreError::Internal(InternalError::Database(e.to_string())))
78            }
79        }
80    }
81
82    #[tracing::instrument(name = "db.user.find", skip(self, user_id))]
83    pub async fn find(&self, user_id: &str) -> Result<User> {
84        let filter = QueryBuilder::default().eq("id", user_id);
85
86        match self.adapter.find_one(filter).await {
87            Ok(Some(user)) => Ok(user),
88            Ok(None) => Err(CoreError::NotFound(ResourceKind::User {
89                id: Some(user_id.into()),
90                email: None,
91            })),
92            Err(err) => {
93                tracing::error!(error_code = "InternalError::Database", error = %err, "Database query failed");
94                Err(err)
95            }
96        }
97    }
98
99    #[tracing::instrument(name = "db.user.find_by_email", skip(self, email))]
100    pub async fn find_by_email(&self, email: &str) -> Result<User> {
101        let filter = QueryBuilder::default().eq("email", email);
102
103        match self.adapter.find_one(filter).await {
104            Ok(Some(user)) => Ok(user),
105            Ok(None) => Err(CoreError::NotFound(ResourceKind::User {
106                id: None,
107                email: Some(email.into()),
108            })),
109            Err(err) => {
110                tracing::error!(error_code = "InternalError::Database", error = %err, "Database query failed");
111                Err(err)
112            }
113        }
114    }
115}