feat: use email for af workspace invitation table

This commit is contained in:
Zack Fu Zi Xiang 2024-03-06 11:29:07 +08:00
parent ba1fa8f307
commit 9b28edb5bc
No known key found for this signature in database
15 changed files with 115 additions and 163 deletions

View File

@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE public.af_workspace_invitation\n SET status = 1\n WHERE invitee = (SELECT uid FROM public.af_user WHERE uuid = $1)\n AND id = $2\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Uuid"
]
},
"nullable": []
},
"hash": "15b177321fc6b28e249bd1ae6863bda18a7d0e4461f2239c25e6ee448b279d3e"
}

View File

@ -1,46 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n workspace_id,\n inviter AS inviter_uid,\n invitee AS invitee_uid,\n status,\n role_id AS role\n FROM\n public.af_workspace_invitation\n WHERE id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "inviter_uid",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "invitee_uid",
"type_info": "Int8"
},
{
"ordinal": 3,
"name": "status",
"type_info": "Int2"
},
{
"ordinal": 4,
"name": "role",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false,
false,
false
]
},
"hash": "5479b2f4b07cf39ff655024d7436223b5812940ae3801cd76cbf5276530ac851"
}

View File

@ -1,59 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n id AS invite_id,\n workspace_id,\n (SELECT workspace_name FROM public.af_workspace WHERE workspace_id = af_workspace_invitation.workspace_id),\n (SELECT email FROM public.af_user WHERE uid = af_workspace_invitation.inviter) AS inviter_email,\n (SELECT name FROM public.af_user WHERE uid = af_workspace_invitation.inviter) AS inviter_name,\n status,\n updated_at\n FROM\n public.af_workspace_invitation\n WHERE af_workspace_invitation.invitee = (SELECT uid FROM public.af_user WHERE uuid = $1)\n AND ($2::SMALLINT IS NULL OR status = $2)\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "invite_id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "workspace_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "workspace_name",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "inviter_email",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "inviter_name",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "status",
"type_info": "Int2"
},
{
"ordinal": 6,
"name": "updated_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Uuid",
"Int2"
]
},
"nullable": [
false,
false,
null,
null,
null,
false,
false
]
},
"hash": "bd39ce152d3468956db3896d892242603963e3a5435c4c9e2457c5afcbbdc510"
}

View File

@ -1,17 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO public.af_workspace_invitation (\n workspace_id,\n inviter,\n invitee,\n role_id\n )\n VALUES (\n $1,\n (SELECT uid FROM public.af_user WHERE uuid = $2),\n (SELECT uid FROM public.af_user WHERE email = $3),\n $4\n )\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Uuid",
"Text",
"Int4"
]
},
"nullable": []
},
"hash": "e3e2533f5b58394d3eaef0d90c2ff9a5e00eb7bfd696e5156ffc840ae401dfc1"
}

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

@ -430,6 +430,19 @@ impl Client {
)
}
// 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
#[cfg(debug_assertions)]
pub fn token(&self) -> Arc<RwLock<ClientToken>> {

View File

@ -175,11 +175,11 @@ pub struct AFSnapshotRow {
pub workspace_id: Uuid,
}
#[derive(FromRow, Deserialize, Serialize)]
#[derive(Debug, FromRow, Deserialize, Serialize)]
pub struct AFWorkspaceInvitationMinimal {
pub workspace_id: Uuid,
pub inviter_uid: i64,
pub invitee_uid: i64,
pub invitee_uid: Option<i64>,
pub status: AFWorkspaceInvitationStatus,
pub role: AFRole,
}

View File

@ -248,13 +248,13 @@ pub async fn insert_workspace_invitation(
INSERT INTO public.af_workspace_invitation (
workspace_id,
inviter,
invitee,
invitee_email,
role_id
)
VALUES (
$1,
(SELECT uid FROM public.af_user WHERE uuid = $2),
(SELECT uid FROM public.af_user WHERE email = $3),
$3,
$4
)
"#,
@ -269,7 +269,7 @@ pub async fn insert_workspace_invitation(
Ok(())
}
pub async fn update_workspace_invitation_set_invited(
pub async fn update_workspace_invitation_set_status_accepted(
txn: &mut Transaction<'_, sqlx::Postgres>,
invitee_uuid: &Uuid,
invite_id: &Uuid,
@ -278,7 +278,7 @@ pub async fn update_workspace_invitation_set_invited(
r#"
UPDATE public.af_workspace_invitation
SET status = 1
WHERE invitee = (SELECT uid FROM public.af_user WHERE uuid = $1)
WHERE invitee_email = (SELECT email FROM public.af_user WHERE uuid = $1)
AND id = $2
"#,
invitee_uuid,
@ -312,7 +312,7 @@ pub async fn get_invitation_by_id(
SELECT
workspace_id,
inviter AS inviter_uid,
invitee AS invitee_uid,
(SELECT uid FROM public.af_user WHERE email = invitee_email) AS invitee_uid,
status,
role_id AS role
FROM
@ -346,7 +346,7 @@ pub async fn select_workspace_invitations_for_user(
updated_at
FROM
public.af_workspace_invitation
WHERE af_workspace_invitation.invitee = (SELECT uid FROM public.af_user WHERE uuid = $1)
WHERE af_workspace_invitation.invitee_email = (SELECT email FROM public.af_user WHERE uuid = $1)
AND ($2::SMALLINT IS NULL OR status = $2)
"#,
invitee_uuid,

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

@ -23,7 +23,7 @@ use crate::middleware::access_control_mw::MiddlewareAccessControlTransform;
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 crate::state::{AppMetrics, AppState, GoTrueAdmin, UserCache};
use actix::Actor;
use actix_identity::IdentityMiddleware;
use actix_session::storage::RedisSessionStore;
@ -182,7 +182,7 @@ pub async fn init_state(config: &Config, rt_cmd_tx: RTCommandSender) -> Result<A
// Gotrue
info!("Connecting to GoTrue...");
let gotrue_client = get_gotrue_client(&config.gotrue).await?;
setup_admin_account(&gotrue_client, &pg_pool, &config.gotrue).await?;
let gotrue_admin = setup_admin_account(&gotrue_client, &pg_pool, &config.gotrue).await?;
// Redis
info!("Connecting to Redis...");
@ -244,6 +244,7 @@ pub async fn init_state(config: &Config, rt_cmd_tx: RTCommandSender) -> Result<A
pg_listeners,
access_control,
metrics,
gotrue_admin,
})
}
@ -251,9 +252,14 @@ async fn setup_admin_account(
gotrue_client: &gotrue::api::Client,
pg_pool: &PgPool,
gotrue_setting: &GoTrueSetting,
) -> Result<(), Error> {
) -> Result<GoTrueAdmin, Error> {
let admin_email = gotrue_setting.admin_email.as_str();
let password = gotrue_setting.admin_password.as_str();
let gotrue_admin = GoTrueAdmin {
admin_email: admin_email.to_owned(),
password: admin_email.to_owned().into(),
};
let res_resp = gotrue_client.sign_up(admin_email, password, None).await;
match res_resp {
Err(err) => {
@ -261,7 +267,7 @@ async fn setup_admin_account(
match (err.code, err.msg.as_str()) {
(400, "User already registered") => {
info!("Admin user already registered");
Ok(())
Ok(gotrue_admin)
},
_ => Err(err.into()),
}
@ -279,7 +285,7 @@ async fn setup_admin_account(
match admin_user.role.as_str() {
"supabase_admin" => {
info!("Admin user already created and set role to supabase_admin");
Ok(())
Ok(gotrue_admin)
},
_ => {
let user_id = admin_user.id.parse::<uuid::Uuid>()?;
@ -298,7 +304,7 @@ async fn setup_admin_account(
assert_eq!(result.rows_affected(), 1);
info!("Admin user created and set role to supabase_admin");
Ok(())
Ok(gotrue_admin)
},
}
},

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

@ -11,8 +11,8 @@ use database::workspace::{
change_workspace_icon, delete_from_workspace, delete_workspace_members, get_invitation_by_id,
insert_user_workspace, insert_workspace_invitation, rename_workspace, select_all_user_workspaces,
select_workspace, select_workspace_invitations_for_user, select_workspace_member_list,
update_updated_at_of_workspace, update_workspace_invitation_set_invited, upsert_workspace_member,
upsert_workspace_member_with_txn,
update_updated_at_of_workspace, update_workspace_invitation_set_status_accepted,
upsert_workspace_member, upsert_workspace_member_with_txn,
};
use database_entity::dto::{
AFAccessLevel, AFRole, AFWorkspace, AFWorkspaceInvitation, AFWorkspaceInvitationStatus,
@ -144,10 +144,13 @@ pub async fn accept_workspace_invite(
invite_id: &Uuid,
) -> Result<(), AppError> {
let mut txn = pg_pool.begin().await?;
update_workspace_invitation_set_invited(&mut txn, user_uuid, invite_id).await?;
update_workspace_invitation_set_status_accepted(&mut txn, user_uuid, invite_id).await?;
let inv = get_invitation_by_id(&mut txn, invite_id).await?;
let invited_uid = inv
.invitee_uid
.ok_or_else(|| AppError::Internal(anyhow::anyhow!("Invitee uid is missing for {:?}", inv)))?;
workspace_access_control
.insert_role(&inv.invitee_uid, &inv.workspace_id, inv.role)
.insert_role(&invited_uid, &inv.workspace_id, inv.role)
.await?;
txn.commit().await?;
Ok(())

View File

@ -4,6 +4,8 @@ use crate::biz::pg_listener::PgListeners;
use crate::config::config::Config;
use app_error::AppError;
use gotrue::grant::{Grant, PasswordGrant};
use secrecy::{ExposeSecret, Secret};
use crate::api::metrics::RequestMetrics;
use crate::biz::casbin::access_control::AccessControl;
@ -40,6 +42,7 @@ pub struct AppState {
pub pg_listeners: Arc<PgListeners>,
pub access_control: AccessControl,
pub metrics: AppMetrics,
pub gotrue_admin: GoTrueAdmin,
}
impl AppState {
@ -132,3 +135,28 @@ impl AppMetrics {
}
}
}
#[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)
}
}

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;