feat: account deletion request and active-user check

This commit is contained in:
2026-10-26 02:11:44 +01:00
parent 83bb5d9847
commit 0b5d18c4ee
27 changed files with 577 additions and 151 deletions

View File

@@ -0,0 +1,60 @@
use domain::{errors::DomainError, models::UserRole, value_objects::UserId};
use crate::users::deps::RequestAccountDeletionDeps;
/// Tombstones `target`. A user may delete themselves; an admin may delete
/// anyone. Either way the *actor's* password is the proof, and the last
/// active admin can never be removed. `UserDeleted` rides the repository
/// write, so the tombstone and its event commit together.
pub async fn execute(
deps: &RequestAccountDeletionDeps,
actor: UserId,
target: UserId,
password: &str,
) -> Result<(), DomainError> {
let actor_user = deps
.user
.find_by_id(&actor)
.await?
.ok_or_else(|| DomainError::Unauthorized("account is not active".into()))?;
let is_self = actor == target;
if !is_self && !matches!(actor_user.role(), UserRole::Admin) {
return Err(DomainError::Forbidden(
"only an admin can delete another account".into(),
));
}
if !deps
.password_hasher
.verify(password, actor_user.password_hash())
.await?
{
return Err(DomainError::Unauthorized("incorrect password".into()));
}
let target_user = if is_self {
actor_user
} else {
deps.user
.find_by_id(&target)
.await?
.ok_or_else(|| DomainError::NotFound("User not found".into()))?
};
if matches!(target_user.role(), UserRole::Admin) && deps.user.count_active_admins().await? <= 1
{
return Err(DomainError::Conflict(
"the last active admin cannot be deleted".into(),
));
}
deps.user
.mark_deleted(
&target,
&[domain::events::DomainEvent::UserDeleted {
user_id: target.clone(),
}],
)
.await
}