use std::time::Duration; use export::AppState; use models::users::user_access_keys; use models::users::user_deletion_requests::{self, UserDeletionRequestStatus}; use models::users::user_emails; use models::users::user_recovery_codes; use models::users::user_sessions; use models::users::user_ssh_keys; use models::users::{user, user_profiles}; use sea_orm::sea_query::Expr; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, Set}; use time::OffsetDateTime; use crate::services::AuthService; const CHECK_INTERVAL_SECS: u64 = 3600; impl AuthService { #[tracing::instrument(name = "auth.execute_due_deletions", skip_all)] pub async fn execute_due_deletions(&self) -> Result { let db = self.db(); let now = OffsetDateTime::now_utc(); let due = user_deletion_requests::Entity::find() .filter(user_deletion_requests::Column::Status.eq(UserDeletionRequestStatus::Scheduled)) .filter(user_deletion_requests::Column::ScheduledFor.lte(now)) .all(db.read()) .await?; let mut executed = 0u64; for request in due { let uid = request.user_id; let Some(user_row) = user::Entity::find_by_id(uid).one(db.write()).await? else { let mut am: user_deletion_requests::ActiveModel = request.into(); am.status = Set(UserDeletionRequestStatus::Completed); am.completed_at = Set(Some(now)); am.update(db.write()).await?; executed += 1; continue; }; let anon_name = format!("deleted-{}", &uid.simple().to_string()[..8]); let mut active = user_row.into_active_model(); active.username = Set(anon_name.clone()); active.lower_name = Set(anon_name.clone()); active.display_name = Set("deleted user".to_string()); active.avatar_url = Set(String::new()); active.is_active = Set(false); active.prohibit_login = Set(true); active.passwd_hash = Set(None); active.password_last_changed_at = Set(None); active.passwd_version = Set(Some(OffsetDateTime::now_utc().unix_timestamp())); active.totp_encrypted_secret = Set(None); active.totp_enabled = Set(false); active.timezone = Set(None); active.locale = Set(None); active.language = Set(None); active.updated_at = Set(now); active.update(db.write()).await?; if let Some(profile) = user_profiles::Entity::find_by_id(uid) .one(db.write()) .await? { let mut am: user_profiles::ActiveModel = profile.into(); am.real_name = Set(None); am.bio = Set(None); am.pronouns = Set(None); am.website = Set(None); am.location = Set(None); am.company = Set(None); am.date_of_birth = Set(None); am.banner_url = Set(None); am.banner_hash = Set(None); am.banner_color = Set(None); am.accent_color = Set(None); am.custom_status_emoji = Set(None); am.custom_status_text = Set(None); am.custom_status_expires_at = Set(None); am.updated_at = Set(now); am.update(db.write()).await?; } user_emails::Entity::update_many() .col_expr( user_emails::Column::Email, Expr::value(format!("{anon_name}@deleted.invalid")), ) .col_expr( user_emails::Column::LowerEmail, Expr::value(format!("{anon_name}@deleted.invalid")), ) .col_expr(user_emails::Column::DeletedAt, Expr::value(Some(now))) .col_expr(user_emails::Column::UpdatedAt, Expr::value(now)) .filter(user_emails::Column::UserId.eq(uid)) .exec(db.write()) .await?; user_sessions::Entity::update_many() .col_expr(user_sessions::Column::IsRevoked, Expr::value(true)) .col_expr(user_sessions::Column::RevokedAt, Expr::value(Some(now))) .col_expr(user_sessions::Column::UpdatedAt, Expr::value(now)) .filter(user_sessions::Column::UserId.eq(uid)) .filter(user_sessions::Column::IsRevoked.eq(false)) .exec(db.write()) .await?; user_access_keys::Entity::delete_many() .filter(user_access_keys::Column::UserId.eq(uid)) .exec(db.write()) .await?; user_ssh_keys::Entity::delete_many() .filter(user_ssh_keys::Column::UserId.eq(uid)) .exec(db.write()) .await?; user_recovery_codes::Entity::delete_many() .filter(user_recovery_codes::Column::UserId.eq(uid)) .exec(db.write()) .await?; let mut am: user_deletion_requests::ActiveModel = request.into(); am.status = Set(UserDeletionRequestStatus::Completed); am.completed_at = Set(Some(now)); am.update(db.write()).await?; executed += 1; } Ok(executed) } } pub async fn run_deletion_executor(state: AppState) { let service = AuthService::new(state); let mut interval = tokio::time::interval(Duration::from_secs(CHECK_INTERVAL_SECS)); interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); loop { interval.tick().await; match service.execute_due_deletions().await { Ok(0) => {} Ok(n) => tracing::info!(count = n, "executed due account deletions"), Err(error) => tracing::warn!(%error, "account deletion executor failed"), } } }