Keep deleted players' names reserved

This commit is contained in:
Claude committed 2026-09-30 17:46:54 +00:00
1 parent e344e7144e
commit 78da10364f
3 files changed
+8 -6

No files matched your search

+2 -1
View File
@@ -119,7 +119,8 @@ pub async fn purge_user(state: &AppState, user: &UserRow) -> AppResult<PurgeRepo
run(c, "UPDATE economy_transactions SET to_uuid = ?, to_name = ? WHERE to_uuid = ?", &[DELETED_UUID, DELETED_NAME, uuid]).await?;
// ---- identity ----
report.add("name_reservations", run(c, "DELETE FROM reserved_usernames WHERE uuid = ?", &[uuid]).await?);
// `reserved_usernames` is kept on purpose: a deleted player's name stays
// taken so nobody can impersonate them. It holds only the name and UUID.
// Explicit, in case a foreign key is ever relaxed.
let id = user.id.to_string();
for (area, sql) in [
+5 -4
View File
@@ -146,8 +146,8 @@ async fn deleting_an_account_removes_all_its_data() {
assert!(v["report"]["guilds_transferred"] == 1 && v["report"]["removed"]["stats"].as_u64().unwrap() > 0, "{v}");
// Nothing refers to them any more (their name appears only as plain text in free-form rows we don't keep).
assert_eq!(mentions(&t, &steve_uuid).await, Vec::<String>::new());
for table in ["users", "user_levels", "player_stats", "friendships", "direct_messages", "user_profiles", "user_posts", "server_market", "server_economy", "reserved_usernames"] {
assert_eq!(mentions(&t, &steve_uuid).await, vec!["reserved_usernames.uuid".to_string()]);
for table in ["users", "user_levels", "player_stats", "friendships", "direct_messages", "user_profiles", "user_posts", "server_market", "server_economy"] {
let n: i64 = sqlx::query_scalar(&format!("SELECT COUNT(*) FROM {table} WHERE CAST(uuid AS TEXT) = ?")).bind(&steve_uuid).fetch_one(&t.db).await.unwrap_or(0);
assert_eq!(n, 0, "{table}");
}
@@ -166,9 +166,10 @@ async fn deleting_an_account_removes_all_its_data() {
assert!(friends.as_array().unwrap().is_empty());
let (_, list) = t.call("GET", "/api/v1/members/search", Some(&alex), None).await;
assert!(list.as_array().unwrap().iter().all(|m| m["username"] != "Steve"));
// The old name can be taken again, and the deleted token no longer works.
// The deleted token no longer works, and the name stays reserved against impersonation.
let (s, _) = t.call("GET", "/api/v1/auth/me", Some(&steve), None).await;
assert_eq!(s, StatusCode::UNAUTHORIZED);
player(&t, &admin, "Steve").await;
let (s, _) = t.call("POST", "/api/admin/users", Some(&admin), Some(json!({"username": "Steve", "password": "password123"}))).await;
assert_eq!(s, StatusCode::CONFLICT);
assert_ne!(mia_uuid, steve_uuid);
}
+1 -1
View File
@@ -254,7 +254,7 @@
</Modal>
<Modal bind:open={confirmOpen} title="Delete {confirm?.username}?">
<p class="muted">This removes the account <strong>and all of its data</strong>: levels, XP, quests, achievements, stats, friends, messages, profile and posts, economy balance and market listings, and guild membership. Guilds they lead pass to the next member (or are dissolved if they're alone). Their name becomes available again. This can't be undone.</p>
<p class="muted">This removes the account <strong>and all of its data</strong>: levels, XP, quests, achievements, stats, friends, messages, profile and posts, economy balance and market listings, and guild membership. Guilds they lead pass to the next member (or are dissolved if they're alone). Their name stays reserved so nobody can impersonate them. This can't be undone.</p>
{#snippet footer()}
<button class="ghost" onclick={() => (confirmOpen = false)}>Cancel</button>
<button class="danger" onclick={removeUser}><Trash2 size={16} /> Delete</button>