delete handler + queries

This commit is contained in:
Brandon Thomas committed 2026-04-02 22:44:15 -04:00
1 parent 3180252a06
commit 0ffdd17a1f
4 files changed
+129 -6

No files matched your search

@@ -0,0 +1,26 @@
use sqlx::pool::PoolConnection;
use sqlx::MySql;
use tokens::tokens::characters::CharacterToken;
/// Soft-delete a character by setting deleted_at to NOW().
///
/// This is idempotent — calling it on an already-deleted character is a no-op.
pub async fn delete_character(
character_token: &CharacterToken,
connection: &mut PoolConnection<MySql>,
) -> Result<(), sqlx::Error> {
sqlx::query(
r#"
UPDATE characters
SET deleted_at = NOW()
WHERE token = ?
AND deleted_at IS NULL
"#,
)
.bind(character_token.as_str())
.execute(&mut **connection)
.await?;
Ok(())
}
@@ -0,0 +1,39 @@
use sqlx::pool::PoolConnection;
use sqlx::{FromRow, MySql};
use tokens::tokens::characters::CharacterToken;
use tokens::tokens::users::UserToken;
/// Minimal character record for ownership and deletion checks.
#[derive(Debug, FromRow)]
pub struct CharacterOwnershipRecord {
pub token: CharacterToken,
pub maybe_creator_user_token: Option<UserToken>,
pub is_active: bool,
pub is_deleted: bool,
}
/// Look up a character by token, including soft-deleted records.
///
/// Returns `None` if the character does not exist at all.
pub async fn get_character_by_token_including_deleted(
character_token: &CharacterToken,
connection: &mut PoolConnection<MySql>,
) -> Result<Option<CharacterOwnershipRecord>, sqlx::Error> {
let result = sqlx::query_as::<_, CharacterOwnershipRecord>(
r#"
SELECT
token,
maybe_creator_user_token,
is_active,
(deleted_at IS NOT NULL) as is_deleted
FROM characters
WHERE token = ?
"#,
)
.bind(character_token.as_str())
.fetch_optional(&mut **connection)
.await?;
Ok(result)
}
@@ -1,2 +1,4 @@
pub mod batch_lookup_characters_by_token_for_prompting;
pub mod create_pending_character;
pub mod delete_character;
pub mod get_character_by_token_including_deleted;
@@ -2,11 +2,15 @@ use std::sync::Arc;
use actix_web::web::{Json, Path};
use actix_web::{web, HttpRequest};
use log::{info, warn};
use artcraft_api_defs::characters::delete_character::{DeleteCharacterPathInfo, DeleteCharacterResponse};
use tokens::tokens::characters::CharacterToken;
use mysql_queries::queries::characters::delete_character::delete_character;
use mysql_queries::queries::characters::get_character_by_token_including_deleted::get_character_by_token_including_deleted;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
use crate::state::server_state::ServerState;
/// Delete a character.
@@ -25,9 +29,61 @@ use crate::state::server_state::ServerState;
)
)]
pub async fn delete_character_handler(
_http_request: HttpRequest,
_path: Path<DeleteCharacterPathInfo>,
_server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<DeleteCharacterResponse>, CommonWebError> {
todo!()
http_request: HttpRequest,
path: Path<DeleteCharacterPathInfo>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<DeleteCharacterResponse>, AdvancedCommonWebError> {
let character_token = &path.character_token;
// --- Auth ---
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
let user_session = require_user_session_using_connection(
&http_request,
&server_state.session_checker,
&mut mysql_connection,
).await?;
let user_token = &user_session.user_token;
let is_mod = user_session.is_mod();
// --- Look up the character (including deleted) ---
let character = get_character_by_token_including_deleted(character_token, &mut mysql_connection)
.await?
.ok_or_else(|| {
warn!("Character not found: {}", character_token);
AdvancedCommonWebError::NotFound
})?;
// Already deleted — return success idempotently.
if character.is_deleted {
info!("Character {} is already deleted", character_token);
return Ok(Json(DeleteCharacterResponse { success: true }));
}
// --- Ownership check ---
let is_owner = character.maybe_creator_user_token
.as_ref()
.map(|owner| owner == user_token)
.unwrap_or(false);
if !is_owner && !is_mod {
warn!(
"User {} attempted to delete character {} owned by {:?}",
user_token, character_token, character.maybe_creator_user_token,
);
return Err(AdvancedCommonWebError::NotAuthorized);
}
// --- Soft delete ---
delete_character(character_token, &mut mysql_connection).await?;
info!("Character {} deleted by user {}", character_token, user_token);
Ok(Json(DeleteCharacterResponse { success: true }))
}