mirror of
https://github.com/AppFlowy-IO/AppFlowy-Cloud.git
synced 2025-04-19 03:24:42 -04:00
feat: workspace manager & test (#117)
* chore: add workspace tests * chore: add slqx files * feat: update workspace member role * chore: update
This commit is contained in:
parent
5c58f95f9f
commit
3e73adc82d
21 changed files with 496 additions and 171 deletions
23
.sqlx/query-36733444fc8fac851fb540105ea6c9dca785455ae44ae518b98d8b57082e11d8.json
generated
Normal file
23
.sqlx/query-36733444fc8fac851fb540105ea6c9dca785455ae44ae518b98d8b57082e11d8.json
generated
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT EXISTS(\n SELECT 1\n FROM public.af_workspace_member\n JOIN af_roles ON af_workspace_member.role_id = af_roles.id\n WHERE workspace_id = $1\n AND af_workspace_member.uid = (\n SELECT uid FROM public.af_user WHERE uuid = $2\n )\n AND af_roles.name = 'Owner'\n ) AS \"exists\";\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "36733444fc8fac851fb540105ea6c9dca785455ae44ae518b98d8b57082e11d8"
|
||||
}
|
16
.sqlx/query-4162ec00fad0abe726492a5b916205eec1f004bcf71864dd59c84fad6f3b7e98.json
generated
Normal file
16
.sqlx/query-4162ec00fad0abe726492a5b916205eec1f004bcf71864dd59c84fad6f3b7e98.json
generated
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE af_workspace_member\n SET \n role_id = COALESCE($1, role_id)\n WHERE workspace_id = $2 AND uid = (\n SELECT uid FROM af_user WHERE email = $3\n )\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4",
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4162ec00fad0abe726492a5b916205eec1f004bcf71864dd59c84fad6f3b7e98"
|
||||
}
|
15
.sqlx/query-54c2e86c48a1549d8006bd5592eb5610985d5d43cd67efd417a0c6e52a967dfd.json
generated
Normal file
15
.sqlx/query-54c2e86c48a1549d8006bd5592eb5610985d5d43cd67efd417a0c6e52a967dfd.json
generated
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM public.af_workspace_member\n WHERE \n workspace_id = $1 \n AND uid = (\n SELECT uid FROM public.af_user WHERE email = $2\n )\n -- Ensure the user to be deleted is not the original owner\n AND uid <> (\n SELECT owner_uid FROM public.af_workspace WHERE workspace_id = $1\n );\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "54c2e86c48a1549d8006bd5592eb5610985d5d43cd67efd417a0c6e52a967dfd"
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM public.af_workspace_member\n WHERE workspace_id = $1 AND uid IN (\n SELECT uid FROM public.af_user WHERE email = ANY($2::text[])\n )\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a8bafeabd18222d562ae65a6bb56a55615d4e2dbf93db484378f470b529ac08a"
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT EXISTS(\n SELECT 1 FROM public.af_workspace\n WHERE workspace_id = $1 AND owner_uid = (\n SELECT uid FROM public.af_user WHERE uuid = $2\n )\n )\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "ede9205510d7deac87d47a56d65235b1c930d2b6e0765176e4e30935cc347299"
|
||||
}
|
23
.sqlx/query-fa7a89a789e010e29dd4737b5614ebb750e446a4c76e298731b1801dee679278.json
generated
Normal file
23
.sqlx/query-fa7a89a789e010e29dd4737b5614ebb750e446a4c76e298731b1801dee679278.json
generated
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT EXISTS (\n SELECT 1 \n FROM public.af_workspace\n WHERE \n workspace_id = $1 \n AND owner_uid = (\n SELECT uid FROM public.af_user WHERE email = $2\n )\n ) AS \"is_owner\";\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "is_owner",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "fa7a89a789e010e29dd4737b5614ebb750e446a4c76e298731b1801dee679278"
|
||||
}
|
|
@ -23,16 +23,19 @@ use reqwest::RequestBuilder;
|
|||
use scraper::{Html, Selector};
|
||||
use shared_entity::app_error::AppError;
|
||||
use shared_entity::data::AppResponse;
|
||||
use shared_entity::dto::SignInTokenResponse;
|
||||
use shared_entity::dto::UpdateUsernameParams;
|
||||
use shared_entity::dto::UserUpdateParams;
|
||||
use shared_entity::dto::{CreateWorkspaceMembers, WorkspaceMembers};
|
||||
use shared_entity::dto::auth_dto::SignInTokenResponse;
|
||||
use shared_entity::dto::auth_dto::UpdateUsernameParams;
|
||||
use shared_entity::dto::auth_dto::UserUpdateParams;
|
||||
use shared_entity::dto::workspace_dto::{
|
||||
CreateWorkspaceMembers, WorkspaceMemberChangeset, WorkspaceMembers,
|
||||
};
|
||||
use shared_entity::error_code::url_missing_param;
|
||||
use shared_entity::error_code::ErrorCode;
|
||||
use std::sync::Arc;
|
||||
use std::time::SystemTime;
|
||||
use tracing::instrument;
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// `Client` is responsible for managing communication with the GoTrue API and cloud storage.
|
||||
///
|
||||
|
@ -360,7 +363,7 @@ impl Client {
|
|||
#[instrument(level = "debug", skip_all, err)]
|
||||
pub async fn get_workspace_members(
|
||||
&self,
|
||||
workspace_uuid: uuid::Uuid,
|
||||
workspace_uuid: Uuid,
|
||||
) -> Result<Vec<AFWorkspaceMember>, AppError> {
|
||||
let url = format!("{}/api/workspace/{}/member", self.base_url, workspace_uuid);
|
||||
let resp = self
|
||||
|
@ -376,7 +379,7 @@ impl Client {
|
|||
#[instrument(level = "debug", skip_all, err)]
|
||||
pub async fn add_workspace_members<T: Into<CreateWorkspaceMembers>>(
|
||||
&self,
|
||||
workspace_uuid: uuid::Uuid,
|
||||
workspace_uuid: Uuid,
|
||||
members: T,
|
||||
) -> Result<(), AppError> {
|
||||
let members = members.into();
|
||||
|
@ -392,23 +395,42 @@ impl Client {
|
|||
}
|
||||
|
||||
#[instrument(level = "debug", skip_all, err)]
|
||||
pub async fn remove_workspace_members(
|
||||
pub async fn update_workspace_member(
|
||||
&self,
|
||||
workspace_uuid: uuid::Uuid,
|
||||
member_emails: Vec<String>,
|
||||
workspace_uuid: Uuid,
|
||||
changeset: WorkspaceMemberChangeset,
|
||||
) -> Result<(), AppError> {
|
||||
let url = format!("{}/api/workspace/{}/member", self.base_url, workspace_uuid);
|
||||
let req = WorkspaceMembers(member_emails);
|
||||
let resp = self
|
||||
.http_client_with_auth(Method::DELETE, &url)
|
||||
.http_client_with_auth(Method::PUT, &url)
|
||||
.await?
|
||||
.json(&req)
|
||||
.json(&changeset)
|
||||
.send()
|
||||
.await?;
|
||||
AppResponse::<()>::from_response(resp).await?.into_error()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip_all, err)]
|
||||
pub async fn remove_workspace_members(
|
||||
&self,
|
||||
workspace_uuid: Uuid,
|
||||
member_emails: Vec<String>,
|
||||
) -> Result<(), AppError> {
|
||||
let url = format!("{}/api/workspace/{}/member", self.base_url, workspace_uuid);
|
||||
let payload = WorkspaceMembers::from(member_emails);
|
||||
let resp = self
|
||||
.http_client_with_auth(Method::DELETE, &url)
|
||||
.await?
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await?;
|
||||
AppResponse::<()>::from_response(resp).await?.into_error()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// pub async fn update_workspace_member(&self, workspace_uuid: Uuid, member)
|
||||
|
||||
#[instrument(skip_all, err)]
|
||||
pub async fn sign_in_password(&self, email: &str, password: &str) -> Result<bool, AppError> {
|
||||
let access_token_resp = self
|
||||
|
|
|
@ -24,6 +24,9 @@ pub enum DatabaseError {
|
|||
#[error("Bucket error:{0}")]
|
||||
BucketError(String),
|
||||
|
||||
#[error("Not enough permission:{0}")]
|
||||
NotEnoughPermissions(String),
|
||||
|
||||
#[error(transparent)]
|
||||
Internal(#[from] anyhow::Error),
|
||||
}
|
||||
|
|
|
@ -182,26 +182,17 @@ impl AFWorkspaces {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)]
|
||||
pub enum AFRole {
|
||||
Owner,
|
||||
Member,
|
||||
Guest,
|
||||
}
|
||||
|
||||
impl AFRole {
|
||||
pub fn id(&self) -> i32 {
|
||||
match self {
|
||||
AFRole::Owner => 1,
|
||||
AFRole::Member => 2,
|
||||
AFRole::Guest => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for AFRole {
|
||||
fn from(item: i32) -> Self {
|
||||
match item {
|
||||
fn from(value: i32) -> Self {
|
||||
// Can't modify the value of the enum
|
||||
match value {
|
||||
1 => AFRole::Owner,
|
||||
2 => AFRole::Member,
|
||||
3 => AFRole::Guest,
|
||||
|
@ -210,6 +201,16 @@ impl From<i32> for AFRole {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<AFRole> for i32 {
|
||||
fn from(role: AFRole) -> Self {
|
||||
// Can't modify the value of the enum
|
||||
match role {
|
||||
AFRole::Owner => 1,
|
||||
AFRole::Member => 2,
|
||||
AFRole::Guest => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(FromRow, Serialize, Deserialize)]
|
||||
pub struct AFWorkspaceMember {
|
||||
pub email: String,
|
||||
|
|
|
@ -25,20 +25,29 @@ pub async fn select_all_workspaces_owned(
|
|||
Ok(workspaces)
|
||||
}
|
||||
|
||||
/// Checks whether a user, identified by a UUID, is an 'Owner' of a workspace, identified by its
|
||||
/// workspace_id.
|
||||
pub async fn select_user_is_workspace_owner(
|
||||
pg_pool: &PgPool,
|
||||
user_uuid: &Uuid,
|
||||
workspace_uuid: &Uuid,
|
||||
) -> Result<bool, DatabaseError> {
|
||||
// 1. Identifies the user's UID in the 'af_user' table using the provided user UUID ($2).
|
||||
// 2. Then, it checks the 'af_workspace_member' table to find a record that matches the provided workspace_id ($1) and the identified UID.
|
||||
// 3. It joins with 'af_roles' to ensure that the role associated with the workspace member is 'Owner'.
|
||||
let exists = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM public.af_workspace
|
||||
WHERE workspace_id = $1 AND owner_uid = (
|
||||
SELECT uid FROM public.af_user WHERE uuid = $2
|
||||
)
|
||||
)
|
||||
"#,
|
||||
SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM public.af_workspace_member
|
||||
JOIN af_roles ON af_workspace_member.role_id = af_roles.id
|
||||
WHERE workspace_id = $1
|
||||
AND af_workspace_member.uid = (
|
||||
SELECT uid FROM public.af_user WHERE uuid = $2
|
||||
)
|
||||
AND af_roles.name = 'Owner'
|
||||
) AS "exists";
|
||||
"#,
|
||||
workspace_uuid,
|
||||
user_uuid
|
||||
)
|
||||
|
@ -54,6 +63,7 @@ pub async fn insert_workspace_member(
|
|||
member_email: String,
|
||||
role: AFRole,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let role_id: i32 = role.into();
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO public.af_workspace_member (workspace_id, uid, role_id)
|
||||
|
@ -65,7 +75,7 @@ pub async fn insert_workspace_member(
|
|||
"#,
|
||||
workspace_id,
|
||||
member_email,
|
||||
role.id()
|
||||
role_id
|
||||
)
|
||||
.execute(txn.deref_mut())
|
||||
.await?;
|
||||
|
@ -73,20 +83,29 @@ pub async fn insert_workspace_member(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_workspace_members(
|
||||
pub async fn upsert_workspace_member(
|
||||
pool: &PgPool,
|
||||
workspace_id: &uuid::Uuid,
|
||||
member_emails: &[String],
|
||||
) -> Result<(), DatabaseError> {
|
||||
workspace_id: &Uuid,
|
||||
email: &str,
|
||||
role: Option<AFRole>,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
if role.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let role_id: Option<i32> = role.map(|role| role.into());
|
||||
sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM public.af_workspace_member
|
||||
WHERE workspace_id = $1 AND uid IN (
|
||||
SELECT uid FROM public.af_user WHERE email = ANY($2::text[])
|
||||
UPDATE af_workspace_member
|
||||
SET
|
||||
role_id = COALESCE($1, role_id)
|
||||
WHERE workspace_id = $2 AND uid = (
|
||||
SELECT uid FROM af_user WHERE email = $3
|
||||
)
|
||||
"#,
|
||||
role_id,
|
||||
workspace_id,
|
||||
&member_emails
|
||||
email
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
@ -94,6 +113,59 @@ pub async fn delete_workspace_members(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_workspace_members(
|
||||
_user_uuid: &Uuid,
|
||||
txn: &mut Transaction<'_, sqlx::Postgres>,
|
||||
workspace_id: &Uuid,
|
||||
member_email: String,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let is_owner = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM public.af_workspace
|
||||
WHERE
|
||||
workspace_id = $1
|
||||
AND owner_uid = (
|
||||
SELECT uid FROM public.af_user WHERE email = $2
|
||||
)
|
||||
) AS "is_owner";
|
||||
"#,
|
||||
workspace_id,
|
||||
member_email
|
||||
)
|
||||
.fetch_one(txn.deref_mut())
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
|
||||
if is_owner {
|
||||
return Err(DatabaseError::NotEnoughPermissions(
|
||||
"Owner cannot be deleted".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM public.af_workspace_member
|
||||
WHERE
|
||||
workspace_id = $1
|
||||
AND uid = (
|
||||
SELECT uid FROM public.af_user WHERE email = $2
|
||||
)
|
||||
-- Ensure the user to be deleted is not the original owner
|
||||
AND uid <> (
|
||||
SELECT owner_uid FROM public.af_workspace WHERE workspace_id = $1
|
||||
);
|
||||
"#,
|
||||
workspace_id,
|
||||
member_email,
|
||||
)
|
||||
.execute(txn.deref_mut())
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn select_workspace_members(
|
||||
pg_pool: &PgPool,
|
||||
workspace_id: &uuid::Uuid,
|
||||
|
|
|
@ -57,6 +57,9 @@ impl From<DatabaseError> for AppError {
|
|||
match &value {
|
||||
DatabaseError::RecordNotFound => AppError::new(ErrorCode::RecordNotFound, value),
|
||||
DatabaseError::UnexpectedData(_) => AppError::new(ErrorCode::InvalidRequestParams, value),
|
||||
DatabaseError::NotEnoughPermissions(msg) => {
|
||||
AppError::new(ErrorCode::NotEnoughPermissions, msg.clone())
|
||||
},
|
||||
DatabaseError::StorageSpaceNotEnough => {
|
||||
AppError::new(ErrorCode::StorageSpaceNotEnough, value)
|
||||
},
|
||||
|
|
|
@ -1,44 +1,7 @@
|
|||
// Data Transfer Objects (DTO)
|
||||
|
||||
use database_entity::AFRole;
|
||||
use gotrue_entity::AccessTokenResponse;
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct WorkspaceMembers(pub Vec<String>);
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct CreateWorkspaceMembers(pub Vec<CreateWorkspaceMember>);
|
||||
|
||||
impl From<Vec<CreateWorkspaceMember>> for CreateWorkspaceMembers {
|
||||
fn from(value: Vec<CreateWorkspaceMember>) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde_repr::Deserialize_repr, serde_repr::Serialize_repr)]
|
||||
#[repr(u8)]
|
||||
pub enum WorkspacePermission {
|
||||
Owner = 0,
|
||||
Member = 1,
|
||||
Guest = 2,
|
||||
}
|
||||
|
||||
impl From<WorkspacePermission> for AFRole {
|
||||
fn from(value: WorkspacePermission) -> Self {
|
||||
match value {
|
||||
WorkspacePermission::Owner => AFRole::Owner,
|
||||
WorkspacePermission::Member => AFRole::Member,
|
||||
WorkspacePermission::Guest => AFRole::Guest,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct CreateWorkspaceMember {
|
||||
pub email: String,
|
||||
pub permission: WorkspacePermission,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct SignInParams {
|
||||
pub email: String,
|
2
libs/shared-entity/src/dto/mod.rs
Normal file
2
libs/shared-entity/src/dto/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod auth_dto;
|
||||
pub mod workspace_dto;
|
60
libs/shared-entity/src/dto/workspace_dto.rs
Normal file
60
libs/shared-entity/src/dto/workspace_dto.rs
Normal file
|
@ -0,0 +1,60 @@
|
|||
use database_entity::AFRole;
|
||||
use std::ops::Deref;
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct WorkspaceMembers(pub Vec<WorkspaceMember>);
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct WorkspaceMember(pub String);
|
||||
impl Deref for WorkspaceMember {
|
||||
type Target = String;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<String>> for WorkspaceMembers {
|
||||
fn from(value: Vec<String>) -> Self {
|
||||
Self(value.into_iter().map(WorkspaceMember).collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct CreateWorkspaceMembers(pub Vec<CreateWorkspaceMember>);
|
||||
impl From<Vec<CreateWorkspaceMember>> for CreateWorkspaceMembers {
|
||||
fn from(value: Vec<CreateWorkspaceMember>) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct CreateWorkspaceMember {
|
||||
pub email: String,
|
||||
pub role: AFRole,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct WorkspaceMemberChangeset {
|
||||
pub email: String,
|
||||
pub role: Option<AFRole>,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
impl WorkspaceMemberChangeset {
|
||||
pub fn new(email: String) -> Self {
|
||||
Self {
|
||||
email,
|
||||
role: None,
|
||||
name: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_role(mut self, role: AFRole) -> Self {
|
||||
self.role = Some(role);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_name(mut self, name: String) -> Self {
|
||||
self.name = Some(name);
|
||||
self
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
pub mod app_error;
|
||||
pub mod data;
|
||||
pub mod dto;
|
||||
pub mod error_code;
|
||||
|
||||
#[cfg(feature = "cloud")]
|
||||
mod data_actix;
|
||||
pub mod dto;
|
||||
|
|
|
@ -11,7 +11,7 @@ use crate::domain::{UserEmail, UserName, UserPassword};
|
|||
use crate::state::AppState;
|
||||
use database_entity::AFUserProfileView;
|
||||
use shared_entity::data::{AppResponse, JsonAppResponse};
|
||||
use shared_entity::dto::{SignInTokenResponse, UpdateUsernameParams};
|
||||
use shared_entity::dto::auth_dto::{SignInTokenResponse, UpdateUsernameParams};
|
||||
|
||||
use crate::component::auth::jwt::{Authorization, UserUuid};
|
||||
use actix_web::web::{Data, Json};
|
||||
|
|
|
@ -8,7 +8,7 @@ use actix_web::Result;
|
|||
use actix_web::{web, Scope};
|
||||
use database_entity::{AFWorkspaceMember, AFWorkspaces};
|
||||
use shared_entity::data::{AppResponse, JsonAppResponse};
|
||||
use shared_entity::dto::{CreateWorkspaceMembers, WorkspaceMembers};
|
||||
use shared_entity::dto::workspace_dto::*;
|
||||
use sqlx::types::uuid;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
@ -21,7 +21,6 @@ pub const WORKSPACE_ID_PATH: &str = "workspace_id";
|
|||
const SCOPE_PATH: &str = "/api/workspace";
|
||||
const WORKSPACE_LIST_PATH: &str = "list";
|
||||
const WORKSPACE_MEMBER_PATH: &str = "{workspace_id}/member";
|
||||
const WORKSPACE_MEMBER_PERMISSION_PATH: &str = "{workspace_id}/member/permission";
|
||||
|
||||
pub fn workspace_scope() -> Scope {
|
||||
web::scope(SCOPE_PATH)
|
||||
|
@ -30,12 +29,9 @@ pub fn workspace_scope() -> Scope {
|
|||
web::resource(WORKSPACE_MEMBER_PATH)
|
||||
.route(web::get().to(list_workspace_members_handler))
|
||||
.route(web::post().to(add_workspace_members_handler))
|
||||
.route(web::put().to(update_workspace_member_handler))
|
||||
.route(web::delete().to(remove_workspace_member_handler)),
|
||||
)
|
||||
.service(
|
||||
web::resource(WORKSPACE_MEMBER_PERMISSION_PATH)
|
||||
.route(web::post().to(update_workspace_member_permission_handler)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn workspace_scope_access_control(
|
||||
|
@ -53,11 +49,6 @@ pub fn workspace_scope_access_control(
|
|||
Arc::new(WorkspaceOwnerAccessControl),
|
||||
);
|
||||
|
||||
access_control.insert(
|
||||
format!("{}/{}", SCOPE_PATH, WORKSPACE_MEMBER_PERMISSION_PATH),
|
||||
Arc::new(WorkspaceOwnerAccessControl),
|
||||
);
|
||||
|
||||
access_control
|
||||
}
|
||||
|
||||
|
@ -106,18 +97,30 @@ async fn remove_workspace_member_handler(
|
|||
state: Data<AppState>,
|
||||
workspace_id: web::Path<Uuid>,
|
||||
) -> Result<JsonAppResponse<()>> {
|
||||
let members = payload.into_inner();
|
||||
workspace::ops::remove_workspace_members(&state.pg_pool, &user_uuid, &workspace_id, &members.0)
|
||||
.await?;
|
||||
let member_emails = payload
|
||||
.into_inner()
|
||||
.0
|
||||
.into_iter()
|
||||
.map(|member| member.0)
|
||||
.collect();
|
||||
workspace::ops::remove_workspace_members(
|
||||
&user_uuid,
|
||||
&state.pg_pool,
|
||||
workspace_id.into_inner(),
|
||||
member_emails,
|
||||
)
|
||||
.await?;
|
||||
Ok(AppResponse::Ok().into())
|
||||
}
|
||||
|
||||
#[instrument(skip_all, err)]
|
||||
async fn update_workspace_member_permission_handler(
|
||||
_user_uuid: UserUuid,
|
||||
_req: Json<CreateWorkspaceMembers>,
|
||||
_state: Data<AppState>,
|
||||
_workspace_id: web::Path<Uuid>,
|
||||
async fn update_workspace_member_handler(
|
||||
payload: Json<WorkspaceMemberChangeset>,
|
||||
state: Data<AppState>,
|
||||
workspace_id: web::Path<Uuid>,
|
||||
) -> Result<JsonAppResponse<()>> {
|
||||
todo!()
|
||||
let workspace_id = workspace_id.into_inner();
|
||||
let changeset = payload.into_inner();
|
||||
workspace::ops::update_workspace_member(&state.pg_pool, &workspace_id, changeset).await?;
|
||||
Ok(AppResponse::Ok().into())
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
use crate::component::auth::jwt::UserUuid;
|
||||
use anyhow::Context;
|
||||
use database::workspace::{
|
||||
delete_workspace_members, insert_workspace_member, select_all_workspaces_owned,
|
||||
select_user_is_workspace_owner, select_workspace_members,
|
||||
select_user_is_workspace_owner, select_workspace_members, upsert_workspace_member,
|
||||
};
|
||||
use database_entity::{AFWorkspaceMember, AFWorkspaces};
|
||||
use shared_entity::dto::CreateWorkspaceMember;
|
||||
use shared_entity::dto::workspace_dto::{CreateWorkspaceMember, WorkspaceMemberChangeset};
|
||||
use shared_entity::{app_error::AppError, error_code::ErrorCode};
|
||||
use sqlx::{types::uuid, PgPool};
|
||||
|
||||
|
@ -27,13 +28,7 @@ pub async fn add_workspace_members(
|
|||
.await
|
||||
.context("Begin transaction to insert workspace members")?;
|
||||
for member in members {
|
||||
insert_workspace_member(
|
||||
&mut txn,
|
||||
workspace_id,
|
||||
member.email,
|
||||
member.permission.into(),
|
||||
)
|
||||
.await?;
|
||||
insert_workspace_member(&mut txn, workspace_id, member.email, member.role).await?;
|
||||
}
|
||||
|
||||
txn
|
||||
|
@ -44,12 +39,25 @@ pub async fn add_workspace_members(
|
|||
}
|
||||
|
||||
pub async fn remove_workspace_members(
|
||||
user_uuid: &UserUuid,
|
||||
pg_pool: &PgPool,
|
||||
_user_uuid: &uuid::Uuid,
|
||||
workspace_id: &uuid::Uuid,
|
||||
member_emails: &[String],
|
||||
workspace_id: uuid::Uuid,
|
||||
member_emails: Vec<String>,
|
||||
) -> Result<(), AppError> {
|
||||
Ok(delete_workspace_members(pg_pool, workspace_id, member_emails).await?)
|
||||
let mut txn = pg_pool
|
||||
.begin()
|
||||
.await
|
||||
.context("Begin transaction to delete workspace members")?;
|
||||
|
||||
for email in member_emails {
|
||||
delete_workspace_members(user_uuid, &mut txn, &workspace_id, email).await?;
|
||||
}
|
||||
|
||||
txn
|
||||
.commit()
|
||||
.await
|
||||
.context("Commit transaction to delete workspace members")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_workspace_members(
|
||||
|
@ -61,13 +69,13 @@ pub async fn get_workspace_members(
|
|||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn update_workspace_member_permission(
|
||||
_pg_pool: &PgPool,
|
||||
_user_uuid: &uuid::Uuid,
|
||||
_workspace_id: &uuid::Uuid,
|
||||
_member_emails: &[String],
|
||||
pub async fn update_workspace_member(
|
||||
pg_pool: &PgPool,
|
||||
workspace_id: &uuid::Uuid,
|
||||
changeset: WorkspaceMemberChangeset,
|
||||
) -> Result<(), AppError> {
|
||||
todo!()
|
||||
upsert_workspace_member(pg_pool, workspace_id, &changeset.email, changeset.role).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn require_user_is_workspace_owner(
|
||||
|
|
|
@ -4,7 +4,7 @@ use crate::middleware::permission_mw::WorkspaceAccessControlService;
|
|||
use async_trait::async_trait;
|
||||
use shared_entity::app_error::AppError;
|
||||
use sqlx::PgPool;
|
||||
use tracing::trace;
|
||||
use tracing::{debug, trace};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -19,7 +19,9 @@ impl WorkspaceAccessControlService for WorkspaceOwnerAccessControl {
|
|||
pg_pool: &PgPool,
|
||||
) -> Result<(), AppError> {
|
||||
let result = require_user_is_workspace_owner(pg_pool, &user_uuid, &workspace_id).await;
|
||||
trace!("Workspace owner access control: {:?}", result);
|
||||
if let Err(err) = result.as_ref() {
|
||||
debug!("Workspace access control: {:?}", err);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use shared_entity::dto::UserUpdateParams;
|
||||
use shared_entity::dto::auth_dto::UserUpdateParams;
|
||||
use shared_entity::error_code::ErrorCode;
|
||||
|
||||
use crate::localhost_client;
|
||||
|
|
|
@ -1,25 +1,23 @@
|
|||
use shared_entity::dto::{CreateWorkspaceMember, WorkspacePermission};
|
||||
use database_entity::AFRole;
|
||||
|
||||
use shared_entity::dto::workspace_dto::{CreateWorkspaceMember, WorkspaceMemberChangeset};
|
||||
use shared_entity::error_code::ErrorCode;
|
||||
|
||||
use crate::user::utils::generate_unique_registered_user_client;
|
||||
|
||||
#[tokio::test]
|
||||
async fn add_workspace_members_not_enough_permission() {
|
||||
let (c1, _user1) = generate_unique_registered_user_client().await;
|
||||
let (c1, user1) = generate_unique_registered_user_client().await;
|
||||
let (c2, _user2) = generate_unique_registered_user_client().await;
|
||||
|
||||
let user2_workspace = c2.workspaces().await.unwrap();
|
||||
let user2_workspace_id = user2_workspace.first().unwrap().workspace_id;
|
||||
let workspace_id_2 = c2.workspaces().await.unwrap().first().unwrap().workspace_id;
|
||||
|
||||
// attempt to add user2 to user1's workspace
|
||||
// using user1's client
|
||||
let email = c1.token().read().as_ref().unwrap().user.email.to_owned();
|
||||
let err = c1
|
||||
.add_workspace_members(
|
||||
user2_workspace_id,
|
||||
workspace_id_2,
|
||||
vec![CreateWorkspaceMember {
|
||||
email,
|
||||
permission: WorkspacePermission::Member,
|
||||
email: user1.email,
|
||||
role: AFRole::Member,
|
||||
}],
|
||||
)
|
||||
.await
|
||||
|
@ -41,7 +39,7 @@ async fn add_workspace_members_then_delete() {
|
|||
c1_workspace_id,
|
||||
vec![CreateWorkspaceMember {
|
||||
email,
|
||||
permission: WorkspacePermission::Member,
|
||||
role: AFRole::Member,
|
||||
}],
|
||||
)
|
||||
.await
|
||||
|
@ -72,3 +70,152 @@ async fn add_workspace_members_then_delete() {
|
|||
.any(|w| w.email == *c2_email));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn workspace_member_add_new_member() {
|
||||
let (c1, _user1) = generate_unique_registered_user_client().await;
|
||||
let (c2, user2) = generate_unique_registered_user_client().await;
|
||||
let (_c3, user3) = generate_unique_registered_user_client().await;
|
||||
|
||||
let workspace_id = c1.workspaces().await.unwrap().first().unwrap().workspace_id;
|
||||
|
||||
c1.add_workspace_members(
|
||||
workspace_id,
|
||||
vec![CreateWorkspaceMember {
|
||||
email: user2.email,
|
||||
role: AFRole::Member,
|
||||
}],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let err = c2
|
||||
.add_workspace_members(
|
||||
workspace_id,
|
||||
vec![CreateWorkspaceMember {
|
||||
email: user3.email,
|
||||
role: AFRole::Member,
|
||||
}],
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(err.code, ErrorCode::NotEnoughPermissions);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn workspace_owner_add_new_owner() {
|
||||
let (c1, user1) = generate_unique_registered_user_client().await;
|
||||
let (_c2, user2) = generate_unique_registered_user_client().await;
|
||||
|
||||
let workspace_id = c1.workspaces().await.unwrap().first().unwrap().workspace_id;
|
||||
c1.add_workspace_members(
|
||||
workspace_id,
|
||||
vec![CreateWorkspaceMember {
|
||||
email: user2.email.clone(),
|
||||
role: AFRole::Owner,
|
||||
}],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let members = c1.get_workspace_members(workspace_id).await.unwrap();
|
||||
assert_eq!(members[0].email, user1.email);
|
||||
assert_eq!(members[0].role, AFRole::Owner);
|
||||
|
||||
assert_eq!(members[1].email, user2.email);
|
||||
assert_eq!(members[1].role, AFRole::Owner);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn workspace_second_owner_add_new_member() {
|
||||
let (c1, user1) = generate_unique_registered_user_client().await;
|
||||
let (c2, user2) = generate_unique_registered_user_client().await;
|
||||
let (_c3, user3) = generate_unique_registered_user_client().await;
|
||||
|
||||
let workspace_id = c1.workspaces().await.unwrap().first().unwrap().workspace_id;
|
||||
c1.add_workspace_members(
|
||||
workspace_id,
|
||||
vec![CreateWorkspaceMember {
|
||||
email: user2.email.clone(),
|
||||
role: AFRole::Owner,
|
||||
}],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
c2.add_workspace_members(
|
||||
workspace_id,
|
||||
vec![CreateWorkspaceMember {
|
||||
email: user3.email.clone(),
|
||||
role: AFRole::Member,
|
||||
}],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let members = c1.get_workspace_members(workspace_id).await.unwrap();
|
||||
assert_eq!(members[0].email, user1.email);
|
||||
assert_eq!(members[0].role, AFRole::Owner);
|
||||
|
||||
assert_eq!(members[1].email, user2.email);
|
||||
assert_eq!(members[1].role, AFRole::Owner);
|
||||
|
||||
assert_eq!(members[2].email, user3.email);
|
||||
assert_eq!(members[2].role, AFRole::Member);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn workspace_second_owner_can_not_delete_origin_owner() {
|
||||
let (c1, user1) = generate_unique_registered_user_client().await;
|
||||
let (c2, user2) = generate_unique_registered_user_client().await;
|
||||
|
||||
let workspace_id = c1.workspaces().await.unwrap().first().unwrap().workspace_id;
|
||||
c1.add_workspace_members(
|
||||
workspace_id,
|
||||
vec![CreateWorkspaceMember {
|
||||
email: user2.email.clone(),
|
||||
role: AFRole::Owner,
|
||||
}],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let err = c2
|
||||
.remove_workspace_members(workspace_id, [user1.email].to_vec())
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(err.code, ErrorCode::NotEnoughPermissions);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn workspace_owner_update_member_role() {
|
||||
let (c1, _user1) = generate_unique_registered_user_client().await;
|
||||
let (_c2, user2) = generate_unique_registered_user_client().await;
|
||||
|
||||
let workspace_id = c1.workspaces().await.unwrap().first().unwrap().workspace_id;
|
||||
c1.add_workspace_members(
|
||||
workspace_id,
|
||||
vec![CreateWorkspaceMember {
|
||||
email: user2.email.clone(),
|
||||
role: AFRole::Member,
|
||||
}],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let members = c1.get_workspace_members(workspace_id).await.unwrap();
|
||||
assert_eq!(members[1].email, user2.email);
|
||||
assert_eq!(members[1].role, AFRole::Member);
|
||||
|
||||
// Update user2's role to Owner
|
||||
c1.update_workspace_member(
|
||||
workspace_id,
|
||||
WorkspaceMemberChangeset::new(user2.email.clone()).with_role(AFRole::Owner),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let members = c1.get_workspace_members(workspace_id).await.unwrap();
|
||||
assert_eq!(members[1].role, AFRole::Owner);
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue