shared/application/auth/
user_service.rs1use crate::error::Result;
2
3use crate::domain::model::{
4 Account, AccountStatus, CreateUserOutcome, LoginRequest, RegisterRequest, User,
5};
6use crate::intern::auth::AuthService;
7
8use super::support;
9use super::tracker::LoginAttemptTracker;
10
11impl AuthService {
12 #[tracing::instrument(
13 name = "auth.authenticate_user", skip(self, body, device_cookie),
14 fields(
15 attempts.remaining = tracing::field::Empty
16 )
17 )]
18 pub async fn authenticate_user(
19 &self,
20 body: &LoginRequest,
21 device_cookie: Option<&str>,
22 ) -> Result<(User, Account, AccountStatus, u8)> {
23 let user_repo = &self.user_repository;
24
25 let app_state = self;
27 let (target_user, target_account) =
28 support::resolve_user_with_password(&body.email, app_state).await?;
29 let password_valid = self
30 .crypto
31 .password_hasher
32 .verify(&body.password, &target_account.password)?;
33 let user_id = target_user.id()?;
34
35 let tracker = LoginAttemptTracker::new(&self.crypto);
37 let identity = tracker.resolve_identity(device_cookie, &body.email);
38 let lockout_key = tracker.resolve_lockout_key(device_cookie, &body.email);
39
40 if user_repo.is_locked(&lockout_key).await {
43 tracing::warn!(
44 user.id = %user_id,
45 error.code = "ForbiddenReason::AccountSuspended",
46 "Login blocked — account is locked"
47 );
48 return Ok((
49 target_user.clone(),
50 target_account.clone(),
51 AccountStatus::Suspended,
52 0,
53 ));
54 }
55
56 tracing::Span::current().record("user.id", user_id);
58 let (account_status, attempts) = match password_valid {
59 true => {
60 let _ = user_repo.clear_key(&identity).await;
61 (AccountStatus::Active, 0)
62 }
63 _ => {
64 let attempts = self
65 .register_failed_attempt(&identity, device_cookie)
66 .await
67 .unwrap_or(1);
68
69 let remaining = &self
70 .configuration
71 .security
72 .auth
73 .max_failed_attempts
74 .saturating_sub(attempts);
75
76 tracing::Span::current().record("attempts.remaining", remaining);
77 (AccountStatus::InvalidCredentials, attempts)
78 }
79 };
80
81 Ok((
82 target_user.clone(),
83 target_account.clone(),
84 account_status,
85 attempts,
86 ))
87 }
88
89 async fn register_failed_attempt(
90 &self,
91 identity: &str,
92 device_cookie: Option<&str>,
93 ) -> Result<u8> {
94 let user_repo = &self.user_repository;
95
96 let auth_security = &self.configuration.security.auth;
97 let expiry = auth_security.lockout_duration as u64;
98
99 let attempts = user_repo.increment(identity, expiry).await;
100
101 let max_failed_attempts = match device_cookie {
103 Some(_) => auth_security.max_failed_attempts * 2,
104 None => auth_security.max_failed_attempts,
105 };
106 if attempts >= max_failed_attempts {
107 user_repo.put_cookie_in_lockout(identity, expiry).await?;
108 tracing::warn!("Authentication Failed — update lockout countdown");
109 }
110
111 Ok(attempts)
112 }
113
114 #[tracing::instrument(
115 name = "auth.create_user",
116 skip(self, body),
117 fields(user.id = tracing::field::Empty)
118 )]
119 pub async fn create_user(&self, body: RegisterRequest) -> Result<CreateUserOutcome> {
120 let app_state = self;
124 let (_, user_exist) = support::resolve_user(&body.email, app_state).await?;
125
126 let password = self.crypto.password_hasher.hash(&body.password)?;
128
129 if user_exist {
130 tracing::warn!(
131 email = %body.email,
132 error.code = "ConflictReason::AlreadyExists",
133 "Registeration failed — user already exists"
134 );
135 return Ok(CreateUserOutcome::AlreadyExists);
136 }
137
138 let mut user = User::new()
139 .with_username(&body.username)
140 .with_email(&body.email);
141
142 let user_id: String = self.user_repository.insert(&user).await?;
144 user.with_id(&user_id);
145
146 let account = Account::user(&user_id).with_password(&password);
147 self.account_repository.insert(account).await?;
148
149 tracing::info!(user.id = %user_id, "User created successfully");
154 Ok(CreateUserOutcome::Created(user))
155 }
156
157 #[tracing::instrument(
158 name = "auth.find_user_by_email",
159 skip(self),
160 fields(user.email = email)
161 )]
162 pub async fn find_user_by_email(&self, email: &str) -> Result<User> {
163 self.user_repository.find_by_email(email).await
164 }
165
166 #[tracing::instrument(
167 name = "auth.find_user",
168 skip(self),
169 fields(user.id = id)
170 )]
171 pub async fn find_user(&self, id: &str) -> Result<User> {
172 self.user_repository.find(id).await
173 }
174}