shared/domain/user/
repository.rs1use 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 #[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 #[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}