Merge pull request #328 from AppFlowy-IO/workspace-add-email

feat: send email and create user if adding member but member not exist
This commit is contained in:
Zack 2024-02-21 18:08:25 +08:00 committed by GitHub
commit 5e7794646a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 281 additions and 133 deletions

View File

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO af_collab_snapshot (oid, blob, len, encrypt, workspace_id) \n VALUES ($1, $2, $3, $4, $5)\n RETURNING sid AS snapshot_id, oid AS object_id, created_at\n ",
"query": "\n INSERT INTO af_collab_snapshot (oid, blob, len, encrypt, workspace_id)\n VALUES ($1, $2, $3, $4, $5)\n RETURNING sid AS snapshot_id, oid AS object_id, created_at\n ",
"describe": {
"columns": [
{
@ -34,5 +34,5 @@
false
]
},
"hash": "5144da61424400216e10d9327f5136f77d96a83d462bf815f13ebe70d88fada3"
"hash": "f18d6e075a522b0ce5935351dd57ab0dda4d8b4ed3881c2ad0bc09c07c43e6fe"
}

View File

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT sid as \"snapshot_id\", oid as \"object_id\", created_at\n FROM af_collab_snapshot \n WHERE oid = $1 AND deleted_at IS NULL\n ORDER BY created_at DESC;\n ",
"query": "\n SELECT sid as \"snapshot_id\", oid as \"object_id\", created_at\n FROM af_collab_snapshot\n WHERE oid = $1 AND deleted_at IS NULL\n ORDER BY created_at DESC;\n ",
"describe": {
"columns": [
{
@ -30,5 +30,5 @@
false
]
},
"hash": "eff32b7631cb1b715776c66749706943c7ba23eac358cae6d36537991a8f8975"
"hash": "f409626142553d4496d15b5dfa7da8a5a238da86f56c930c09a261f2efa1f55c"
}

View File

@ -146,7 +146,7 @@ pub async fn admin_users_handler(
) -> Result<Html<String>, WebAppError> {
let users = state
.gotrue_client
.admin_list_user(&session.token.access_token)
.admin_list_user(&session.token.access_token, None)
.await
.map_or_else(
|err| {

View File

@ -408,20 +408,32 @@ impl Client {
email: &str,
password: &str,
) -> Result<User, AppResponseError> {
Ok(
self
.gotrue_client
.admin_add_user(
&self.access_token()?,
&AdminUserParams {
email: email.to_owned(),
password: Some(password.to_owned()),
email_confirm: true,
..Default::default()
},
)
.await?,
)
let user = self
.gotrue_client
.admin_add_user(
&self.access_token()?,
&AdminUserParams {
email: email.to_owned(),
password: Some(password.to_owned()),
email_confirm: true,
..Default::default()
},
)
.await?;
Ok(user)
}
// filter is postgre sql like filter
#[instrument(level = "debug", skip_all, err)]
pub async fn admin_list_users(
&self,
filter: Option<&str>,
) -> Result<Vec<User>, AppResponseError> {
let user = self
.gotrue_client
.admin_list_user(&self.access_token()?, filter)
.await?;
Ok(user.users)
}
/// Only expose this method for testing

View File

@ -318,7 +318,7 @@ pub async fn should_create_snapshot<'a, E: Executor<'a, Database = Postgres>>(
) -> Result<bool, sqlx::Error> {
let hours = Utc::now() - Duration::hours(SNAPSHOT_PER_HOUR);
let latest_snapshot_time: Option<chrono::DateTime<Utc>> = sqlx::query_scalar(
"SELECT created_at FROM af_collab_snapshot
"SELECT created_at FROM af_collab_snapshot
WHERE oid = $1 ORDER BY created_at DESC LIMIT 1",
)
.bind(oid)
@ -344,7 +344,7 @@ pub(crate) async fn create_snapshot_and_maintain_limit<'a>(
let snapshot_meta = sqlx::query_as!(
AFSnapshotMeta,
r#"
INSERT INTO af_collab_snapshot (oid, blob, len, encrypt, workspace_id)
INSERT INTO af_collab_snapshot (oid, blob, len, encrypt, workspace_id)
VALUES ($1, $2, $3, $4, $5)
RETURNING sid AS snapshot_id, oid AS object_id, created_at
"#,
@ -360,7 +360,7 @@ pub(crate) async fn create_snapshot_and_maintain_limit<'a>(
// When a new snapshot is created that surpasses the preset limit, older snapshots will be deleted to maintain the limit
sqlx::query(
r#"
DELETE FROM af_collab_snapshot
DELETE FROM af_collab_snapshot
WHERE oid = $1 AND sid NOT IN ( SELECT sid FROM af_collab_snapshot WHERE oid = $1 ORDER BY created_at DESC LIMIT $2)
"#,
)
@ -403,7 +403,7 @@ pub async fn get_all_collab_snapshot_meta(
AFSnapshotMeta,
r#"
SELECT sid as "snapshot_id", oid as "object_id", created_at
FROM af_collab_snapshot
FROM af_collab_snapshot
WHERE oid = $1 AND deleted_at IS NULL
ORDER BY created_at DESC;
"#,

View File

@ -366,6 +366,7 @@ pub async fn select_workspace<'a, E: Executor<'a, Database = Postgres>>(
.await?;
Ok(workspace)
}
#[inline]
pub async fn update_updated_at_of_workspace<'a, E: Executor<'a, Database = Postgres>>(
executor: E,

View File

@ -1,7 +1,7 @@
use super::grant::Grant;
use crate::params::{
AdminDeleteUserParams, AdminUserParams, CreateSSOProviderParams, GenerateLinkParams,
GenerateLinkResponse, MagicLinkParams,
GenerateLinkResponse, InviteUserParams, MagicLinkParams,
};
use anyhow::Context;
use gotrue_entity::dto::{
@ -139,12 +139,14 @@ impl Client {
pub async fn admin_list_user(
&self,
access_token: &str,
filter: Option<&str>,
) -> Result<AdminListUsersResponse, GoTrueError> {
let url = format!("{}/admin/users", self.base_url);
let resp = self
.http_client_with_auth(Method::GET, &url, access_token)
.send()
.await?;
let mut req = self.http_client_with_auth(Method::GET, &url, access_token);
if let Some(filter) = filter {
req = req.query(&[("filter", filter)]);
}
let resp = req.send().await?;
to_gotrue_result(resp).await
}
@ -191,6 +193,20 @@ impl Client {
to_gotrue_result(resp).await
}
pub async fn admin_invite_user(
&self,
access_token: &str,
admin_user_params: &InviteUserParams,
) -> Result<User, GoTrueError> {
let url = format!("{}/invite", self.base_url);
let resp = self
.http_client_with_auth(Method::POST, &url, access_token)
.json(&admin_user_params)
.send()
.await?;
to_gotrue_result(resp).await
}
pub async fn admin_add_user(
&self,
access_token: &str,

View File

@ -8,6 +8,12 @@ pub struct AdminDeleteUserParams {
pub should_soft_delete: bool,
}
#[derive(Default, Serialize)]
pub struct InviteUserParams {
pub email: String,
pub data: serde_json::Value,
}
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct AdminUserParams {
pub aud: String,

View File

@ -176,15 +176,13 @@ async fn add_workspace_members_handler(
state: Data<AppState>,
) -> Result<JsonAppResponse<()>> {
let create_members = payload.into_inner();
let role_by_uid = workspace::ops::add_workspace_members(
&state.pg_pool,
&user_uuid,
&workspace_id,
create_members.0,
)
.await?;
let role_by_uid =
workspace::ops::add_workspace_members(&state, &user_uuid, &workspace_id, create_members.0)
.await?;
for (uid, role) in role_by_uid {
// TODO: use one single query to insert all roles
state
.workspace_access_control
.insert_workspace_role(&uid, &workspace_id, role)

View File

@ -1,5 +1,32 @@
use crate::api::metrics::metrics_scope;
use crate::component::auth::HEADER_TOKEN;
use crate::config::config::{Config, DatabaseSetting, GoTrueSetting, S3Setting};
use crate::middleware::request_id::RequestIdMiddleware;
use crate::self_signed::create_self_signed_certificate;
use crate::state::{AppState, UserCache};
use actix_identity::IdentityMiddleware;
use actix_session::storage::RedisSessionStore;
use actix_session::SessionMiddleware;
use actix_web::cookie::Key;
use actix_web::{dev::Server, web, web::Data, App, HttpServer};
use actix::Actor;
use anyhow::{Context, Error};
use app_error::AppError;
use gotrue::grant::{Grant, PasswordGrant};
use openssl::ssl::{SslAcceptor, SslAcceptorBuilder, SslFiletype, SslMethod};
use openssl::x509::X509;
use secrecy::{ExposeSecret, Secret};
use snowflake::Snowflake;
use sqlx::{postgres::PgPoolOptions, PgPool};
use std::net::TcpListener;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
use tracing::info;
use crate::api::file_storage::file_storage_scope;
use crate::api::user::user_scope;
use crate::api::workspace::{collab_scope, workspace_scope};
@ -10,32 +37,11 @@ use crate::biz::collab::storage::init_collab_storage;
use crate::biz::pg_listener::PgListeners;
use crate::biz::user::RealtimeUserImpl;
use crate::biz::workspace::access_control::WorkspaceHttpAccessControl;
use crate::component::auth::HEADER_TOKEN;
use crate::config::config::{Config, DatabaseSetting, GoTrueSetting, S3Setting};
use crate::middleware::access_control_mw::WorkspaceAccessControl;
use crate::middleware::metrics_mw::MetricsMiddleware;
use crate::middleware::request_id::RequestIdMiddleware;
use crate::self_signed::create_self_signed_certificate;
use crate::state::{AppMetrics, AppState, UserCache};
use actix::Actor;
use actix_identity::IdentityMiddleware;
use actix_session::storage::RedisSessionStore;
use actix_session::SessionMiddleware;
use actix_web::cookie::Key;
use actix_web::{dev::Server, web, web::Data, App, HttpServer};
use anyhow::{Context, Error};
use crate::state::AppMetrics;
use database::file::bucket_s3_impl::S3BucketStorage;
use openssl::ssl::{SslAcceptor, SslAcceptorBuilder, SslFiletype, SslMethod};
use openssl::x509::X509;
use realtime::collaborate::CollabServer;
use secrecy::{ExposeSecret, Secret};
use snowflake::Snowflake;
use sqlx::{postgres::PgPoolOptions, PgPool};
use std::net::TcpListener;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
use tracing::info;
pub struct Application {
port: u16,
@ -167,6 +173,12 @@ pub async fn init_state(config: &Config) -> Result<AppState, Error> {
let gotrue_client = get_gotrue_client(&config.gotrue).await?;
setup_admin_account(&gotrue_client, &pg_pool, &config.gotrue).await?;
// Gotrue Admin
let gotrue_admin = GoTrueAdmin::new(
config.gotrue.admin_email.clone(),
config.gotrue.admin_password.clone(),
);
// Redis
info!("Connecting to Redis...");
let redis_client = get_redis_client(config.redis_uri.expose_secret()).await?;
@ -207,6 +219,7 @@ pub async fn init_state(config: &Config) -> Result<AppState, Error> {
users: Arc::new(users),
id_gen: Arc::new(RwLock::new(Snowflake::new(1))),
gotrue_client,
gotrue_admin,
redis_client,
collab_storage,
collab_access_control,
@ -218,6 +231,31 @@ pub async fn init_state(config: &Config) -> Result<AppState, Error> {
})
}
#[derive(Debug, Clone)]
pub struct GoTrueAdmin {
pub admin_email: String,
pub password: Secret<String>,
}
impl GoTrueAdmin {
pub fn new(admin_email: String, password: String) -> Self {
Self {
admin_email,
password: password.into(),
}
}
pub async fn token(&self, client: &gotrue::api::Client) -> Result<String, AppError> {
let token = client
.token(&Grant::Password(PasswordGrant {
email: self.admin_email.clone(),
password: self.password.expose_secret().clone(),
}))
.await?;
Ok(token.access_token)
}
}
async fn setup_admin_account(
gotrue_client: &gotrue::api::Client,
pg_pool: &PgPool,

View File

@ -1,4 +1,5 @@
use anyhow::{Context, Result};
use gotrue_entity::dto::User;
use crate::biz::workspace::access_control::WorkspaceAccessControl;
use crate::state::AppState;
@ -26,15 +27,30 @@ use workspace_template::{WorkspaceTemplate, WorkspaceTemplateBuilder};
#[instrument(skip_all, err)]
pub async fn verify_token(access_token: &str, state: &AppState) -> Result<bool, AppError> {
let user = state.gotrue_client.user_info(access_token).await?;
let user_uuid = uuid::Uuid::parse_str(&user.id)?;
let name = name_from_user_metadata(&user.user_metadata);
let mut txn = state
.pg_pool
.begin()
.await
.context("acquire transaction to verify token")?;
let is_new = create_user_if_not_exists(&user, &mut txn, state).await;
txn
.commit()
.await
.context("fail to commit transaction to verify token")?;
is_new
}
/// Returns if user is new
pub async fn create_user_if_not_exists(
user: &User,
txn: &mut Transaction<'_, sqlx::Postgres>,
state: &AppState,
) -> Result<bool, AppError> {
let user_uuid = uuid::Uuid::parse_str(&user.id)?;
let name = name_from_user_metadata(&user.user_metadata);
// To prevent concurrent creation of the same user with the same workspace resources, we lock
// the user row when `verify_token` is called. This means that if multiple requests try to
// create the same user simultaneously, the first request will acquire the lock, create the user,
@ -67,16 +83,12 @@ pub async fn verify_token(access_token: &str, state: &AppState) -> Result<bool,
create_workspace_for_user(
new_uid,
&workspace_id,
&mut txn,
txn,
vec![GetStartedDocumentTemplate],
state,
)
.await?;
}
txn
.commit()
.await
.context("fail to commit transaction to verify token")?;
Ok(is_new)
}

View File

@ -1,3 +1,4 @@
use app_error::AppError;
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::io::{self, AsyncRead, ReadBuf};
@ -42,3 +43,19 @@ where
&self.reader
}
}
pub async fn check_user_exists(
admin_token: &str,
gotrue_client: &gotrue::api::Client,
email: &str,
) -> Result<bool, AppError> {
let users = gotrue_client
.admin_list_user(admin_token, Some(email))
.await?;
for user in users.users {
if user.email == email {
return Ok(true);
}
}
Ok(false)
}

View File

@ -9,6 +9,7 @@ use database::workspace::{
select_workspace_member_list, update_updated_at_of_workspace, upsert_workspace_member,
};
use database_entity::dto::{AFAccessLevel, AFRole, AFWorkspace};
use gotrue::params::InviteUserParams;
use shared_entity::dto::workspace_dto::{CreateWorkspaceMember, WorkspaceMemberChangeset};
use shared_entity::response::AppResponseError;
use sqlx::{types::uuid, PgPool};
@ -17,6 +18,10 @@ use std::ops::DerefMut;
use tracing::instrument;
use uuid::Uuid;
use crate::biz::user::create_user_if_not_exists;
use crate::biz::utils::check_user_exists;
use crate::state::AppState;
pub async fn delete_workspace_for_user(
pg_pool: &PgPool,
workspace_id: &Uuid,
@ -83,16 +88,52 @@ pub async fn open_workspace(
///
#[instrument(level = "debug", skip_all, err)]
pub async fn add_workspace_members(
pg_pool: &PgPool,
state: &AppState,
_user_uuid: &Uuid,
workspace_id: &Uuid,
members: Vec<CreateWorkspaceMember>,
) -> Result<HashMap<i64, AFRole>, AppError> {
let mut txn = pg_pool
// Invite user to workspace if user is not registered
let admin_token = state.gotrue_admin.token(&state.gotrue_client).await?;
let mut txn = state
.pg_pool
.begin()
.await
.context("Begin transaction to insert workspace members")?;
let gotrue_client = &state.gotrue_client;
for member in members.iter() {
let user_exists = check_user_exists(&admin_token, gotrue_client, &member.email).await?;
if !user_exists {
let user = gotrue_client
.admin_invite_user(
&admin_token,
&InviteUserParams {
email: member.email.clone(),
..Default::default()
},
)
.await?;
let is_new = create_user_if_not_exists(&user, &mut txn, state).await?;
if !is_new {
tracing::error!("User should be new but is not new. User: {:?}", user);
}
}
}
let role_by_uid = add_workspace_members_db(workspace_id, members, &mut txn).await?;
txn
.commit()
.await
.context("Commit transaction to insert workspace members")?;
Ok(role_by_uid)
}
pub async fn add_workspace_members_db(
workspace_id: &Uuid,
members: Vec<CreateWorkspaceMember>,
txn: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<HashMap<i64, AFRole>, AppError> {
let mut role_by_uid = HashMap::new();
for member in members.into_iter() {
let access_level = match &member.role {
@ -106,16 +147,11 @@ pub async fn add_workspace_members(
// "Failed to get uid from email {} when adding workspace members",
// member.email
// ))?;
insert_workspace_member_with_txn(&mut txn, workspace_id, &member.email, member.role.clone())
.await?;
upsert_collab_member_with_txn(uid, workspace_id.to_string(), &access_level, &mut txn).await?;
insert_workspace_member_with_txn(txn, workspace_id, &member.email, member.role.clone()).await?;
upsert_collab_member_with_txn(uid, workspace_id.to_string(), &access_level, txn).await?;
role_by_uid.insert(uid, member.role);
}
txn
.commit()
.await
.context("Commit transaction to insert workspace members")?;
Ok(role_by_uid)
}

View File

@ -1,3 +1,4 @@
use crate::application::GoTrueAdmin;
use crate::biz::casbin::{CollabAccessControlImpl, WorkspaceAccessControlImpl};
use crate::biz::collab::storage::CollabPostgresDBStorage;
use crate::biz::pg_listener::PgListeners;
@ -29,6 +30,7 @@ pub struct AppState {
pub users: Arc<UserCache>,
pub id_gen: Arc<RwLock<Snowflake>>,
pub gotrue_client: gotrue::api::Client,
pub gotrue_admin: GoTrueAdmin,
pub redis_client: RedisClient,
pub collab_storage: Arc<CollabPostgresDBStorage>,
pub collab_access_control: CollabAccessControlImpl,

View File

@ -1,7 +1,6 @@
use crate::access_control::*;
use actix_http::Method;
use anyhow::{anyhow, Context};
use appflowy_cloud::biz;
use appflowy_cloud::biz::casbin::access_control::{Action, ActionType, ObjectType};
use database_entity::dto::{AFAccessLevel, AFRole};
@ -42,10 +41,7 @@ async fn test_collab_access_control(pool: PgPool) -> anyhow::Result<()> {
role: AFRole::Guest,
},
];
let _ =
biz::workspace::ops::add_workspace_members(&pool, &user.uuid, &workspace.workspace_id, members)
.await
.context("adding users to workspace")?;
let _ = add_workspace_members_in_tx(&pool, &workspace.workspace_id, members).await;
// user that created the workspace should have full access
assert_access_level(
@ -148,18 +144,15 @@ async fn test_collab_access_control_access_http_method(pool: PgPool) -> anyhow::
.next()
.ok_or(anyhow!("workspace should be created"))?;
let _ = biz::workspace::ops::add_workspace_members(
let _ = add_workspace_members_in_tx(
&pool,
&guest.uuid,
&workspace.workspace_id,
vec![CreateWorkspaceMember {
email: guest.email,
role: AFRole::Guest,
}],
)
.await
.context("adding users to workspace")
.unwrap();
.await;
for method in [Method::GET, Method::POST, Method::PUT, Method::DELETE] {
assert_can_access_http_method(
@ -243,17 +236,15 @@ async fn test_collab_access_control_send_receive_collab_update(pool: PgPool) ->
.next()
.ok_or(anyhow!("workspace should be created"))?;
let _ = biz::workspace::ops::add_workspace_members(
let _ = add_workspace_members_in_tx(
&pool,
&guest.uuid,
&workspace.workspace_id,
vec![CreateWorkspaceMember {
email: guest.email,
role: AFRole::Guest,
}],
)
.await
.context("adding users to workspace")?;
.await;
// Need to wait for the listener(spawn_listen_on_workspace_member_change) to receive the event
sleep(Duration::from_secs(2)).await;

View File

@ -1,5 +1,6 @@
use crate::access_control::{
assert_workspace_role, assert_workspace_role_error, create_user, setup_access_control,
add_workspace_members_in_tx, assert_workspace_role, assert_workspace_role_error, create_user,
setup_access_control,
};
use anyhow::{anyhow, Context};
use app_error::ErrorCode;
@ -33,17 +34,16 @@ async fn test_workspace_access_control_get_role(pool: PgPool) -> anyhow::Result<
.await;
let member = create_user(&pool).await?;
let _ = biz::workspace::ops::add_workspace_members(
let _ = add_workspace_members_in_tx(
&pool,
&member.uuid,
&workspace.workspace_id,
vec![CreateWorkspaceMember {
email: member.email.clone(),
role: AFRole::Member,
}],
)
.await
.context("adding users to workspace")?;
.await;
assert_workspace_role(
&workspace_access_control,

View File

@ -1,18 +1,21 @@
use actix_http::Method;
use anyhow::{Context, Error};
use app_error::ErrorCode;
use appflowy_cloud::biz;
use appflowy_cloud::biz::casbin::{CollabAccessControlImpl, WorkspaceAccessControlImpl};
use appflowy_cloud::biz::workspace::access_control::WorkspaceAccessControl;
use client_api_test_util::setup_log;
use database_entity::dto::{AFAccessLevel, AFRole};
use lazy_static::lazy_static;
use realtime::collaborate::CollabAccessControl;
use shared_entity::dto::workspace_dto::CreateWorkspaceMember;
use snowflake::Snowflake;
use sqlx::PgPool;
use std::sync::Arc;
use casbin::{CoreApi, DefaultModel, Enforcer};
use dashmap::DashMap;
use std::collections::HashMap;
use std::time::Duration;
use tokio::sync::RwLock;
use tokio::time::{interval, timeout};
@ -307,3 +310,22 @@ pub async fn assert_can_access_http_method(
timeout(timeout_duration, operation).await?;
Ok(())
}
pub async fn add_workspace_members_in_tx(
pool: &PgPool,
workspace_id: &Uuid,
members: Vec<CreateWorkspaceMember>,
) -> HashMap<i64, AFRole> {
let mut txn = pool
.begin()
.await
.expect("acquire transaction to add workspace members");
let res = biz::workspace::ops::add_workspace_members_db(workspace_id, members, &mut txn)
.await
.expect("add workspace members");
txn
.commit()
.await
.expect("commit transaction to add workspace members");
res
}

View File

@ -1,8 +1,6 @@
use crate::access_control::*;
use anyhow::anyhow;
use appflowy_cloud::biz;
use appflowy_cloud::biz::casbin::access_control::{Action, ObjectType, ToCasbinAction};
use casbin::CoreApi;
use database_entity::dto::{AFAccessLevel, AFRole};
@ -96,14 +94,7 @@ async fn test_add_users_to_workspace(pool: PgPool) -> anyhow::Result<()> {
role: AFRole::Guest,
},
];
let _ = biz::workspace::ops::add_workspace_members(
&pool,
&user_main.uuid,
&workspace.workspace_id,
members,
)
.await
.context("adding users to workspace")?;
let _ = add_workspace_members_in_tx(&pool, &workspace.workspace_id, members).await;
let enforcer = setup_enforcer(&pool).await?;
{
// Owner
@ -232,14 +223,8 @@ async fn test_reload_policy_after_adding_user_to_workspace(pool: PgPool) -> anyh
email: user_member.email.clone(),
role: AFRole::Member,
}];
let _ = biz::workspace::ops::add_workspace_members(
&pool,
&user_owner.uuid,
&workspace.workspace_id,
members,
)
.await
.context("adding users to workspace")?;
let _ = add_workspace_members_in_tx(&pool, &workspace.workspace_id, members).await;
assert!(!enforcer
.enforce((

View File

@ -47,7 +47,7 @@ async fn admin_user_create_list_edit_delete() {
// list users
let users = gotrue_client
.admin_list_user(&admin_token.access_token)
.admin_list_user(&admin_token.access_token, None)
.await
.unwrap()
.users;
@ -94,7 +94,7 @@ async fn admin_user_create_list_edit_delete() {
.unwrap();
let users = gotrue_client
.admin_list_user(&admin_token.access_token)
.admin_list_user(&admin_token.access_token, None)
.await
.unwrap()
.users;

View File

@ -1,5 +1,5 @@
use app_error::ErrorCode;
use client_api_test_util::TestClient;
use client_api_test_util::{admin_user_client, TestClient};
use database_entity::dto::AFRole;
use shared_entity::dto::workspace_dto::CreateWorkspaceMember;
@ -36,25 +36,6 @@ async fn add_duplicate_workspace_members() {
.await;
}
#[tokio::test]
async fn add_not_exist_workspace_members() {
let c1 = TestClient::new_user_without_ws_conn().await;
let workspace_id = c1.workspace_id().await;
let email = format!("{}@appflowy.io", uuid::Uuid::new_v4());
let err = c1
.api_client
.add_workspace_members(
workspace_id,
vec![CreateWorkspaceMember {
email,
role: AFRole::Member,
}],
)
.await
.unwrap_err();
assert_eq!(err.code, ErrorCode::RecordNotFound);
}
#[tokio::test]
async fn update_workspace_member_role_not_enough_permission() {
let c1 = TestClient::new_user_without_ws_conn().await;
@ -253,3 +234,34 @@ async fn get_user_workspace_info_after_open_workspace() {
workspace_id_c1
);
}
#[tokio::test]
async fn add_workspace_member_not_exists() {
let c1 = TestClient::new_user_without_ws_conn().await;
let workspace_id_c1 = c1.workspace_id().await;
let non_exist_email = format!("{}@appflowy.io", uuid::Uuid::new_v4());
c1.api_client
.add_workspace_members(
&workspace_id_c1,
vec![CreateWorkspaceMember {
email: non_exist_email.clone(),
role: AFRole::Member,
}],
)
.await
.expect("should be able to add member whose email not exists yet");
let admin_client = admin_user_client().await;
let users = admin_client
.admin_list_users(Some(&non_exist_email))
.await
.expect("should be able to list users");
println!("---------non_exist_email: {}", non_exist_email);
let non_exist_user = users
.iter()
.find(|u| u.email == non_exist_email)
.expect("should find the user");
// since user does not exist and is just created, it should not be confirmed yet
assert!(non_exist_user.confirmed_at.is_none());
}