diff --git a/.sqlx/query-15b177321fc6b28e249bd1ae6863bda18a7d0e4461f2239c25e6ee448b279d3e.json b/.sqlx/query-15b177321fc6b28e249bd1ae6863bda18a7d0e4461f2239c25e6ee448b279d3e.json deleted file mode 100644 index a0a6f111..00000000 --- a/.sqlx/query-15b177321fc6b28e249bd1ae6863bda18a7d0e4461f2239c25e6ee448b279d3e.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-5479b2f4b07cf39ff655024d7436223b5812940ae3801cd76cbf5276530ac851.json b/.sqlx/query-5479b2f4b07cf39ff655024d7436223b5812940ae3801cd76cbf5276530ac851.json deleted file mode 100644 index d2a04e04..00000000 --- a/.sqlx/query-5479b2f4b07cf39ff655024d7436223b5812940ae3801cd76cbf5276530ac851.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-bd39ce152d3468956db3896d892242603963e3a5435c4c9e2457c5afcbbdc510.json b/.sqlx/query-bd39ce152d3468956db3896d892242603963e3a5435c4c9e2457c5afcbbdc510.json deleted file mode 100644 index cfaf7bf4..00000000 --- a/.sqlx/query-bd39ce152d3468956db3896d892242603963e3a5435c4c9e2457c5afcbbdc510.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-e3e2533f5b58394d3eaef0d90c2ff9a5e00eb7bfd696e5156ffc840ae401dfc1.json b/.sqlx/query-e3e2533f5b58394d3eaef0d90c2ff9a5e00eb7bfd696e5156ffc840ae401dfc1.json deleted file mode 100644 index 84704928..00000000 --- a/.sqlx/query-e3e2533f5b58394d3eaef0d90c2ff9a5e00eb7bfd696e5156ffc840ae401dfc1.json +++ /dev/null @@ -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" -} diff --git a/admin_frontend/src/web_app.rs b/admin_frontend/src/web_app.rs index 4e3796d0..d8873f63 100644 --- a/admin_frontend/src/web_app.rs +++ b/admin_frontend/src/web_app.rs @@ -146,7 +146,7 @@ pub async fn admin_users_handler( ) -> Result, 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| { diff --git a/libs/client-api/src/http.rs b/libs/client-api/src/http.rs index 63d7f309..05777ee3 100644 --- a/libs/client-api/src/http.rs +++ b/libs/client-api/src/http.rs @@ -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, 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> { diff --git a/libs/database/src/pg_row.rs b/libs/database/src/pg_row.rs index b752b944..aa01ca4f 100644 --- a/libs/database/src/pg_row.rs +++ b/libs/database/src/pg_row.rs @@ -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, pub status: AFWorkspaceInvitationStatus, pub role: AFRole, } diff --git a/libs/database/src/workspace.rs b/libs/database/src/workspace.rs index 2e288aa5..4c15d827 100644 --- a/libs/database/src/workspace.rs +++ b/libs/database/src/workspace.rs @@ -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, diff --git a/libs/gotrue/src/api.rs b/libs/gotrue/src/api.rs index 292d2301..d7d487a5 100644 --- a/libs/gotrue/src/api.rs +++ b/libs/gotrue/src/api.rs @@ -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 { 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 { + 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, diff --git a/libs/gotrue/src/params.rs b/libs/gotrue/src/params.rs index 3a1dd390..92988b3d 100644 --- a/libs/gotrue/src/params.rs +++ b/libs/gotrue/src/params.rs @@ -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, diff --git a/src/application.rs b/src/application.rs index d8c9a112..ff5b1eae 100644 --- a/src/application.rs +++ b/src/application.rs @@ -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 Result Result<(), Error> { +) -> Result { 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::()?; @@ -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) }, } }, diff --git a/src/biz/utils.rs b/src/biz/utils.rs index 36f003e5..36029784 100644 --- a/src/biz/utils.rs +++ b/src/biz/utils.rs @@ -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 { + 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) +} diff --git a/src/biz/workspace/ops.rs b/src/biz/workspace/ops.rs index 0ee0ee62..1f8c1ff3 100644 --- a/src/biz/workspace/ops.rs +++ b/src/biz/workspace/ops.rs @@ -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(()) diff --git a/src/state.rs b/src/state.rs index 84aec005..0680c11f 100644 --- a/src/state.rs +++ b/src/state.rs @@ -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, 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, +} + +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 { + let token = client + .token(&Grant::Password(PasswordGrant { + email: self.admin_email.clone(), + password: self.password.expose_secret().clone(), + })) + .await?; + Ok(token.access_token) + } +} diff --git a/tests/gotrue/admin.rs b/tests/gotrue/admin.rs index 5eda6a46..28e91179 100644 --- a/tests/gotrue/admin.rs +++ b/tests/gotrue/admin.rs @@ -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;