use crate::error::Result;
use crate::domain::model::{
Account, AccountStatus, CreateUserOutcome, LoginRequest, RegisterRequest, User,
};
use crate::intern::auth::AuthService;
use super::support;
use super::tracker::LoginAttemptTracker;
impl AuthService {
#[tracing::instrument(
name = "auth.authenticate_user", skip(self, body, device_cookie),
fields(
attempts.remaining = tracing::field::Empty
)
)]
pub async fn authenticate_user(
&self,
body: &LoginRequest,
device_cookie: Option<&str>,
) -> Result<(User, Account, AccountStatus, u8)> {
let user_repo = &self.user_repository;
let app_state = self;
let (target_user, target_account) =
support::resolve_user_with_password(&body.email, app_state).await?;
let password_valid = self
.crypto
.password_hasher
.verify(&body.password, &target_account.password)?;
let user_id = target_user.id()?;
let tracker = LoginAttemptTracker::new(&self.crypto);
let identity = tracker.resolve_identity(device_cookie, &body.email);
let lockout_key = tracker.resolve_lockout_key(device_cookie, &body.email);
if user_repo.is_locked(&lockout_key).await {
tracing::warn!(
user.id = %user_id,
error.code = "ForbiddenReason::AccountSuspended",
"Login blocked — account is locked"
);
return Ok((
target_user.clone(),
target_account.clone(),
AccountStatus::Suspended,
0,
));
}
tracing::Span::current().record("user.id", user_id);
let (account_status, attempts) = match password_valid {
true => {
let _ = user_repo.clear_key(&identity).await;
(AccountStatus::Active, 0)
}
_ => {
let attempts = self
.register_failed_attempt(&identity, device_cookie)
.await
.unwrap_or(1);
let remaining = &self
.configuration
.security
.auth
.max_failed_attempts
.saturating_sub(attempts);
tracing::Span::current().record("attempts.remaining", remaining);
(AccountStatus::InvalidCredentials, attempts)
}
};
Ok((
target_user.clone(),
target_account.clone(),
account_status,
attempts,
))
}
async fn register_failed_attempt(
&self,
identity: &str,
device_cookie: Option<&str>,
) -> Result<u8> {
let user_repo = &self.user_repository;
let auth_security = &self.configuration.security.auth;
let expiry = auth_security.lockout_duration as u64;
let attempts = user_repo.increment(identity, expiry).await;
let max_failed_attempts = match device_cookie {
Some(_) => auth_security.max_failed_attempts * 2,
None => auth_security.max_failed_attempts,
};
if attempts >= max_failed_attempts {
user_repo.put_cookie_in_lockout(identity, expiry).await?;
tracing::warn!("Authentication Failed — update lockout countdown");
}
Ok(attempts)
}
#[tracing::instrument(
name = "auth.create_user",
skip(self, body),
fields(user.id = tracing::field::Empty)
)]
pub async fn create_user(&self, body: RegisterRequest) -> Result<CreateUserOutcome> {
let app_state = self;
let (_, user_exist) = support::resolve_user(&body.email, app_state).await?;
let password = self.crypto.password_hasher.hash(&body.password)?;
if user_exist {
tracing::warn!(
email = %body.email,
error.code = "ConflictReason::AlreadyExists",
"Registeration failed — user already exists"
);
return Ok(CreateUserOutcome::AlreadyExists);
}
let mut user = User::new()
.with_username(&body.username)
.with_email(&body.email);
let user_id: String = self.user_repository.insert(&user).await?;
user.with_id(&user_id);
let account = Account::user(&user_id).with_password(&password);
self.account_repository.insert(account).await?;
tracing::info!(user.id = %user_id, "User created successfully");
Ok(CreateUserOutcome::Created(user))
}
#[tracing::instrument(
name = "auth.find_user_by_email",
skip(self),
fields(user.email = email)
)]
pub async fn find_user_by_email(&self, email: &str) -> Result<User> {
self.user_repository.find_by_email(email).await
}
#[tracing::instrument(
name = "auth.find_user",
skip(self),
fields(user.id = id)
)]
pub async fn find_user(&self, id: &str) -> Result<User> {
self.user_repository.find(id).await
}
}