mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-10 12:01:36 +00:00
delete handler + queries
This commit is contained in:
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(())
|
||||
}
|
||||
+39
@@ -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;
|
||||
+62
-6
@@ -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 }))
|
||||
}
|
||||
Reference in new issue
Block a user