diff --git a/.sqlx/query-0eae8461a1caa6a609bfc4f329a4768ca0372a7b8cac54d83e3277ea0ad5ed9d.json b/.sqlx/query-0eae8461a1caa6a609bfc4f329a4768ca0372a7b8cac54d83e3277ea0ad5ed9d.json deleted file mode 100644 index 0231b748..00000000 --- a/.sqlx/query-0eae8461a1caa6a609bfc4f329a4768ca0372a7b8cac54d83e3277ea0ad5ed9d.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n DELETE FROM public.af_workspace where workspace_id = $1\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [] - }, - "hash": "0eae8461a1caa6a609bfc4f329a4768ca0372a7b8cac54d83e3277ea0ad5ed9d" -} diff --git a/.sqlx/query-2ba7fdabdd71d2e3ac6dc1b67af17e8dcb59d13b644bc336da5351184ec4c9e1.json b/.sqlx/query-2ba7fdabdd71d2e3ac6dc1b67af17e8dcb59d13b644bc336da5351184ec4c9e1.json new file mode 100644 index 00000000..926c6e56 --- /dev/null +++ b/.sqlx/query-2ba7fdabdd71d2e3ac6dc1b67af17e8dcb59d13b644bc336da5351184ec4c9e1.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE af_published_collab\n SET publish_name = $1\n WHERE workspace_id = $2\n AND view_id = $3\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "2ba7fdabdd71d2e3ac6dc1b67af17e8dcb59d13b644bc336da5351184ec4c9e1" +} diff --git a/.sqlx/query-f58a2f05efbda0698d27d83be5c6816fc46e3de33f926c6343bcbfa90a387b07.json b/.sqlx/query-f58a2f05efbda0698d27d83be5c6816fc46e3de33f926c6343bcbfa90a387b07.json new file mode 100644 index 00000000..8eb8b64b --- /dev/null +++ b/.sqlx/query-f58a2f05efbda0698d27d83be5c6816fc46e3de33f926c6343bcbfa90a387b07.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n DELETE FROM public.af_workspace\n WHERE workspace_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "f58a2f05efbda0698d27d83be5c6816fc46e3de33f926c6343bcbfa90a387b07" +} diff --git a/Cargo.toml b/Cargo.toml index 6b96c84f..98c04f20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -144,7 +144,7 @@ tonic-build = "0.11.0" log = "0.4.20" lettre = { version = "0.11.7", features = ["tokio1", "tokio1-native-tls"] } handlebars = "5.1.2" -pin-project = "1.1.5" +pin-project.workspace = true byteorder = "1.5.0" sha2 = "0.10.8" rayon.workspace = true @@ -281,6 +281,7 @@ async_zip = { version = "0.0.17", features = ["full"] } sanitize-filename = "0.5.0" base64 = "0.22" md5 = "0.7.0" +pin-project = "1.1.5" # collaboration yrs = { version = "0.21.2", features = ["sync"] } diff --git a/libs/app-error/src/lib.rs b/libs/app-error/src/lib.rs index 1c12a522..48782e26 100644 --- a/libs/app-error/src/lib.rs +++ b/libs/app-error/src/lib.rs @@ -357,6 +357,9 @@ pub enum ErrorCode { CustomNamespaceDisabled = 1044, CustomNamespaceDisallowed = 1045, TooManyImportTask = 1046, + CustomNamespaceTooShort = 1047, + CustomNamespaceTooLong = 1048, + CustomNamespaceReserved = 1049, } impl ErrorCode { diff --git a/libs/client-api/Cargo.toml b/libs/client-api/Cargo.toml index d8f8a584..758397be 100644 --- a/libs/client-api/Cargo.toml +++ b/libs/client-api/Cargo.toml @@ -45,7 +45,7 @@ collab-rt-entity = { workspace = true } client-api-entity.workspace = true serde_urlencoded = "0.7.1" futures.workspace = true -pin-project = "1.1.5" +pin-project.workspace = true percent-encoding = "2.3.1" lazy_static = { workspace = true } mime_guess = "2.0.5" diff --git a/libs/client-api/src/http_publish.rs b/libs/client-api/src/http_publish.rs index 13b318ef..e8d29247 100644 --- a/libs/client-api/src/http_publish.rs +++ b/libs/client-api/src/http_publish.rs @@ -3,7 +3,8 @@ use client_api_entity::workspace_dto::PublishInfoView; use client_api_entity::{workspace_dto::PublishedDuplicate, PublishInfo, UpdatePublishNamespace}; use client_api_entity::{ CreateGlobalCommentParams, CreateReactionParams, DeleteGlobalCommentParams, DeleteReactionParams, - GetReactionQueryParams, GlobalComments, PublishInfoMeta, Reactions, UpdateDefaultPublishView, + GetReactionQueryParams, GlobalComments, PatchPublishedCollab, PublishInfoMeta, Reactions, + UpdateDefaultPublishView, }; use reqwest::Method; use shared_entity::response::{AppResponse, AppResponseError}; @@ -75,6 +76,22 @@ impl Client { .into_data() } + pub async fn patch_published_collabs( + &self, + workspace_id: &str, + patches: &[PatchPublishedCollab], + ) -> Result<(), AppResponseError> { + let url = format!("{}/api/workspace/{}/publish", self.base_url, workspace_id); + let resp = self + .http_client_with_auth(Method::PATCH, &url) + .await? + .json(patches) + .send() + .await?; + log_request_id(&resp); + AppResponse::<()>::from_response(resp).await?.into_error() + } + pub async fn unpublish_collabs( &self, workspace_id: &str, @@ -251,7 +268,7 @@ impl Client { &self, view_id: &uuid::Uuid, ) -> Result { - let url = format!("{}/api/workspace/published-info/{}", self.base_url, view_id,); + let url = format!("{}/api/workspace/published-info/{}", self.base_url, view_id); let resp = self.cloud_client.get(&url).send().await?; AppResponse::::from_response(resp) diff --git a/libs/database-entity/src/dto.rs b/libs/database-entity/src/dto.rs index 95953872..c10b1664 100644 --- a/libs/database-entity/src/dto.rs +++ b/libs/database-entity/src/dto.rs @@ -1133,6 +1133,12 @@ pub struct PublishCollabItem { pub data: Data, } +#[derive(Debug, Serialize, Deserialize)] +pub struct PatchPublishedCollab { + pub view_id: Uuid, + pub publish_name: Option, +} + #[derive(Serialize, Deserialize, Debug)] pub struct GlobalComments { pub comments: Vec, diff --git a/libs/database/src/publish.rs b/libs/database/src/publish.rs index ffd77023..4f64d115 100644 --- a/libs/database/src/publish.rs +++ b/libs/database/src/publish.rs @@ -1,5 +1,7 @@ use app_error::AppError; -use database_entity::dto::{PublishCollabItem, PublishCollabKey, PublishInfo}; +use database_entity::dto::{ + PatchPublishedCollab, PublishCollabItem, PublishCollabKey, PublishInfo, +}; use sqlx::{Executor, PgPool, Postgres}; use uuid::Uuid; @@ -238,6 +240,46 @@ pub async fn delete_published_collabs<'a, E: Executor<'a, Database = Postgres>>( Ok(()) } +#[inline] +pub async fn update_published_collabs( + txn: &mut sqlx::Transaction<'_, Postgres>, + workspace_id: &Uuid, + patches: &[PatchPublishedCollab], +) -> Result<(), AppError> { + for patch in patches { + let new_publish_name = match &patch.publish_name { + Some(new_publish_name) => new_publish_name, + None => continue, + }; + + let res = sqlx::query!( + r#" + UPDATE af_published_collab + SET publish_name = $1 + WHERE workspace_id = $2 + AND view_id = $3 + "#, + patch.publish_name, + workspace_id, + patch.view_id, + ) + .execute(txn.as_mut()) + .await?; + + if res.rows_affected() != 1 { + tracing::error!( + "Failed to update published collab publish name, workspace_id: {}, view_id: {}, new_publish_name: {}, rows_affected: {}", + workspace_id, + patch.view_id, + new_publish_name, + res.rows_affected() + ); + } + } + + Ok(()) +} + #[inline] pub async fn select_published_metadata_for_view_id( pg_pool: &PgPool, diff --git a/libs/database/src/workspace.rs b/libs/database/src/workspace.rs index 1b05da71..a9037c1b 100644 --- a/libs/database/src/workspace.rs +++ b/libs/database/src/workspace.rs @@ -19,16 +19,24 @@ use app_error::AppError; #[inline] pub async fn delete_from_workspace(pg_pool: &PgPool, workspace_id: &Uuid) -> Result<(), AppError> { - let pg_row = sqlx::query!( + let res = sqlx::query!( r#" - DELETE FROM public.af_workspace where workspace_id = $1 + DELETE FROM public.af_workspace + WHERE workspace_id = $1 "#, workspace_id ) .execute(pg_pool) .await?; - debug_assert!(pg_row.rows_affected() == 1); + if res.rows_affected() != 1 { + tracing::error!( + "Failed to delete workspace, workspace_id: {}, rows_affected: {}", + workspace_id, + res.rows_affected() + ); + } + Ok(()) } diff --git a/libs/infra/Cargo.toml b/libs/infra/Cargo.toml index ce1714e8..bc2d152e 100644 --- a/libs/infra/Cargo.toml +++ b/libs/infra/Cargo.toml @@ -13,9 +13,9 @@ serde_json.workspace = true tracing.workspace = true bytes = { workspace = true } tokio = { workspace = true, optional = true } -pin-project = "1.1.6" +pin-project.workspace = true futures = "0.3.30" [features] file_util = ["tokio/fs"] -request_util = ["reqwest"] \ No newline at end of file +request_util = ["reqwest"] diff --git a/src/api/workspace.rs b/src/api/workspace.rs index e7470ebf..d545c639 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -204,7 +204,8 @@ pub fn workspace_scope() -> Scope { .service( web::resource("/{workspace_id}/publish") .route(web::post().to(post_publish_collabs_handler)) - .route(web::delete().to(delete_published_collabs_handler)), + .route(web::delete().to(delete_published_collabs_handler)) + .route(web::patch().to(patch_published_collabs_handler)), ) .service( web::resource("/{workspace_id}/folder").route(web::get().to(get_workspace_folder_handler)), @@ -1440,6 +1441,23 @@ async fn post_publish_collabs_handler( Ok(Json(AppResponse::Ok())) } +async fn patch_published_collabs_handler( + workspace_id: web::Path, + user_uuid: UserUuid, + state: Data, + patches: Json>, +) -> Result>> { + let workspace_id = workspace_id.into_inner(); + if patches.is_empty() { + return Err(AppError::InvalidRequest("No patches provided".to_string()).into()); + } + state + .published_collab_store + .patch_collabs(&workspace_id, &user_uuid, &patches) + .await?; + Ok(Json(AppResponse::Ok())) +} + async fn delete_published_collabs_handler( workspace_id: web::Path, user_uuid: UserUuid, @@ -1449,11 +1467,11 @@ async fn delete_published_collabs_handler( let workspace_id = workspace_id.into_inner(); let view_ids = view_ids.into_inner(); if view_ids.is_empty() { - return Ok(Json(AppResponse::Ok())); + return Err(AppError::InvalidRequest("No view_ids provided".to_string()).into()); } state .published_collab_store - .delete_collab(&workspace_id, &view_ids, &user_uuid) + .delete_collabs(&workspace_id, &view_ids, &user_uuid) .await?; Ok(Json(AppResponse::Ok())) } diff --git a/src/biz/workspace/publish.rs b/src/biz/workspace/publish.rs index 12735cb3..3ba13fce 100644 --- a/src/biz/workspace/publish.rs +++ b/src/biz/workspace/publish.rs @@ -3,9 +3,11 @@ use database::{ collab::GetCollabOrigin, publish::{ select_all_published_collab_info, select_default_published_view_id, - select_default_published_view_id_for_namespace, update_workspace_default_publish_view, + select_default_published_view_id_for_namespace, update_published_collabs, + update_workspace_default_publish_view, }, }; +use database_entity::dto::PatchPublishedCollab; use std::sync::Arc; use app_error::AppError; @@ -251,12 +253,19 @@ pub trait PublishedCollabStore: Sync + Send + 'static { publish_name: &str, ) -> Result, AppError>; - async fn delete_collab( + async fn delete_collabs( &self, workspace_id: &Uuid, view_ids: &[Uuid], user_uuid: &Uuid, ) -> Result<(), AppError>; + + async fn patch_collabs( + &self, + workspace_id: &Uuid, + user_uuid: &Uuid, + patches: &[PatchPublishedCollab], + ) -> Result<(), AppError>; } pub struct PublishedCollabPostgresStore { @@ -351,7 +360,7 @@ impl PublishedCollabStore for PublishedCollabPostgresStore { result } - async fn delete_collab( + async fn delete_collabs( &self, workspace_id: &Uuid, view_ids: &[Uuid], @@ -361,6 +370,15 @@ impl PublishedCollabStore for PublishedCollabPostgresStore { delete_published_collabs(&self.pg_pool, workspace_id, view_ids).await?; Ok(()) } + + async fn patch_collabs( + &self, + workspace_id: &Uuid, + user_uuid: &Uuid, + patches: &[PatchPublishedCollab], + ) -> Result<(), AppError> { + patch_collabs(&self.pg_pool, workspace_id, user_uuid, patches).await + } } pub struct PublishedCollabS3StoreWithPostgresFallback { @@ -519,7 +537,7 @@ impl PublishedCollabStore for PublishedCollabS3StoreWithPostgresFallback { } } - async fn delete_collab( + async fn delete_collabs( &self, workspace_id: &Uuid, view_ids: &[Uuid], @@ -534,4 +552,36 @@ impl PublishedCollabStore for PublishedCollabS3StoreWithPostgresFallback { delete_published_collabs(&self.pg_pool, workspace_id, view_ids).await?; Ok(()) } + + async fn patch_collabs( + &self, + workspace_id: &Uuid, + user_uuid: &Uuid, + patches: &[PatchPublishedCollab], + ) -> Result<(), AppError> { + patch_collabs(&self.pg_pool, workspace_id, user_uuid, patches).await + } +} + +async fn patch_collabs( + pg_pool: &PgPool, + workspace_id: &Uuid, + user_uuid: &Uuid, + patches: &[PatchPublishedCollab], +) -> Result<(), AppError> { + let view_ids = patches + .iter() + .map(|patch| patch.view_id) + .collect::>(); + for patch in patches { + if let Some(new_publish_name) = patch.publish_name.as_deref() { + check_collab_publish_name(new_publish_name)?; + } + } + check_workspace_owner_or_publisher(pg_pool, user_uuid, workspace_id, &view_ids).await?; + + let mut txn = pg_pool.begin().await?; + update_published_collabs(&mut txn, workspace_id, patches).await?; + txn.commit().await?; + Ok(()) } diff --git a/tests/workspace/publish.rs b/tests/workspace/publish.rs index f5c4fc78..ce8a3498 100644 --- a/tests/workspace/publish.rs +++ b/tests/workspace/publish.rs @@ -2,7 +2,8 @@ use app_error::ErrorCode; use appflowy_cloud::biz::collab::folder_view::collab_folder_to_folder_view; use appflowy_cloud::biz::workspace::ops::collab_from_doc_state; use client_api::entity::{ - AFRole, GlobalComment, PublishCollabItem, PublishCollabMetadata, PublishInfoMeta, + AFRole, GlobalComment, PatchPublishedCollab, PublishCollabItem, PublishCollabMetadata, + PublishInfoMeta, }; use client_api_test::TestClient; use client_api_test::{generate_unique_registered_user_client, localhost_client}; @@ -250,6 +251,56 @@ async fn test_publish_doc() { assert_eq!(default_info_meta.meta.title, "my_title_1"); } + { + let new_publish_name_1 = "new-publish-name-1".to_string(); + + // User change publish name + c.patch_published_collabs( + &workspace_id, + &[PatchPublishedCollab { + view_id: view_id_1, + publish_name: Some(new_publish_name_1.to_string()), + }], + ) + .await + .unwrap(); + + // Guest now cannot access the collab using old publish name + let guest_client = localhost_client(); + let err = guest_client + .get_published_collab::(&my_namespace, publish_name_1) + .await + .err() + .unwrap(); + assert_eq!(err.code, ErrorCode::RecordNotFound, "{:?}", err); + + // Guest now access the collab using new publish name + let guest_client = localhost_client(); + let _ = guest_client + .get_published_collab::(&my_namespace, &new_publish_name_1) + .await + .unwrap(); + + // Switch back to old publish name + c.patch_published_collabs( + &workspace_id, + &[PatchPublishedCollab { + view_id: view_id_1, + publish_name: Some(publish_name_1.to_string()), + }], + ) + .await + .unwrap(); + + // Guest can access the collab using the orginal publish name + let guest_client = localhost_client(); + let _ = guest_client + .get_published_collab::(&my_namespace, publish_name_1) + .await + .unwrap(); + } + + // user unpublish collabs c.unpublish_collabs(&workspace_id, &[view_id_1, view_id_2]) .await .unwrap();