diff --git a/.sqlx/query-391803c24282df23f933f1f3f8db9350a4cc7be50b3c07f267d795f736958527.json b/.sqlx/query-391803c24282df23f933f1f3f8db9350a4cc7be50b3c07f267d795f736958527.json new file mode 100644 index 0000000000..97b8c28b11 --- /dev/null +++ b/.sqlx/query-391803c24282df23f933f1f3f8db9350a4cc7be50b3c07f267d795f736958527.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "\nUPDATE media_files\nSET\n mod_deleted_at = CURRENT_TIMESTAMP,\n\n maybe_mod_user_token = ?\nWHERE\n token = ?\nLIMIT 1\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "391803c24282df23f933f1f3f8db9350a4cc7be50b3c07f267d795f736958527" +} diff --git a/.sqlx/query-80c27030645e34e7583bcf80ae025d82dfdad709346d1c6c1c8480a6c1a8f9c2.json b/.sqlx/query-80c27030645e34e7583bcf80ae025d82dfdad709346d1c6c1c8480a6c1a8f9c2.json new file mode 100644 index 0000000000..879a749ddf --- /dev/null +++ b/.sqlx/query-80c27030645e34e7583bcf80ae025d82dfdad709346d1c6c1c8480a6c1a8f9c2.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "\nUPDATE media_files\nSET\n user_deleted_at = NULL\nWHERE\n token = ?\nLIMIT 1\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "80c27030645e34e7583bcf80ae025d82dfdad709346d1c6c1c8480a6c1a8f9c2" +} diff --git a/.sqlx/query-a982f37b9c2d0785dfaced73e82a799a071566a0b6255ab6365e12cd1c4ff9b3.json b/.sqlx/query-a982f37b9c2d0785dfaced73e82a799a071566a0b6255ab6365e12cd1c4ff9b3.json new file mode 100644 index 0000000000..fcc617c51f --- /dev/null +++ b/.sqlx/query-a982f37b9c2d0785dfaced73e82a799a071566a0b6255ab6365e12cd1c4ff9b3.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "\n UPDATE media_files\n SET\n maybe_creator_user_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n maybe_mod_user_token = ?\n\n WHERE token = ?\n LIMIT 1\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 5 + }, + "nullable": [] + }, + "hash": "a982f37b9c2d0785dfaced73e82a799a071566a0b6255ab6365e12cd1c4ff9b3" +} diff --git a/.sqlx/query-ce033bdb289139aaed672a76b9532c59f37499a76fc8d64280b53b75e866a37b.json b/.sqlx/query-ce033bdb289139aaed672a76b9532c59f37499a76fc8d64280b53b75e866a37b.json new file mode 100644 index 0000000000..ddd4ea0ec8 --- /dev/null +++ b/.sqlx/query-ce033bdb289139aaed672a76b9532c59f37499a76fc8d64280b53b75e866a37b.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "\nUPDATE media_files\nSET\n mod_deleted_at = NULL,\n maybe_mod_user_token = ?\nWHERE\n token = ?\nLIMIT 1\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "ce033bdb289139aaed672a76b9532c59f37499a76fc8d64280b53b75e866a37b" +} diff --git a/.sqlx/query-f4870b971eabdfc78e77d1279270d32d7980e1997622d38dbf4e108475a55cd2.json b/.sqlx/query-f4870b971eabdfc78e77d1279270d32d7980e1997622d38dbf4e108475a55cd2.json new file mode 100644 index 0000000000..50072fd364 --- /dev/null +++ b/.sqlx/query-f4870b971eabdfc78e77d1279270d32d7980e1997622d38dbf4e108475a55cd2.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "\nUPDATE media_files\nSET\n user_deleted_at = CURRENT_TIMESTAMP\nWHERE\n token = ?\nLIMIT 1\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "f4870b971eabdfc78e77d1279270d32d7980e1997622d38dbf4e108475a55cd2" +} diff --git a/crates/lib/mysql_queries/src/queries/media_files/delete_media_file.rs b/crates/lib/mysql_queries/src/queries/media_files/delete_media_file.rs new file mode 100644 index 0000000000..fee2758c92 --- /dev/null +++ b/crates/lib/mysql_queries/src/queries/media_files/delete_media_file.rs @@ -0,0 +1,91 @@ +use sqlx::MySqlPool; + +use errors::AnyhowResult; +use tokens::tokens::media_files::MediaFileToken; + +pub async fn delete_media_file_as_user( + media_file_token: &MediaFileToken, + mysql_pool: &MySqlPool +) -> AnyhowResult<()> { + let _r = sqlx::query!( + r#" +UPDATE media_files +SET + user_deleted_at = CURRENT_TIMESTAMP +WHERE + token = ? +LIMIT 1 + "#, + media_file_token, + ) + .execute(mysql_pool) + .await?; + Ok(()) +} + +pub async fn delete_media_file_as_mod( + media_file_token: &MediaFileToken, + mod_user_token: &str, + mysql_pool: &MySqlPool +) -> AnyhowResult<()> { + let _r = sqlx::query!( + r#" +UPDATE media_files +SET + mod_deleted_at = CURRENT_TIMESTAMP, + + maybe_mod_user_token = ? +WHERE + token = ? +LIMIT 1 + "#, + mod_user_token, + media_file_token, + ) + .execute(mysql_pool) + .await?; + Ok(()) +} + +pub async fn undelete_media_file_as_user( + media_file_token: &MediaFileToken, + mysql_pool: &MySqlPool +) -> AnyhowResult<()> { + let _r = sqlx::query!( + r#" +UPDATE media_files +SET + user_deleted_at = NULL +WHERE + token = ? +LIMIT 1 + "#, + media_file_token, + ) + .execute(mysql_pool) + .await?; + Ok(()) +} + +pub async fn undelete_media_file_as_mod( + media_file_token: &MediaFileToken, + mod_user_token: &str, + mysql_pool: &MySqlPool +) -> AnyhowResult<()> { + let _r = sqlx::query!( + r#" +UPDATE media_files +SET + mod_deleted_at = NULL, + maybe_mod_user_token = ? +WHERE + token = ? +LIMIT 1 + "#, + mod_user_token, + media_file_token, + ) + .execute(mysql_pool) + .await?; + Ok(()) +} diff --git a/crates/lib/mysql_queries/src/queries/media_files/mod.rs b/crates/lib/mysql_queries/src/queries/media_files/mod.rs index 72f9e07feb..3225ced58e 100644 --- a/crates/lib/mysql_queries/src/queries/media_files/mod.rs +++ b/crates/lib/mysql_queries/src/queries/media_files/mod.rs @@ -3,3 +3,5 @@ pub mod insert_media_file_from_face_animation; pub mod insert_media_file_from_zero_shot_tts; pub mod insert_media_file_from_cli_tool; pub mod insert_media_file_from_voice_conversion; +pub mod delete_media_file; +pub mod update_media_file; diff --git a/crates/lib/mysql_queries/src/queries/media_files/update_media_file.rs b/crates/lib/mysql_queries/src/queries/media_files/update_media_file.rs new file mode 100644 index 0000000000..10ea2024c4 --- /dev/null +++ b/crates/lib/mysql_queries/src/queries/media_files/update_media_file.rs @@ -0,0 +1,39 @@ +use anyhow::anyhow; +use sqlx::MySqlPool; + +use enums::common::visibility::Visibility; +use errors::AnyhowResult; +use tokens::tokens::media_files::MediaFileToken; + +pub struct UpdateMediaFileArgs<'a> { + pub media_file_token: &'a MediaFileToken, + pub creator_set_visibility: &'a Visibility, + pub maybe_mod_user_token: Option<&'a str>, + pub mysql_pool: &'a MySqlPool +} + +pub async fn update_media_file(args: UpdateMediaFileArgs<'_>) -> AnyhowResult<()>{ + // TODO: enforce checks for idempotency token + let mut transaction = args.mysql_pool.begin().await?; + let query_result = sqlx::query!( + r#" + UPDATE media_files + SET + creator_set_visibility = ?, + maybe_mod_user_token = ? + WHERE token = ? + LIMIT 1 + "#, + args.creator_set_visibility.to_str(), + args.maybe_mod_user_token, + args.media_file_token.as_str(), + ).execute(args.mysql_pool).await; + + // TODO(Kasisnu): This should probably rollback + transaction.commit().await?; + + match query_result { + Ok(_) => Ok(()), + Err(err) => Err(anyhow!("media_file update error: {:?}", err)), + } +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/delete_media_file.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/delete_media_file.rs new file mode 100644 index 0000000000..0a94d091c4 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/delete_media_file.rs @@ -0,0 +1,171 @@ +use std::fmt; +use std::sync::Arc; + +use actix_web::{HttpRequest, HttpResponse, web}; +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::web::Path; +use log::warn; + +use http_server_common::response::serialize_as_json_error::serialize_as_json_error; +use mysql_queries::queries::media_files::delete_media_file::{delete_media_file_as_mod, delete_media_file_as_user, undelete_media_file_as_mod, undelete_media_file_as_user}; +use mysql_queries::queries::media_files::get_media_file::get_media_file; +use tokens::tokens::media_files::MediaFileToken; + +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 DeleteMediaFileRequest { + 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 DeleteMediaFilePathInfo { + token: MediaFileToken, +} + +// =============== Error Response =============== + +#[derive(Debug, Serialize)] +pub enum DeleteMediaFileError { + BadInput(String), + NotFound, + NotAuthorized, + ServerError, +} + +impl ResponseError for DeleteMediaFileError { + fn status_code(&self) -> StatusCode { + match *self { + DeleteMediaFileError::BadInput(_) => StatusCode::BAD_REQUEST, + DeleteMediaFileError::NotFound => StatusCode::NOT_FOUND, + DeleteMediaFileError::NotAuthorized => StatusCode::UNAUTHORIZED, + DeleteMediaFileError::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 DeleteMediaFileError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +// =============== Handler =============== + +pub async fn delete_media_file_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); + DeleteMediaFileError::ServerError + })?; + + let user_session = match maybe_user_session { + Some(session) => session, + None => { + warn!("not logged in"); + return Err(DeleteMediaFileError::NotAuthorized); + } + }; + + let is_mod = user_session.can_ban_users; + + let media_file_lookup_result = get_media_file( + &path.token, + is_mod, + &server_state.mysql_pool, + ).await; + + let media_file = match media_file_lookup_result { + Ok(Some(media_file)) => media_file, + Ok(None) => { + warn!("MediaFile not found: {:?}", path.token); + return Err(DeleteMediaFileError::NotFound); + }, + Err(err) => { + warn!("Error looking up media_file: {:?}", err); + return Err(DeleteMediaFileError::ServerError); + } + }; + + let is_creator = media_file.maybe_creator_user_token + .is_some_and(|t| t.as_str() == &user_session.user_token); + + if !is_creator && !is_mod { + warn!("user is not allowed to delete this media_file: {}", user_session.user_token); + return Err(DeleteMediaFileError::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 media_files: {}", user_session.user_token); + return Err(DeleteMediaFileError::NotAuthorized); + } + DeleteRole::AsUser => { + delete_media_file_as_user( + &path.token, + &server_state.mysql_pool + ).await + } + DeleteRole::AsMod => { + delete_media_file_as_mod( + &path.token, + &user_session.user_token, + &server_state.mysql_pool + ).await + } + } + } else { + match delete_role { + DeleteRole::ErrorDoNotDelete => { + warn!("user is not allowed to undelete voices: {}", user_session.user_token); + return Err(DeleteMediaFileError::NotAuthorized); + } + DeleteRole::AsUser => { + // NB: Technically only mods can see their own media_files + undelete_media_file_as_user( + &path.token, + &server_state.mysql_pool + ).await + } + DeleteRole::AsMod => { + undelete_media_file_as_mod( + &path.token, + &user_session.user_token, + &server_state.mysql_pool + ).await + } + } + }; + + match query_result { + Ok(_) => {}, + Err(err) => { + warn!("Update media_file mod approval status DB error: {:?}", err); + return Err(DeleteMediaFileError::ServerError); + } + }; + + Ok(simple_json_success()) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/mod.rs index a965ca7b81..014fa68854 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/mod.rs @@ -1,2 +1,3 @@ pub mod drain_multipart_request; pub mod get_media_file; +pub mod delete_media_file; diff --git a/crates/service/web/storyteller_web/src/routes.rs b/crates/service/web/storyteller_web/src/routes.rs index a0d8079f49..870977024e 100644 --- a/crates/service/web/storyteller_web/src/routes.rs +++ b/crates/service/web/storyteller_web/src/routes.rs @@ -37,6 +37,7 @@ use crate::http_server::endpoints::inference_job::terminate_inference_job_handle use crate::http_server::endpoints::investor_demo::disable_demo_mode_handler::disable_demo_mode_handler; use crate::http_server::endpoints::investor_demo::enable_demo_mode_handler::enable_demo_mode_handler; use crate::http_server::endpoints::leaderboard::get_leaderboard::leaderboard_handler; +use crate::http_server::endpoints::media_files::delete_media_file::delete_media_file_handler; use crate::http_server::endpoints::media_files::get_media_file::get_media_file_handler; use crate::http_server::endpoints::media_uploads::list_user_media_uploads_of_type::list_user_media_uploads_of_type_handler; use crate::http_server::endpoints::media_uploads::upload_audio::upload_audio_handler; @@ -982,6 +983,7 @@ fn add_media_file_routes (app: App) -> App app.service(web::scope("/v1/media_files") .service(web::resource("/file/{token}") .route(web::get().to(get_media_file_handler)) + .route(web::delete().to(delete_media_file_handler)) .route(web::head().to(|| HttpResponse::Ok())) ) )