diff --git a/crates/lib/mysql_queries/src/queries/voice_designer/delete_dataset.rs b/crates/lib/mysql_queries/src/queries/voice_designer/delete_dataset.rs new file mode 100644 index 0000000000..d777e16a9e --- /dev/null +++ b/crates/lib/mysql_queries/src/queries/voice_designer/delete_dataset.rs @@ -0,0 +1,89 @@ +use sqlx::MySqlPool; + +use errors::AnyhowResult; + +pub async fn delete_dataset_as_user( + dataset_token: &str, + mysql_pool: &MySqlPool +) -> AnyhowResult<()> { + let _r = sqlx::query!( + r#" +UPDATE zs_voice_datasets +SET + user_deleted_at = CURRENT_TIMESTAMP +WHERE + token = ? +LIMIT 1 + "#, + dataset_token, + ) + .execute(mysql_pool) + .await?; + Ok(()) +} + +pub async fn delete_dataset_as_mod( + dataset_token: &str, + mod_user_token: &str, + mysql_pool: &MySqlPool +) -> AnyhowResult<()> { + let _r = sqlx::query!( + r#" +UPDATE zs_voice_datasets +SET + mod_deleted_at = CURRENT_TIMESTAMP, + maybe_mod_user_token = ? +WHERE + token = ? +LIMIT 1 + "#, + mod_user_token, + dataset_token, + ) + .execute(mysql_pool) + .await?; + Ok(()) +} + +pub async fn undelete_dataset_as_user( + dataset_token: &str, + mysql_pool: &MySqlPool +) -> AnyhowResult<()> { + let _r = sqlx::query!( + r#" +UPDATE zs_voice_datasets +SET + user_deleted_at = NULL +WHERE + token = ? +LIMIT 1 + "#, + dataset_token, + ) + .execute(mysql_pool) + .await?; + Ok(()) +} + +pub async fn undelete_dataset_as_mod( + dataset_token: &str, + mod_user_token: &str, + mysql_pool: &MySqlPool +) -> AnyhowResult<()> { + let _r = sqlx::query!( + r#" +UPDATE zs_voice_datasets +SET + mod_deleted_at = NULL, + maybe_mod_user_token = ? +WHERE + token = ? +LIMIT 1 + "#, + mod_user_token, + dataset_token, + ) + .execute(mysql_pool) + .await?; + Ok(()) +} diff --git a/crates/lib/mysql_queries/src/queries/voice_designer/get_dataset.rs b/crates/lib/mysql_queries/src/queries/voice_designer/get_dataset.rs index 0587b5848e..8daaa8a121 100644 --- a/crates/lib/mysql_queries/src/queries/voice_designer/get_dataset.rs +++ b/crates/lib/mysql_queries/src/queries/voice_designer/get_dataset.rs @@ -21,38 +21,34 @@ pub struct ZsDataset { pub async fn get_dataset_by_token( dataset_token: &ZsDatasetToken, + can_see_deleted: bool, mysql_pool: &MySqlPool, ) -> AnyhowResult> { let mut connection = mysql_pool.acquire().await?; get_dataset_by_token_with_connection( dataset_token, + can_see_deleted, &mut connection ).await } pub async fn get_dataset_by_token_with_connection( dataset_token: &ZsDatasetToken, + can_see_deleted: bool, mysql_connection: &mut PoolConnection, ) -> AnyhowResult> { - let maybe_result = sqlx::query_as!( - RawDataset, - r#" - SELECT - zd.token as `token: tokens::tokens::dataset::ZsDatasetToken`, - zd.title, - zd.ietf_language_tag, - zd.ietf_primary_language_subtag, - zd.maybe_creator_user_token, - zd.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility` - FROM zs_voice_datasets as zd - WHERE - zd.token = ? - AND zd.user_deleted_at IS NULL - AND zd.mod_deleted_at IS NULL - "#, - dataset_token.as_str() - ) - .fetch_one(mysql_connection).await; + + let maybe_result = if can_see_deleted { + select_include_deleted( + dataset_token, + mysql_connection + ).await + } else { + select_without_deleted( + dataset_token, + mysql_connection + ).await + }; let record = match maybe_result { Ok(record) => record, @@ -79,6 +75,54 @@ pub async fn get_dataset_by_token_with_connection( maybe_creator_user_token: record.maybe_creator_user_token, })) } + +async fn select_include_deleted( + dataset_token: &ZsDatasetToken, + mysql_connection: &mut PoolConnection, +) -> Result { + sqlx::query_as!( + RawDataset, + r#" + SELECT + zd.token as `token: tokens::tokens::dataset::ZsDatasetToken`, + zd.title, + zd.ietf_language_tag, + zd.ietf_primary_language_subtag, + zd.maybe_creator_user_token, + zd.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility` + FROM zs_voice_datasets as zd + WHERE + zd.token = ? + "#, + dataset_token.as_str() + ) + .fetch_one(mysql_connection).await +} + +async fn select_without_deleted( + dataset_token: &ZsDatasetToken, + mysql_connection: &mut PoolConnection, +) -> Result { + sqlx::query_as!( + RawDataset, + r#" + SELECT + zd.token as `token: tokens::tokens::dataset::ZsDatasetToken`, + zd.title, + zd.ietf_language_tag, + zd.ietf_primary_language_subtag, + zd.maybe_creator_user_token, + zd.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility` + FROM zs_voice_datasets as zd + WHERE + zd.token = ? + AND zd.user_deleted_at IS NULL + AND zd.mod_deleted_at IS NULL + "#, + dataset_token.as_str() + ) + .fetch_one(mysql_connection).await +} #[derive(Serialize)] pub struct RawDataset { token: ZsDatasetToken, diff --git a/crates/lib/mysql_queries/src/queries/voice_designer/mod.rs b/crates/lib/mysql_queries/src/queries/voice_designer/mod.rs index 03143d407d..f437edd8c6 100644 --- a/crates/lib/mysql_queries/src/queries/voice_designer/mod.rs +++ b/crates/lib/mysql_queries/src/queries/voice_designer/mod.rs @@ -1,3 +1,4 @@ pub mod create_dataset; pub mod update_dataset; -pub mod get_dataset; \ No newline at end of file +pub mod get_dataset; +pub mod delete_dataset; \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/delete_dataset.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/delete_dataset.rs index 495806e32b..46e04f9c74 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/delete_dataset.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/delete_dataset.rs @@ -6,14 +6,175 @@ use actix_web::error::ResponseError; use actix_web::http::StatusCode; use actix_web::web::Path; use log::{error, log, warn}; +use enums::by_table::audit_logs::audit_log_entity_action::AuditLogEntityAction::Delete; use http_server_common::request::get_request_ip::get_request_ip; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; +use mysql_queries::queries::voice_designer::delete_dataset::{delete_dataset_as_mod, delete_dataset_as_user, undelete_dataset_as_mod, undelete_dataset_as_user}; +use mysql_queries::queries::voice_designer::get_dataset::get_dataset_by_token; +use tokens::tokens::dataset::ZsDatasetToken; +use crate::http_server::web_utils::response_success_helpers::simple_json_success; +use crate::server_state::ServerState; +use crate::util::delete_role_disambiguation::{delete_role_disambiguation, DeleteRole}; + +#[derive(Deserialize)] +pub struct DeleteDatasetRequest { + set_delete: bool, + /// NB: this is only to disambiguate when a user is both a mod and an author. + as_mod: Option, +} + +/// For the URL PathInfo +#[derive(Deserialize)] +pub struct DeleteDatasetPathInfo { + dataset_token: String, +} + +// =============== Error Response =============== + +#[derive(Debug, Serialize)] +pub enum DeleteDatasetError { + BadInput(String), + NotFound, + NotAuthorized, + ServerError, +} + +impl ResponseError for DeleteDatasetError { + fn status_code(&self) -> StatusCode { + match *self { + DeleteDatasetError::BadInput(_) => StatusCode::BAD_REQUEST, + DeleteDatasetError::NotFound => StatusCode::NOT_FOUND, + DeleteDatasetError::NotAuthorized => StatusCode::UNAUTHORIZED, + DeleteDatasetError::ServerError => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> HttpResponse { + serialize_as_json_error(self) + } +} + +// NB: Not using derive_more::Display since Clion doesn't understand it. +impl fmt::Display for DeleteDatasetError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +// =============== Handler =============== + +pub async fn delete_dataset_handler( + http_request: HttpRequest, + path: Path, + request: web::Json, + server_state: web::Data> +) -> Result{ + let maybe_user_session = server_state + .session_checker + .maybe_get_user_session(&http_request, &server_state.mysql_pool) + .await + .map_err(|e| { + warn!("Session checker error: {:?}", e); + DeleteDatasetError::ServerError + })?; + + let user_session = match maybe_user_session { + Some(session) => session, + None => { + warn!("not logged in"); + return Err(DeleteDatasetError::NotAuthorized); + } + }; + + let dataset_token = path.dataset_token.clone(); + let is_mod = user_session.can_ban_users; + + let dataset_lookup_result = get_dataset_by_token( + &ZsDatasetToken::new(dataset_token.clone()), + is_mod, + &server_state.mysql_pool, + ).await; + + let dataset = match dataset_lookup_result { + Ok(Some(dataset)) => dataset, + Ok(None) => { + warn!("Dataset not found: {:?}", dataset_token); + return Err(DeleteDatasetError::NotFound); + }, + Err(err) => { + warn!("Error looking up dataset: {:?}", err); + return Err(DeleteDatasetError::ServerError); + } + }; + + //TODO(kasisnu): Confirm who can delete/undelete and cleanup this logic + let is_creator = dataset.maybe_creator_user_token.as_deref() + .map(|creator_user_token| creator_user_token == &user_session.user_token) + .unwrap_or(false); + + if !is_creator && !is_mod { + warn!("user is not allowed to delete this dataset: {}", user_session.user_token); + return Err(DeleteDatasetError::NotAuthorized); + } + + let delete_role = delete_role_disambiguation(is_mod, is_creator, request.as_mod); + + let query_result = if request.set_delete { + match delete_role { + DeleteRole::ErrorDoNotDelete => { + warn!("user is not allowed to delete datasets: {}", user_session.user_token); + return Err(DeleteDatasetError::NotAuthorized); + } + DeleteRole::AsUser => { + delete_dataset_as_user( + &path.dataset_token, + &server_state.mysql_pool + ).await + } + DeleteRole::AsMod => { + delete_dataset_as_mod( + &path.dataset_token, + &user_session.user_token, + &server_state.mysql_pool + ).await + } + } + } else { + match delete_role { + DeleteRole::ErrorDoNotDelete => { + warn!("user is not allowed to undelete inference results: {}", user_session.user_token); + return Err(DeleteDatasetError::NotAuthorized); + } + DeleteRole::AsUser => { + // NB: Technically only mods can see their own datasets + //TODO(kasisnu): Is this a case that can apply to datasets? + undelete_dataset_as_user( + &path.dataset_token, + &server_state.mysql_pool + ).await + } + DeleteRole::AsMod => { + undelete_dataset_as_mod( + &path.dataset_token, + &user_session.user_token, + &server_state.mysql_pool + ).await + } + } + }; + + match query_result { + Ok(_) => {}, + Err(err) => { + warn!("Update dataset mod approval status DB error: {:?}", err); + return Err(DeleteDatasetError::ServerError); + } + }; + + Ok(simple_json_success()) -pub async fn delete_dataset(dataset_token: web::Path) -> HttpResponse { - // Implementation for deleting a dataset (soft delete) - HttpResponse::Ok().json(format!("Dataset {} deleted (soft delete)", dataset_token)) } \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/update_dataset.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/update_dataset.rs index 4462d971e5..353dfbeb00 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/update_dataset.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/update_dataset.rs @@ -9,6 +9,7 @@ use log::{error, log, warn}; use enums::common::visibility::Visibility; use http_server_common::request::get_request_ip::get_request_ip; +use http_server_common::response::response_success_helpers::simple_json_success; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; use mysql_queries::queries::voice_designer::create_dataset::{create_dataset, CreateDatasetArgs}; use mysql_queries::queries::voice_designer::get_dataset::get_dataset_by_token; @@ -97,8 +98,11 @@ pub async fn update_dataset_handler( }; let dataset_token = path.dataset_token.clone(); + let is_mod = user_session.can_ban_users; + let dataset_lookup_result = get_dataset_by_token( &ZsDatasetToken::new(dataset_token.clone()), + is_mod, &server_state.mysql_pool, ).await; @@ -114,8 +118,10 @@ pub async fn update_dataset_handler( } }; - let is_creator = dataset.maybe_creator_user_token == Some(user_session.user_token); - let is_mod = user_session.can_ban_users; + // let is_creator = dataset.maybe_creator_user_token == Some(user_session.user_token); + let is_creator = dataset.maybe_creator_user_token.as_deref() + .map(|creator_user_token| creator_user_token == &user_session.user_token) + .unwrap_or(false); if !is_creator && !is_mod { warn!("user is not allowed to edit this dataset: {}", user_session.user_token); @@ -192,14 +198,5 @@ pub async fn update_dataset_handler( } }; - let response = UpdateDatasetResponse { - success: true, - }; - - let body = serde_json::to_string(&response) - .map_err(|e| UpdateDatasetError::ServerError)?; - - Ok(HttpResponse::Ok() - .content_type("application/json") - .body(body)) + Ok(simple_json_success()) } diff --git a/crates/service/web/storyteller_web/src/routes.rs b/crates/service/web/storyteller_web/src/routes.rs index fe0ba78f04..6e74669196 100644 --- a/crates/service/web/storyteller_web/src/routes.rs +++ b/crates/service/web/storyteller_web/src/routes.rs @@ -122,7 +122,7 @@ use crate::http_server::endpoints::w2l::set_w2l_template_mod_approval::set_w2l_t use crate::http_server::endpoints::voice_designer::create_dataset::create_dataset_handler; use crate::http_server::endpoints::voice_designer::update_dataset::update_dataset_handler; -use crate::http_server::endpoints::voice_designer::delete_dataset::delete_dataset; +use crate::http_server::endpoints::voice_designer::delete_dataset::delete_dataset_handler; use crate::http_server::endpoints::voice_designer::list_datasets_by_user::list_datasets_by_user; use crate::http_server::endpoints::voice_designer::create_voice::create_voice; @@ -1145,7 +1145,7 @@ fn add_voice_designer_routes (app:App)-> App .route(web::post().to(update_dataset_handler)) .route(web::head().to(|| HttpResponse::Ok())) ) - .route("/{dataset_token}/delete", web::delete().to(delete_dataset)) + .route("/{dataset_token}/delete", web::delete().to(delete_dataset_handler)) .route("/user/{user_token}/list", web::get().to(list_datasets_by_user)) ) .service(