feat: client-api integration: save user meta data (#133)

* chore: update

* feat: get user workspace info

* feat: return list of workspace

* feat: return latest workspace id

* feat: latest workspace id

* test: add tests
This commit is contained in:
Nathan.fooo 2023-10-23 15:03:31 +08:00 committed by GitHub
parent d0d2e916a7
commit 7c503372e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 579 additions and 125 deletions

View file

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT * FROM public.af_workspace WHERE owner_uid = (\n SELECT uid FROM public.af_user WHERE uuid = $1\n )\n ",
"query": "\n SELECT * FROM public.af_workspace WHERE owner_uid = (\n SELECT uid FROM public.af_user WHERE uuid = $1\n )\n ",
"describe": {
"columns": [
{
@ -46,13 +46,13 @@
},
"nullable": [
false,
true,
true,
false,
false,
true,
false,
true,
true
]
},
"hash": "d37119628f436d151a5559e692a3d6a7a6bc3c8226631ce6a0bcd0c7f1c0a8c5"
"hash": "030b315f14742d266f545d6db37cc8cb083f9d52ebecd252311c4faf6fb5ab22"
}

View file

@ -0,0 +1,58 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT w.* \n FROM af_workspace w\n JOIN af_workspace_member wm ON w.workspace_id = wm.workspace_id\n WHERE wm.uid = (\n SELECT uid FROM public.af_user WHERE uuid = $1\n );\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "database_storage_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "owner_uid",
"type_info": "Int8"
},
{
"ordinal": 3,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "workspace_type",
"type_info": "Int4"
},
{
"ordinal": 5,
"name": "deleted_at",
"type_info": "Timestamptz"
},
{
"ordinal": 6,
"name": "workspace_name",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false,
true,
false,
true,
true
]
},
"hash": "5855567a45f990e03f9249e0c591ebb8c9949a9b7e5bec114535c99f89a2a1dd"
}

View file

@ -1,6 +1,6 @@
{
"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 ",
"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 -- 1. TODO(nathan): User must transfer ownership to another user first.\n -- 2. User must have at least one workspace\n AND uid <> (\n SELECT owner_uid FROM public.af_workspace WHERE workspace_id = $1\n );\n ",
"describe": {
"columns": [],
"parameters": {
@ -11,5 +11,5 @@
},
"nullable": []
},
"hash": "54c2e86c48a1549d8006bd5592eb5610985d5d43cd67efd417a0c6e52a967dfd"
"hash": "69ab5d0cc26ae1830849de97d53731dbddbf4189cca04706928848e932b7a72c"
}

View file

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT *\n FROM public.af_user_profile_view WHERE uuid = $1\n ",
"query": "\n SELECT *\n FROM public.af_user_profile_view WHERE uuid = $1\n ",
"describe": {
"columns": [
{
@ -78,5 +78,5 @@
true
]
},
"hash": "a4610ec5cb6fdccf7ef07a0526d35b7659a7e953b47b1e7b87188fa4b7277b1f"
"hash": "7438ddaf9ed9fab7ba4cb1b9c79ff541fa2e63a629cad4f8e9692a50a4e5e03c"
}

View file

@ -0,0 +1,58 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT * FROM public.af_workspace WHERE workspace_id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "database_storage_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "owner_uid",
"type_info": "Int8"
},
{
"ordinal": 3,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "workspace_type",
"type_info": "Int4"
},
{
"ordinal": 5,
"name": "deleted_at",
"type_info": "Timestamptz"
},
{
"ordinal": 6,
"name": "workspace_name",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false,
true,
false,
true,
true
]
},
"hash": "af5c5c1fbf22870171f644e70b0abe5a46d40b9bff68df3896adc5348539d27b"
}

View file

@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE af_workspace_member\n SET updated_at = CURRENT_TIMESTAMP\n WHERE uid = (SELECT uid FROM public.af_user WHERE uuid = $1) AND workspace_id = $2;\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Uuid"
]
},
"nullable": []
},
"hash": "d921f52e4bc3fef72c810e19455a2fa4fbd52f5a1f3a1838b146d001eadabd47"
}

View file

@ -2,10 +2,10 @@ use crate::notify::{ClientToken, TokenStateReceiver};
use anyhow::{anyhow, Context};
use bytes::Bytes;
use database_entity::dto::{
AFBlobMetadata, AFBlobRecord, AFCollabMember, AFCollabMembers, AFUserProfile, AFWorkspaceMember,
AFWorkspaces, BatchQueryCollabParams, BatchQueryCollabResult, CollabMemberIdentify,
DeleteCollabParams, InsertCollabMemberParams, InsertCollabParams, QueryCollabMembers,
QueryCollabParams, RawData, UpdateCollabMemberParams,
AFBlobMetadata, AFBlobRecord, AFCollabMember, AFCollabMembers, AFUserProfile,
AFUserWorkspaceInfo, AFWorkspace, AFWorkspaceMember, AFWorkspaces, BatchQueryCollabParams,
BatchQueryCollabResult, CollabMemberIdentify, DeleteCollabParams, InsertCollabMemberParams,
InsertCollabParams, QueryCollabMembers, QueryCollabParams, RawData, UpdateCollabMemberParams,
};
use futures_util::StreamExt;
use gotrue::grant::Grant;
@ -335,6 +335,19 @@ impl Client {
.into_data()
}
#[instrument(level = "debug", skip_all, err)]
pub async fn get_user_workspace_info(&self) -> Result<AFUserWorkspaceInfo, AppError> {
let url = format!("{}/api/user/workspace", self.base_url);
let resp = self
.http_client_with_auth(Method::GET, &url)
.await?
.send()
.await?;
AppResponse::<AFUserWorkspaceInfo>::from_response(resp)
.await?
.into_data()
}
#[instrument(level = "debug", skip_all, err)]
pub async fn get_workspaces(&self) -> Result<AFWorkspaces, AppError> {
let url = format!("{}/api/workspace/list", self.base_url);
@ -348,12 +361,25 @@ impl Client {
.into_data()
}
#[instrument(level = "debug", skip_all, err)]
pub async fn open_workspace(&self, workspace_id: &str) -> Result<AFWorkspace, AppError> {
let url = format!("{}/api/workspace/{}/open", self.base_url, workspace_id);
let resp = self
.http_client_with_auth(Method::PUT, &url)
.await?
.send()
.await?;
AppResponse::<AFWorkspace>::from_response(resp)
.await?
.into_data()
}
#[instrument(level = "debug", skip_all, err)]
pub async fn get_workspace_members(
&self,
workspace_uuid: Uuid,
workspace_id: &str,
) -> Result<Vec<AFWorkspaceMember>, AppError> {
let url = format!("{}/api/workspace/{}/member", self.base_url, workspace_uuid);
let url = format!("{}/api/workspace/{}/member", self.base_url, workspace_id);
let resp = self
.http_client_with_auth(Method::GET, &url)
.await?

View file

@ -1,4 +1,6 @@
use crate::pg_row::{AFBlobMetadataRow, AFUserProfileRow, AFWorkspaceMemberRow, AFWorkspaceRows};
use crate::error::DatabaseError;
use crate::pg_row::{AFBlobMetadataRow, AFUserProfileRow, AFWorkspaceMemberRow, AFWorkspaceRow};
use anyhow::anyhow;
use chrono::{DateTime, Utc};
use collab_entity::CollabType;
use serde::{Deserialize, Serialize};
@ -6,6 +8,7 @@ use serde_repr::{Deserialize_repr, Serialize_repr};
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
use tracing::error;
use uuid::Uuid;
use validator::{Validate, ValidationError};
#[derive(Debug, Clone, Validate, Serialize, Deserialize)]
@ -307,9 +310,94 @@ pub struct AFCollabMembers(pub Vec<AFCollabMember>);
pub type RawData = Vec<u8>;
#[derive(Serialize, Deserialize)]
pub struct AFUserProfile {
pub uid: i64,
pub uuid: Uuid,
pub email: Option<String>,
pub password: Option<String>,
pub name: Option<String>,
pub metadata: Option<serde_json::Value>,
pub encryption_sign: Option<String>,
pub latest_workspace_id: Uuid,
}
impl TryFrom<AFUserProfileRow> for AFUserProfile {
type Error = DatabaseError;
fn try_from(value: AFUserProfileRow) -> Result<Self, Self::Error> {
let uid = value
.uid
.ok_or(DatabaseError::Internal(anyhow!("Unexpect empty uid")))?;
let uuid = value
.uuid
.ok_or(DatabaseError::Internal(anyhow!("Unexpect empty uuid")))?;
let latest_workspace_id = value
.latest_workspace_id
.ok_or(DatabaseError::Internal(anyhow!(
"Unexpect empty latest_workspace_id"
)))?;
Ok(Self {
uid,
uuid,
email: value.email,
password: value.password,
name: value.name,
metadata: value.metadata,
encryption_sign: value.encryption_sign,
latest_workspace_id,
})
}
}
#[derive(Serialize, Deserialize)]
pub struct AFWorkspace {
pub workspace_id: Uuid,
pub database_storage_id: Uuid,
pub owner_uid: i64,
pub workspace_type: i32,
pub workspace_name: String,
pub created_at: DateTime<Utc>,
}
impl TryFrom<AFWorkspaceRow> for AFWorkspace {
type Error = DatabaseError;
fn try_from(value: AFWorkspaceRow) -> Result<Self, Self::Error> {
let owner_uid = value
.owner_uid
.ok_or(DatabaseError::Internal(anyhow!("Unexpect empty owner_uid")))?;
let database_storage_id = value
.database_storage_id
.ok_or(DatabaseError::Internal(anyhow!(
"Unexpect empty workspace_id"
)))?;
let workspace_name = value.workspace_name.unwrap_or_default();
let created_at = value.created_at.unwrap_or_else(Utc::now);
Ok(Self {
workspace_id: value.workspace_id,
database_storage_id,
owner_uid,
workspace_type: value.workspace_type,
workspace_name,
created_at,
})
}
}
#[derive(Serialize, Deserialize)]
pub struct AFWorkspaces(pub Vec<AFWorkspace>);
#[derive(Serialize, Deserialize)]
pub struct AFUserWorkspaceInfo {
pub user_profile: AFUserProfile,
pub visiting_workspace: AFWorkspace,
pub workspaces: Vec<AFWorkspace>,
}
/// ***************************************************************
/// Make alias for the database entity. Hiding the Sqlx Rows type.
pub type AFUserProfile = AFUserProfileRow;
pub type AFWorkspaces = AFWorkspaceRows;
pub type AFWorkspaceMember = AFWorkspaceMemberRow;
pub type AFBlobMetadata = AFBlobMetadataRow;

View file

@ -2,7 +2,6 @@ use crate::dto::AFRole;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use std::ops::Deref;
use uuid::Uuid;
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
@ -31,31 +30,6 @@ pub struct AFUserProfileRow {
pub latest_workspace_id: Option<Uuid>,
}
#[derive(Debug, FromRow, Deserialize, Serialize)]
pub struct AFWorkspaceRows(pub Vec<AFWorkspaceRow>);
impl Deref for AFWorkspaceRows {
type Target = Vec<AFWorkspaceRow>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<Vec<AFWorkspaceRow>> for AFWorkspaceRows {
fn from(v: Vec<AFWorkspaceRow>) -> Self {
Self(v)
}
}
impl AFWorkspaceRows {
pub fn get_latest(&self, profile: &AFUserProfileRow) -> Option<AFWorkspaceRow> {
match profile.latest_workspace_id {
Some(ws_id) => self.0.iter().find(|ws| ws.workspace_id == ws_id).cloned(),
None => None,
}
}
}
#[derive(FromRow, Serialize, Deserialize)]
pub struct AFWorkspaceMemberRow {
pub email: String,

View file

@ -10,24 +10,6 @@ use crate::user::select_uid_from_email;
use database_entity::error::DatabaseError;
use database_entity::pg_row::{AFUserProfileRow, AFWorkspaceMemberRow, AFWorkspaceRow};
pub async fn select_all_workspaces_owned(
pool: &PgPool,
owner_uuid: &Uuid,
) -> Result<Vec<AFWorkspaceRow>, DatabaseError> {
let workspaces = sqlx::query_as!(
AFWorkspaceRow,
r#"
SELECT * FROM public.af_workspace WHERE owner_uid = (
SELECT uid FROM public.af_user WHERE uuid = $1
)
"#,
owner_uuid
)
.fetch_all(pool)
.await?;
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(
@ -235,7 +217,9 @@ pub async fn delete_workspace_members(
AND uid = (
SELECT uid FROM public.af_user WHERE email = $2
)
-- Ensure the user to be deleted is not the original owner
-- Ensure the user to be deleted is not the original owner.
-- 1. TODO(nathan): User must transfer ownership to another user first.
-- 2. User must have at least one workspace
AND uid <> (
SELECT owner_uid FROM public.af_workspace WHERE workspace_id = $1
);
@ -293,19 +277,93 @@ pub async fn select_workspace_member(
Ok(member)
}
pub async fn select_user_profile_view_by_uuid(
pool: &PgPool,
pub async fn select_user_profile<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
user_uuid: &Uuid,
) -> Result<Option<AFUserProfileRow>, DatabaseError> {
let user_profile = sqlx::query_as!(
AFUserProfileRow,
r#"
SELECT *
FROM public.af_user_profile_view WHERE uuid = $1
"#,
SELECT *
FROM public.af_user_profile_view WHERE uuid = $1
"#,
user_uuid
)
.fetch_optional(pool)
.fetch_optional(executor)
.await?;
Ok(user_profile)
}
pub async fn select_workspace<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
workspace_id: &Uuid,
) -> Result<AFWorkspaceRow, DatabaseError> {
let workspace = sqlx::query_as!(
AFWorkspaceRow,
r#"
SELECT * FROM public.af_workspace WHERE workspace_id = $1
"#,
workspace_id
)
.fetch_one(executor)
.await?;
Ok(workspace)
}
pub async fn update_updated_at_of_workspace<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
user_uuid: &Uuid,
workspace_id: &Uuid,
) -> Result<(), DatabaseError> {
sqlx::query!(
r#"
UPDATE af_workspace_member
SET updated_at = CURRENT_TIMESTAMP
WHERE uid = (SELECT uid FROM public.af_user WHERE uuid = $1) AND workspace_id = $2;
"#,
user_uuid,
workspace_id
)
.execute(executor)
.await?;
Ok(())
}
/// Returns a list of workspaces that the user is a member of.
pub async fn select_user_workspace<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
user_uuid: &Uuid,
) -> Result<Vec<AFWorkspaceRow>, DatabaseError> {
let workspaces = sqlx::query_as!(
AFWorkspaceRow,
r#"
SELECT w.*
FROM af_workspace w
JOIN af_workspace_member wm ON w.workspace_id = wm.workspace_id
WHERE wm.uid = (
SELECT uid FROM public.af_user WHERE uuid = $1
);
"#,
user_uuid
)
.fetch_all(executor)
.await?;
Ok(workspaces)
}
pub async fn select_all_user_workspaces(
pool: &PgPool,
owner_uuid: &Uuid,
) -> Result<Vec<AFWorkspaceRow>, DatabaseError> {
let workspaces = sqlx::query_as!(
AFWorkspaceRow,
r#"
SELECT * FROM public.af_workspace WHERE owner_uid = (
SELECT uid FROM public.af_user WHERE uuid = $1
)
"#,
owner_uuid
)
.fetch_all(pool)
.await?;
Ok(workspaces)
}

View file

@ -49,12 +49,10 @@ impl WorkspaceMemberChangeset {
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

View file

@ -1,8 +1,8 @@
-- af_workspace contains all the workspaces. Each workspace contains a list of members defined in af_workspace_member
CREATE TABLE IF NOT EXISTS af_workspace (
workspace_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
database_storage_id UUID DEFAULT uuid_generate_v4(),
owner_uid BIGINT REFERENCES af_user(uid) ON DELETE CASCADE,
workspace_id UUID NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(),
database_storage_id UUID NOT NULL DEFAULT uuid_generate_v4(),
owner_uid BIGINT NOT NULL REFERENCES af_user(uid) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-- 0: Free
workspace_type INTEGER NOT NULL DEFAULT 0,

View file

@ -17,7 +17,8 @@ use actix_web::web::{Data, Json};
use actix_web::HttpRequest;
use actix_web::Result;
use actix_web::{web, HttpResponse, Scope};
use database_entity::pg_row::AFUserProfileRow;
use database_entity::dto::{AFUserProfile, AFUserWorkspaceInfo};
use tracing_actix_web::RequestId;
pub fn user_scope() -> Scope {
@ -26,6 +27,7 @@ pub fn user_scope() -> Scope {
.service(web::resource("/verify/{access_token}").route(web::get().to(verify_user_handler)))
.service(web::resource("/update").route(web::post().to(update_user_handler)))
.service(web::resource("/profile").route(web::get().to(get_user_profile_handler)))
.service(web::resource("/workspace").route(web::get().to(get_user_workspace_info_handler)))
// deprecated
.service(web::resource("/login").route(web::post().to(login_handler)))
@ -38,7 +40,7 @@ pub fn user_scope() -> Scope {
async fn verify_user_handler(
path: web::Path<String>,
state: Data<AppState>,
required_id: RequestId,
request_id: RequestId,
) -> Result<JsonAppResponse<SignInTokenResponse>> {
let access_token = path.into_inner();
let is_new = biz::user::token_verify(&state.pg_pool, &state.gotrue_client, &access_token).await?;
@ -50,18 +52,28 @@ async fn verify_user_handler(
async fn get_user_profile_handler(
uuid: UserUuid,
state: Data<AppState>,
required_id: RequestId,
) -> Result<JsonAppResponse<AFUserProfileRow>> {
request_id: RequestId,
) -> Result<JsonAppResponse<AFUserProfile>> {
let profile = biz::user::get_profile(&state.pg_pool, &uuid).await?;
Ok(AppResponse::Ok().with_data(profile).into())
}
#[tracing::instrument(skip(state), err)]
async fn get_user_workspace_info_handler(
uuid: UserUuid,
state: Data<AppState>,
request_id: RequestId,
) -> Result<JsonAppResponse<AFUserWorkspaceInfo>> {
let info = biz::user::get_user_workspace_info(&state.pg_pool, &uuid).await?;
Ok(AppResponse::Ok().with_data(info).into())
}
#[tracing::instrument(skip(state, auth, payload), err)]
async fn update_user_handler(
auth: Authorization,
payload: Json<UpdateUserParams>,
state: Data<AppState>,
required_id: RequestId,
request_id: RequestId,
) -> Result<JsonAppResponse<()>> {
let params = payload.into_inner();
biz::user::update_user(&state.pg_pool, auth.uuid()?, params).await?;

View file

@ -15,7 +15,7 @@ use shared_entity::data::{AppResponse, JsonAppResponse};
use shared_entity::dto::workspace_dto::*;
use shared_entity::error_code::ErrorCode;
use sqlx::types::uuid;
use tracing::{debug, instrument};
use tracing::{debug, event, instrument};
use tracing_actix_web::RequestId;
use uuid::Uuid;
@ -25,6 +25,7 @@ pub const COLLAB_OBJECT_ID_PATH: &str = "object_id";
pub fn workspace_scope() -> Scope {
web::scope("/api/workspace")
.service(web::resource("list").route(web::get().to(list_handler)))
.service(web::resource("{workspace_id}/open").route(web::put().to(open_workspace_handler)))
.service(
web::resource("{workspace_id}/member")
.route(web::get().to(get_workspace_members_handler))
@ -62,13 +63,27 @@ async fn list_handler(
uuid: UserUuid,
state: Data<AppState>,
) -> Result<JsonAppResponse<AFWorkspaces>> {
let workspaces = workspace::ops::get_workspaces(&state.pg_pool, &uuid).await?;
Ok(AppResponse::Ok().with_data(workspaces).into())
let rows = workspace::ops::get_all_user_workspaces(&state.pg_pool, &uuid).await?;
let workspaces = rows
.into_iter()
.flat_map(|row| {
let result = AFWorkspace::try_from(row);
if let Err(err) = &result {
event!(
tracing::Level::ERROR,
"Failed to convert workspace row to AFWorkspace: {:?}",
err
);
}
result
})
.collect::<Vec<_>>();
Ok(AppResponse::Ok().with_data(AFWorkspaces(workspaces)).into())
}
#[instrument(skip(payload, state), err)]
async fn add_workspace_members_handler(
required_id: RequestId,
request_id: RequestId,
user_uuid: UserUuid,
workspace_id: web::Path<Uuid>,
payload: Json<CreateWorkspaceMembers>,
@ -140,6 +155,17 @@ async fn remove_workspace_member_handler(
Ok(AppResponse::Ok().into())
}
#[instrument(skip_all, err)]
async fn open_workspace_handler(
user_uuid: UserUuid,
state: Data<AppState>,
workspace_id: web::Path<Uuid>,
) -> Result<JsonAppResponse<AFWorkspace>> {
let workspace_id = workspace_id.into_inner();
let workspace = workspace::ops::open_workspace(&state.pg_pool, &user_uuid, &workspace_id).await?;
Ok(AppResponse::Ok().with_data(workspace).into())
}
#[instrument(skip_all, err)]
async fn update_workspace_member_handler(
payload: Json<WorkspaceMemberChangeset>,

View file

@ -319,7 +319,7 @@ where
.get_collab_access_level(uid.into(), oid)
.await
.map_err(|err| {
to_database_error(
app_err_to_database_error(
err,
format!(
"failed to get the collab access level of user:{} for object:{}",
@ -336,7 +336,7 @@ where
.get_role_from_uid(uid, &workspace_id.parse()?)
.await
.map_err(|err| {
to_database_error(
app_err_to_database_error(
err,
format!(
"failed to get the role of the user:{} in workspace:{}",
@ -347,7 +347,7 @@ where
}
}
fn to_database_error(err: AppError, msg: String) -> DatabaseError {
fn app_err_to_database_error(err: AppError, msg: String) -> DatabaseError {
if err.is_record_not_found() {
DatabaseError::RecordNotFound(msg)
} else {

View file

@ -1,13 +1,17 @@
use anyhow::Result;
use database::{user::create_user_if_not_exists, workspace::select_user_profile_view_by_uuid};
use anyhow::{Context, Result};
use database::{user::create_user_if_not_exists, workspace::select_user_profile};
use gotrue::api::Client;
use serde_json::json;
use shared_entity::app_error::AppError;
use std::ops::DerefMut;
use uuid::Uuid;
use database_entity::pg_row::AFUserProfileRow;
use database::workspace::{select_user_workspace, select_workspace};
use database_entity::dto::{AFUserProfile, AFUserWorkspaceInfo, AFWorkspace};
use shared_entity::dto::auth_dto::UpdateUserParams;
use sqlx::{types::uuid, PgPool};
use tracing::instrument;
pub async fn token_verify(
pg_pool: &PgPool,
@ -21,16 +25,56 @@ pub async fn token_verify(
Ok(is_new)
}
pub async fn get_profile(
pg_pool: &PgPool,
uuid: &uuid::Uuid,
) -> Result<AFUserProfileRow, AppError> {
let profile = select_user_profile_view_by_uuid(pg_pool, uuid)
pub async fn get_profile(pg_pool: &PgPool, uuid: &Uuid) -> Result<AFUserProfile, AppError> {
let row = select_user_profile(pg_pool, uuid)
.await?
.ok_or(sqlx::Error::RowNotFound)?;
let profile = AFUserProfile::try_from(row)?;
Ok(profile)
}
#[instrument(skip(pg_pool), err)]
pub async fn get_user_workspace_info(
pg_pool: &PgPool,
uuid: &Uuid,
) -> Result<AFUserWorkspaceInfo, AppError> {
let mut txn = pg_pool
.begin()
.await
.context("failed to acquire the transaction to query the user workspace info")?;
let row = select_user_profile(txn.deref_mut(), uuid)
.await?
.ok_or(sqlx::Error::RowNotFound)?;
// Get the latest workspace that the user has visited recently
// TODO(nathan): the visiting_workspace might be None if the user get deleted from the workspace
let visiting_workspace = AFWorkspace::try_from(
select_workspace(txn.deref_mut(), &row.latest_workspace_id.unwrap()).await?,
)?;
// Get the user profile
let user_profile = AFUserProfile::try_from(row)?;
// Get all workspaces that the user can access to
let workspaces = select_user_workspace(txn.deref_mut(), uuid)
.await?
.into_iter()
.flat_map(|row| AFWorkspace::try_from(row).ok())
.collect::<Vec<AFWorkspace>>();
txn
.commit()
.await
.context("failed to commit the transaction to get user workspace info")?;
Ok(AFUserWorkspaceInfo {
user_profile,
visiting_workspace,
workspaces,
})
}
pub async fn update_user(
pg_pool: &PgPool,
user_uuid: Uuid,

View file

@ -3,23 +3,46 @@ use anyhow::Context;
use database::collab::upsert_collab_member_with_txn;
use database::user::select_uid_from_email;
use database::workspace::{
delete_workspace_members, insert_workspace_member_with_txn, select_all_workspaces_owned,
select_workspace_member_list, upsert_workspace_member,
delete_workspace_members, insert_workspace_member_with_txn, select_all_user_workspaces,
select_workspace, select_workspace_member_list, update_updated_at_of_workspace,
upsert_workspace_member,
};
use database_entity::dto::{AFAccessLevel, AFRole};
use database_entity::pg_row::{AFWorkspaceMemberRow, AFWorkspaceRows};
use database_entity::dto::{AFAccessLevel, AFRole, AFWorkspace};
use database_entity::pg_row::{AFWorkspaceMemberRow, AFWorkspaceRow};
use shared_entity::app_error::AppError;
use shared_entity::dto::workspace_dto::{CreateWorkspaceMember, WorkspaceMemberChangeset};
use sqlx::{types::uuid, PgPool};
use std::ops::DerefMut;
use uuid::Uuid;
pub async fn get_workspaces(
pub async fn get_all_user_workspaces(
pg_pool: &PgPool,
user_uuid: &Uuid,
) -> Result<AFWorkspaceRows, AppError> {
let workspaces = select_all_workspaces_owned(pg_pool, user_uuid).await?;
Ok(AFWorkspaceRows(workspaces))
) -> Result<Vec<AFWorkspaceRow>, AppError> {
let workspaces = select_all_user_workspaces(pg_pool, user_uuid).await?;
Ok(workspaces)
}
/// Returns the workspace with the given workspace_id and update the updated_at field of the
/// workspace.
pub async fn open_workspace(
pg_pool: &PgPool,
user_uuid: &Uuid,
workspace_id: &Uuid,
) -> Result<AFWorkspace, AppError> {
let mut txn = pg_pool
.begin()
.await
.context("Begin transaction to open workspace")?;
let row = select_workspace(txn.deref_mut(), workspace_id).await?;
update_updated_at_of_workspace(txn.deref_mut(), user_uuid, workspace_id).await?;
txn
.commit()
.await
.context("Commit transaction to open workspace")?;
let workspace = AFWorkspace::try_from(row)?;
Ok(workspace)
}
pub async fn add_workspace_members(

View file

@ -13,7 +13,7 @@ async fn collab_owner_permission_test() {
let raw_data = "hello world".to_string().as_bytes().to_vec();
let workspace_id = workspace_id_from_client(&c).await;
let object_id = Uuid::new_v4().to_string();
let uid = c.get_profile().await.unwrap().uid.unwrap();
let uid = c.get_profile().await.unwrap().uid;
c.create_collab(InsertCollabParams::new(
&object_id,
@ -42,7 +42,7 @@ async fn update_collab_member_permission_test() {
let raw_data = "hello world".to_string().as_bytes().to_vec();
let workspace_id = workspace_id_from_client(&c).await;
let object_id = Uuid::new_v4().to_string();
let uid = c.get_profile().await.unwrap().uid.unwrap();
let uid = c.get_profile().await.unwrap().uid;
c.create_collab(InsertCollabParams::new(
&object_id,
@ -91,7 +91,7 @@ async fn add_collab_member_test() {
// create new client
let (c_2, _user) = generate_unique_registered_user_client().await;
let uid_2 = c_2.get_profile().await.unwrap().uid.unwrap();
let uid_2 = c_2.get_profile().await.unwrap().uid;
// add new member
c_1
@ -136,7 +136,7 @@ async fn add_collab_member_then_remove_test() {
// Create new client
let (c_2, _user) = generate_unique_registered_user_client().await;
let uid_2 = c_2.get_profile().await.unwrap().uid.unwrap();
let uid_2 = c_2.get_profile().await.unwrap().uid;
// Add new member
c_1

View file

@ -12,6 +12,7 @@ pub(crate) async fn workspace_id_from_client(c: &Client) -> String {
c.get_workspaces()
.await
.unwrap()
.0
.first()
.unwrap()
.workspace_id

View file

@ -10,8 +10,8 @@ async fn edit_workspace_without_permission() {
let mut client_2 = TestClient::new_user().await;
let workspace_id = client_1.workspace_id().await;
client_1.open_workspace(&workspace_id).await;
client_2.open_workspace(&workspace_id).await;
client_1.edit_workspace_collab(&workspace_id).await;
client_2.edit_workspace_collab(&workspace_id).await;
client_1
.collab_by_object_id
@ -31,13 +31,13 @@ async fn init_sync_workspace_with_guest_permission() {
let mut client_1 = TestClient::new_user().await;
let mut client_2 = TestClient::new_user().await;
let workspace_id = client_1.workspace_id().await;
client_1.open_workspace(&workspace_id).await;
client_1.edit_workspace_collab(&workspace_id).await;
// add client 2 as the member of the workspace then the client 2 will receive the update.
client_1
.add_workspace_member(&workspace_id, &client_2, AFRole::Guest)
.await;
client_2.open_workspace(&workspace_id).await;
client_2.edit_workspace_collab(&workspace_id).await;
client_1
.collab_by_object_id
@ -57,7 +57,7 @@ async fn edit_workspace_with_guest_permission() {
let mut client_1 = TestClient::new_user().await;
let mut client_2 = TestClient::new_user().await;
let workspace_id = client_1.workspace_id().await;
client_1.open_workspace(&workspace_id).await;
client_1.edit_workspace_collab(&workspace_id).await;
// add client 2 as the member of the workspace then the client 2 will receive the update.
client_1
@ -73,7 +73,7 @@ async fn edit_workspace_with_guest_permission() {
.insert("name", "zack");
client_1.wait_object_sync_complete(&workspace_id).await;
client_2.open_workspace(&workspace_id).await;
client_2.edit_workspace_collab(&workspace_id).await;
// make sure the client 2 has received the remote updates before the client 2 edits the collab
client_2
.wait_object_sync_complete_with_secs(&workspace_id, 10)

View file

@ -153,5 +153,5 @@ async fn admin_generate_link_and_user_sign_in() {
assert!(is_new);
let workspaces = client.get_workspaces().await.unwrap();
assert_eq!(workspaces.len(), 1);
assert_eq!(workspaces.0.len(), 1);
}

View file

@ -66,9 +66,7 @@ async fn sign_in_success() {
let workspaces = c.get_workspaces().await.unwrap();
assert_eq!(workspaces.0.len(), 1);
let profile = c.get_profile().await.unwrap();
let latest_workspace = workspaces.get_latest(&profile);
assert!(latest_workspace.is_some());
let _ = c.get_profile().await.unwrap();
}
{

View file

@ -8,8 +8,8 @@ use collab::core::origin::{CollabClient, CollabOrigin};
use collab::preclude::Collab;
use collab_entity::CollabType;
use database_entity::dto::{
AFAccessLevel, AFBlobMetadata, AFRole, InsertCollabMemberParams, QueryCollabParams,
UpdateCollabMemberParams,
AFAccessLevel, AFBlobMetadata, AFRole, AFUserWorkspaceInfo, AFWorkspace, AFWorkspaceMember,
InsertCollabMemberParams, QueryCollabParams, UpdateCollabMemberParams,
};
use image::io::Reader as ImageReader;
use serde_json::Value;
@ -102,6 +102,14 @@ impl TestClient {
.unwrap();
}
pub(crate) async fn get_user_workspace_info(&self) -> AFUserWorkspaceInfo {
self.api_client.get_user_workspace_info().await.unwrap()
}
pub(crate) async fn open_workspace(&self, workspace_id: &str) -> AFWorkspace {
self.api_client.open_workspace(workspace_id).await.unwrap()
}
pub(crate) async fn try_update_workspace_member(
&self,
workspace_id: &str,
@ -144,6 +152,14 @@ impl TestClient {
.await
}
pub async fn get_workspace_members(&self, workspace_id: &str) -> Vec<AFWorkspaceMember> {
self
.api_client
.get_workspace_members(workspace_id)
.await
.unwrap()
}
pub(crate) async fn add_client_as_collab_member(
&self,
workspace_id: &str,
@ -253,6 +269,7 @@ impl TestClient {
.get_workspaces()
.await
.unwrap()
.0
.first()
.unwrap()
.workspace_id
@ -264,7 +281,7 @@ impl TestClient {
}
pub(crate) async fn uid(&self) -> i64 {
self.api_client.get_profile().await.unwrap().uid.unwrap()
self.api_client.get_profile().await.unwrap().uid
}
#[allow(clippy::await_holding_lock)]
@ -274,15 +291,13 @@ impl TestClient {
collab_type: CollabType,
) -> String {
let object_id = Uuid::new_v4().to_string();
let uid = self.api_client.get_profile().await.unwrap().uid.unwrap();
// Subscribe to object
let handler = self
.ws_client
.subscribe(BusinessID::CollabId, object_id.clone())
.unwrap();
let (sink, stream) = (handler.sink(), handler.stream());
let origin = CollabOrigin::Client(CollabClient::new(uid, self.device_id.clone()));
let origin = CollabOrigin::Client(CollabClient::new(self.uid().await, self.device_id.clone()));
let collab = Arc::new(MutexCollab::new(origin.clone(), &object_id, vec![]));
let ws_connect_state = self.ws_client.subscribe_connect_state();
@ -309,7 +324,7 @@ impl TestClient {
object_id
}
pub(crate) async fn open_workspace(&mut self, workspace_id: &str) {
pub(crate) async fn edit_workspace_collab(&mut self, workspace_id: &str) {
self
.open_collab(workspace_id, workspace_id, CollabType::Folder)
.await;
@ -322,15 +337,13 @@ impl TestClient {
object_id: &str,
collab_type: CollabType,
) {
let uid = self.api_client.get_profile().await.unwrap().uid.unwrap();
// Subscribe to object
let handler = self
.ws_client
.subscribe(BusinessID::CollabId, object_id.to_string())
.unwrap();
let (sink, stream) = (handler.sink(), handler.stream());
let origin = CollabOrigin::Client(CollabClient::new(uid, self.device_id.clone()));
let origin = CollabOrigin::Client(CollabClient::new(self.uid().await, self.device_id.clone()));
let collab = Arc::new(MutexCollab::new(origin.clone(), object_id, vec![]));
let ws_connect_state = self.ws_client.subscribe_connect_state();

View file

@ -156,6 +156,23 @@ async fn add_workspace_member_and_owner_then_delete_all() {
assert_eq!(members[0].email, c1.email().await);
}
#[tokio::test]
async fn workspace_owner_remove_self_from_workspace() {
let c1 = TestClient::new_user_without_ws_conn().await;
let workspace_id = c1.workspace_id().await;
// the workspace owner can not remove 'self' from the workspace
let error = c1
.try_remove_workspace_member(&workspace_id, &c1)
.await
.unwrap_err();
assert_eq!(error.code, ErrorCode::NotEnoughPermissions);
let members = c1.get_workspace_members(&workspace_id).await;
assert_eq!(members.len(), 1);
assert_eq!(members[0].email, c1.email().await);
}
#[tokio::test]
async fn workspace_second_owner_can_not_delete_origin_owner() {
let c1 = TestClient::new_user_without_ws_conn().await;
@ -170,3 +187,48 @@ async fn workspace_second_owner_can_not_delete_origin_owner() {
.unwrap_err();
assert_eq!(error.code, ErrorCode::NotEnoughPermissions);
}
#[tokio::test]
async fn user_workspace_info() {
let c1 = TestClient::new_user_without_ws_conn().await;
let workspace_id = c1.workspace_id().await;
let info = c1.get_user_workspace_info().await;
assert_eq!(info.workspaces.len(), 1);
assert_eq!(
info.visiting_workspace.workspace_id.to_string(),
workspace_id
);
let c2 = TestClient::new_user_without_ws_conn().await;
c1.add_workspace_member(&workspace_id, &c2, AFRole::Owner)
.await;
// c2 should have 2 workspaces
let info = c2.get_user_workspace_info().await;
assert_eq!(info.workspaces.len(), 2);
}
#[tokio::test]
async fn get_user_workspace_info_after_open_workspace() {
let c1 = TestClient::new_user_without_ws_conn().await;
let workspace_id_c1 = c1.workspace_id().await;
let c2 = TestClient::new_user_without_ws_conn().await;
c1.add_workspace_member(&workspace_id_c1, &c2, AFRole::Owner)
.await;
let info = c2.get_user_workspace_info().await;
let workspace_id_c2 = c1.workspace_id().await;
assert_eq!(
info.visiting_workspace.workspace_id.to_string(),
workspace_id_c2
);
// After open workspace, the visiting workspace should be the workspace that user just opened
c2.open_workspace(&workspace_id_c1).await;
let info = c2.get_user_workspace_info().await;
assert_eq!(
info.visiting_workspace.workspace_id.to_string(),
workspace_id_c1
);
}