Fix Friends & Social member search; purge all account data on deletion
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DjMbLQujBHunCCu5GpsHaT
This commit is contained in:
10 files changed
+430
-43
No files matched your search
@@ -227,8 +227,15 @@ pub async fn delete_user(AdminUser(me): AdminUser, State(state): State<AppState>
|
||||
if me.id == id {
|
||||
return Err(AppError::bad_request("you can't delete your own account"));
|
||||
}
|
||||
sqlx::query("DELETE FROM users WHERE id = ?").bind(id).execute(&state.db).await?;
|
||||
Ok(Json(json!({ "ok": true })))
|
||||
let user: UserRow = sqlx::query_as("SELECT * FROM users WHERE id = ?")
|
||||
.bind(id)
|
||||
.fetch_optional(&state.db)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("player not found"))?;
|
||||
// Removes the account and everything tied to its UUID, not just the login.
|
||||
let report = crate::purge::purge_user(&state, &user).await?;
|
||||
crate::routes::activity::record(&state, &me, "panel", "account_deleted", Some(&format!("{} ({} records removed)", user.username, report.total()))).await?;
|
||||
Ok(Json(json!({ "ok": true, "report": report })))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, sqlx::FromRow)]
|
||||
|
||||
@@ -304,6 +304,20 @@ pub async fn send_direct_message(
|
||||
return Err(AppError::bad_request("Message cannot be empty"));
|
||||
}
|
||||
|
||||
if target_uuid == auth.uuid {
|
||||
return Err(AppError::bad_request("You can't message yourself"));
|
||||
}
|
||||
let exists: bool = sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM users WHERE uuid = ? AND status = 'active')")
|
||||
.bind(&target_uuid)
|
||||
.fetch_one(&state.db)
|
||||
.await?;
|
||||
if !exists {
|
||||
return Err(AppError::not_found("Player not found"));
|
||||
}
|
||||
if content.chars().count() > 1000 {
|
||||
return Err(AppError::bad_request("Messages can be up to 1000 characters"));
|
||||
}
|
||||
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
let id: i64 = sqlx::query_scalar(
|
||||
@@ -400,6 +414,24 @@ pub async fn send_game_invite(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<SendInvitePayload>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
let friends: bool = sqlx::query_scalar(
|
||||
"SELECT EXISTS(SELECT 1 FROM friendships WHERE status = 'accepted'
|
||||
AND ((user_uuid = ?1 AND friend_uuid = ?2) OR (user_uuid = ?2 AND friend_uuid = ?1)))",
|
||||
)
|
||||
.bind(&auth.uuid)
|
||||
.bind(&payload.recipient_uuid)
|
||||
.fetch_one(&state.db)
|
||||
.await?;
|
||||
if !friends {
|
||||
return Err(AppError::forbidden("You can only invite friends"));
|
||||
}
|
||||
let instance: bool = sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM instances WHERE id = ?)")
|
||||
.bind(&payload.instance_id)
|
||||
.fetch_one(&state.db)
|
||||
.await?;
|
||||
if !instance {
|
||||
return Err(AppError::not_found("Instance not found"));
|
||||
}
|
||||
let invite_id = format!("inv_{}", uuid::Uuid::new_v4().simple());
|
||||
let now = chrono::Utc::now();
|
||||
let expires = now + chrono::Duration::minutes(30);
|
||||
@@ -700,54 +732,54 @@ pub async fn search_members(
|
||||
Query(query): Query<SearchMembersQuery>,
|
||||
State(state): State<AppState>,
|
||||
) -> AppResult<Json<Vec<scopenet_shared::MemberProfile>>> {
|
||||
let q = query.q.unwrap_or_default().trim().to_lowercase();
|
||||
// `%` and `_` are LIKE wildcards; players shouldn't be able to inject them.
|
||||
let q = query.q.unwrap_or_default().trim().to_lowercase().replace(['%', '_', '\\'], "");
|
||||
let pattern = format!("%{q}%");
|
||||
|
||||
let rows: Vec<(String, String, String, String, Option<String>, Option<i64>, Option<String>, i64, Option<String>, Option<String>)> = sqlx::query_as(
|
||||
"SELECT u.uuid, u.username, u.role, u.status, u.skin_url,
|
||||
ul.global_level, ul.title, u.playtime_secs, u.last_seen_ingame,
|
||||
f.status as friendship_status
|
||||
FROM users u
|
||||
LEFT JOIN user_levels ul ON ul.uuid = u.uuid
|
||||
LEFT JOIN friendships f ON (f.user_uuid = ? AND f.friend_uuid = u.uuid)
|
||||
WHERE (? = '' OR LOWER(u.username) LIKE ?)
|
||||
ORDER BY (CASE WHEN LOWER(u.username) = ? THEN 1 ELSE 2 END), u.playtime_secs DESC
|
||||
LIMIT 60",
|
||||
)
|
||||
.bind(&auth.uuid)
|
||||
.bind(&q)
|
||||
.bind(&pattern)
|
||||
.bind(&q)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
|
||||
let now_ts = chrono::Utc::now().timestamp();
|
||||
let rows: Vec<(String, String, String, String, Option<String>, Option<i64>, Option<String>, i64, Option<String>, bool, Option<String>, Option<String>)> =
|
||||
sqlx::query_as(
|
||||
"SELECT u.uuid, u.username, u.role, u.status, u.skin_hash,
|
||||
ul.global_level, ul.title,
|
||||
COALESCE((SELECT SUM(ps.playtime_secs) FROM player_stats ps WHERE ps.uuid = u.uuid), 0),
|
||||
(SELECT MAX(ps.last_seen) FROM player_stats ps WHERE ps.uuid = u.uuid),
|
||||
EXISTS(SELECT 1 FROM server_online so WHERE so.uuid = u.uuid),
|
||||
f.status, f.action_uuid
|
||||
FROM users u
|
||||
LEFT JOIN user_levels ul ON ul.uuid = u.uuid
|
||||
LEFT JOIN friendships f ON (f.user_uuid = ?1 AND f.friend_uuid = u.uuid) OR (f.friend_uuid = ?1 AND f.user_uuid = u.uuid)
|
||||
WHERE u.status = 'active' AND u.uuid <> ?1 AND (?2 = '' OR LOWER(u.username) LIKE ?3)
|
||||
ORDER BY (CASE WHEN LOWER(u.username) = ?2 THEN 0 ELSE 1 END), 10 DESC, u.username COLLATE NOCASE
|
||||
LIMIT 60",
|
||||
)
|
||||
.bind(&auth.uuid)
|
||||
.bind(&q)
|
||||
.bind(&pattern)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
|
||||
let list = rows
|
||||
.into_iter()
|
||||
.map(|(uuid, username, role, status, skin_url, glvl, title, playtime, last_seen, friend_status)| {
|
||||
let online = if let Some(ref ls) = last_seen {
|
||||
if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(ls) {
|
||||
(now_ts - dt.timestamp()).abs() < 180
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let is_friend = friend_status.as_deref() == Some("accepted");
|
||||
.map(|(uuid, username, role, status, skin_hash, glvl, title, playtime, last_seen, online, friend_status, action)| {
|
||||
let friendship_status = match (friend_status.as_deref(), action.as_deref()) {
|
||||
(Some("accepted"), _) => "accepted",
|
||||
(Some(_), Some(a)) if a == auth.uuid => "pending_outgoing",
|
||||
(Some(_), _) => "pending_incoming",
|
||||
_ => "none",
|
||||
}
|
||||
.to_string();
|
||||
scopenet_shared::MemberProfile {
|
||||
uuid,
|
||||
username,
|
||||
role,
|
||||
status,
|
||||
skin_url,
|
||||
skin_url: skin_hash.map(|h| format!("/textures/{h}")),
|
||||
global_level: glvl.unwrap_or(1),
|
||||
title,
|
||||
playtime_secs: playtime,
|
||||
last_seen,
|
||||
online,
|
||||
is_friend,
|
||||
is_friend: friendship_status == "accepted",
|
||||
friendship_status,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Reference in new issue
Block a user