feat: account deletion request and active-user check
This commit is contained in:
@@ -1,19 +0,0 @@
|
||||
use domain::{errors::DomainError, events::DomainEvent, value_objects::UserId};
|
||||
|
||||
use crate::users::deps::DeleteAccountDeps;
|
||||
|
||||
pub async fn execute(deps: &DeleteAccountDeps, user_id: uuid::Uuid) -> Result<(), DomainError> {
|
||||
let uid = UserId::from_uuid(user_id);
|
||||
|
||||
deps.user
|
||||
.find_by_id(&uid)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("User not found".into()))?;
|
||||
|
||||
// Notify federation peers before any data is removed so they can process the tombstone.
|
||||
deps.event_publisher
|
||||
.publish(&DomainEvent::UserDeleted { user_id: uid })
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2,7 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use domain::ports::{
|
||||
DiaryQuery, EventPublisher, FederatedProfileQuery, FederationAdminQuery, FollowGraphQuery,
|
||||
ObjectStorage, StatsRepository, UserProfileFieldsRepository, UserRepository,
|
||||
ObjectStorage, PasswordHasher, StatsRepository, UserProfileFieldsRepository, UserRepository,
|
||||
UserSettingsRepository,
|
||||
};
|
||||
use domain::value_objects::InstanceIdentity;
|
||||
@@ -47,9 +47,13 @@ pub struct UpdateProfileDeps {
|
||||
pub event_publisher: Arc<dyn EventPublisher>,
|
||||
}
|
||||
|
||||
pub struct DeleteAccountDeps {
|
||||
pub struct RequestAccountDeletionDeps {
|
||||
pub user: Arc<dyn UserRepository>,
|
||||
pub password_hasher: Arc<dyn PasswordHasher>,
|
||||
}
|
||||
|
||||
pub struct EnsureActiveDeps {
|
||||
pub user: Arc<dyn UserRepository>,
|
||||
pub event_publisher: Arc<dyn EventPublisher>,
|
||||
}
|
||||
|
||||
pub struct GetCurrentProfileDeps {
|
||||
|
||||
12
crates/application/src/users/ensure_active.rs
Normal file
12
crates/application/src/users/ensure_active.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use domain::{errors::DomainError, value_objects::UserId};
|
||||
|
||||
use crate::users::deps::EnsureActiveDeps;
|
||||
|
||||
/// A token outlives its account; this is the check that stops a deleted
|
||||
/// (or vanished) user from acting on one.
|
||||
pub async fn execute(deps: &EnsureActiveDeps, user: &UserId) -> Result<(), DomainError> {
|
||||
match deps.user.find_by_id(user).await? {
|
||||
Some(_) => Ok(()),
|
||||
None => Err(DomainError::Unauthorized("account is not active".into())),
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
pub mod authorize_admin;
|
||||
pub mod commands;
|
||||
pub mod delete_account;
|
||||
pub mod deps;
|
||||
mod diary_filter;
|
||||
pub mod ensure_active;
|
||||
pub mod get_current_profile;
|
||||
pub mod get_federated_profile;
|
||||
pub mod get_federated_profile_stats;
|
||||
@@ -12,6 +12,7 @@ pub mod get_profile_settings;
|
||||
pub mod get_settings;
|
||||
pub mod get_users;
|
||||
pub mod queries;
|
||||
pub mod request_account_deletion;
|
||||
pub mod resolve_username_to_id;
|
||||
pub mod update_profile;
|
||||
pub mod update_profile_fields;
|
||||
|
||||
60
crates/application/src/users/request_account_deletion.rs
Normal file
60
crates/application/src/users/request_account_deletion.rs
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user