feat(server): user account deletion

This commit is contained in:
Henri Bourcereau 2026-05-25 17:12:23 +02:00
parent 6fd3499d7b
commit 20b8353cfb
9 changed files with 252 additions and 6 deletions

View file

@ -188,6 +188,25 @@ pub async fn update_password_hash(pool: &Pool, user_id: i64, hash: &str) -> Resu
Ok(())
}
/// Permanently deletes a user and their auth data.
/// Game history rows are kept but de-associated (user_id set to NULL).
pub async fn delete_user(pool: &Pool, user_id: i64) -> Result<(), DbError> {
let client = pool.get().await?;
client
.execute(
"UPDATE game_participants SET user_id = NULL WHERE user_id = $1",
&[&user_id],
)
.await?;
client
.execute("DELETE FROM email_tokens WHERE user_id = $1", &[&user_id])
.await?;
client
.execute("DELETE FROM users WHERE id = $1", &[&user_id])
.await?;
Ok(())
}
// ── Email tokens ──────────────────────────────────────────────────────────────
pub async fn create_email_token(

View file

@ -19,7 +19,7 @@ use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::{IntoResponse, Response},
routing::{get, post},
routing::{delete, get, post},
};
use axum_login::AuthSession;
use rand::distributions::Alphanumeric;
@ -48,6 +48,7 @@ pub fn router() -> Router<Arc<AppState>> {
.route("/auth/resend-verification", post(resend_verification))
.route("/auth/forgot-password", post(forgot_password))
.route("/auth/reset-password", post(reset_password))
.route("/auth/account", delete(delete_account))
.route("/users/{username}", get(user_profile))
.route("/users/{username}/games", get(user_games))
.route("/games/result", post(game_result))
@ -286,6 +287,16 @@ async fn logout(mut auth_session: AuthSession<AuthBackend>) -> Result<StatusCode
Ok(StatusCode::NO_CONTENT)
}
async fn delete_account(
mut auth_session: AuthSession<AuthBackend>,
State(state): State<Arc<AppState>>,
) -> Result<StatusCode, AppError> {
let user = auth_session.user.clone().ok_or(AppError::Unauthorized)?;
auth_session.logout().await.map_err(|_| AppError::Internal)?;
db::delete_user(&state.db, user.id).await?;
Ok(StatusCode::NO_CONTENT)
}
async fn me(auth_session: AuthSession<AuthBackend>) -> Result<impl IntoResponse, AppError> {
match auth_session.user {
Some(user) => Ok(Json(MeResponse {

View file

@ -87,7 +87,7 @@ async fn main() {
.allow_origin(AllowOrigin::list([
"http://localhost:9091".parse().unwrap(), // unified web dev server
]))
.allow_methods([Method::GET, Method::POST, Method::OPTIONS])
.allow_methods([Method::GET, Method::POST, Method::DELETE, Method::OPTIONS])
.allow_headers([
HeaderName::from_static("content-type"),
HeaderName::from_static("cookie"),