diff --git a/app/auth-portal/src/pages/WorkspaceDetailsPage.vue b/app/auth-portal/src/pages/WorkspaceDetailsPage.vue index 3c92a6bb5c..01e1f3abf7 100644 --- a/app/auth-portal/src/pages/WorkspaceDetailsPage.vue +++ b/app/auth-portal/src/pages/WorkspaceDetailsPage.vue @@ -269,7 +269,12 @@ const editWorkspace = async () => { const deleteUserHandlerReq = workspacesStore.getRequestStatus("REMOVE_USER"); const deleteUserHandler = async (email: string) => { if (email === "") return; - return await workspacesStore.REMOVE_USER(email, props.workspaceId); + const res = await workspacesStore.REMOVE_USER(email, props.workspaceId); + if (res.result.success) { + if (!draftWorkspace.instanceUrl.includes("localhost")) { + window.location.href = ` ${draftWorkspace.instanceUrl}/refresh-auth?workspaceId=${props.workspaceId}`; + } + } }; const inviteButtonHandler = async () => { diff --git a/app/web/src/pages/auth/RefreshAuthPage.vue b/app/web/src/pages/auth/RefreshAuthPage.vue new file mode 100644 index 0000000000..bb2f9da23a --- /dev/null +++ b/app/web/src/pages/auth/RefreshAuthPage.vue @@ -0,0 +1,22 @@ + + + diff --git a/app/web/src/router.ts b/app/web/src/router.ts index d33cad907b..b2e03f24b0 100644 --- a/app/web/src/router.ts +++ b/app/web/src/router.ts @@ -113,6 +113,12 @@ const routes: RouteRecordRaw[] = [ meta: { public: true }, component: () => import("@/pages/auth/AuthConnectPage.vue"), }, + { + path: "/refresh-auth", + name: "refresh-auth", + meta: { public: true }, + component: () => import("@/pages/auth/RefreshAuthPage.vue"), + }, { path: "/login", name: "login", diff --git a/app/web/src/store/auth.store.ts b/app/web/src/store/auth.store.ts index a64f714ea4..9ad090ab08 100644 --- a/app/web/src/store/auth.store.ts +++ b/app/web/src/store/auth.store.ts @@ -186,5 +186,14 @@ export const useAuthStore = defineStore("auth", { email: loginResponse.user.email, }); }, + async FORCE_REFRESH_MEMBERS(workspaceId: string) { + return new ApiRequest({ + method: "post", + url: "/session/refresh_workspace_members", + params: { + workspaceId, + }, + }); + }, }, }); diff --git a/lib/dal/src/queries/user/list_members_for_workspace.sql b/lib/dal/src/queries/user/list_members_for_workspace.sql new file mode 100644 index 0000000000..89c64716f9 --- /dev/null +++ b/lib/dal/src/queries/user/list_members_for_workspace.sql @@ -0,0 +1,5 @@ +SELECT row_to_json(u.*) AS object +FROM users AS u +INNER JOIN user_belongs_to_workspaces bt ON bt.user_pk = u.pk +WHERE bt.workspace_pk = $1 +ORDER BY u.created_at ASC diff --git a/lib/dal/src/user.rs b/lib/dal/src/user.rs index 3d183a3ab5..4b231841fe 100644 --- a/lib/dal/src/user.rs +++ b/lib/dal/src/user.rs @@ -13,6 +13,7 @@ use crate::{ const USER_GET_BY_PK: &str = include_str!("queries/user/get_by_pk.sql"); const USER_GET_BY_EMAIL_RAW: &str = include_str!("queries/user/get_by_email_raw.sql"); +const USER_LIST_FOR_WORKSPACE: &str = include_str!("queries/user/list_members_for_workspace.sql"); #[remain::sorted] #[derive(Error, Debug)] @@ -154,6 +155,43 @@ impl User { .await?; Ok(()) } + + pub async fn delete_user_from_workspace( + ctx: &DalContext, + user_pk: UserPk, + workspace_pkg: String, + ) -> UserResult<()> { + ctx.txns() + .await? + .pg() + .execute( + "DELETE from user_belongs_to_workspaces WHERE user_pk = $1 AND workspace_pk = $2", + &[&user_pk, &workspace_pkg], + ) + .await?; + Ok(()) + } + + pub async fn list_members_for_workspace( + ctx: &DalContext, + workspace_pk: String, + ) -> UserResult> { + let rows = ctx + .txns() + .await? + .pg() + .query(USER_LIST_FOR_WORKSPACE, &[&workspace_pk]) + .await?; + + let mut users: Vec = Vec::new(); + for row in rows.into_iter() { + let json: serde_json::Value = row.try_get("object")?; + let object = serde_json::from_value(json)?; + users.push(object); + } + + Ok(users) + } } #[derive(Deserialize, Serialize, Debug, Clone, Copy)] diff --git a/lib/sdf-server/src/server/service/session.rs b/lib/sdf-server/src/server/service/session.rs index 55f871121e..6a0386fae3 100644 --- a/lib/sdf-server/src/server/service/session.rs +++ b/lib/sdf-server/src/server/service/session.rs @@ -7,12 +7,14 @@ use dal::{ KeyPairError, StandardModelError, TransactionsError, UserError, UserPk, WorkspaceError, WorkspacePk, }; +use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::server::state::AppState; pub mod auth_connect; pub mod load_workspaces; +mod refresh_workspace_members; pub mod restore_authentication; #[remain::sorted] @@ -46,6 +48,12 @@ pub enum SessionError { Workspace(#[from] WorkspaceError), } +#[derive(Debug, Serialize, Deserialize)] +struct AuthApiErrBody { + pub kind: String, + pub message: String, +} + pub type SessionResult = std::result::Result; impl IntoResponse for SessionError { @@ -81,4 +89,8 @@ pub fn routes() -> Router { get(restore_authentication::restore_authentication), ) .route("/load_workspaces", get(load_workspaces::load_workspaces)) + .route( + "/refresh_workspace_members", + post(refresh_workspace_members::refresh_workspace_members), + ) } diff --git a/lib/sdf-server/src/server/service/session/auth_connect.rs b/lib/sdf-server/src/server/service/session/auth_connect.rs index 383ff254ca..b2c7e2c65e 100644 --- a/lib/sdf-server/src/server/service/session/auth_connect.rs +++ b/lib/sdf-server/src/server/service/session/auth_connect.rs @@ -1,5 +1,6 @@ use super::{SessionError, SessionResult}; use crate::server::extract::{HandlerContext, RawAccessToken}; +use crate::service::session::AuthApiErrBody; use axum::Json; use dal::{DalContext, HistoryActor, KeyPair, Tenancy, User, UserPk, Workspace, WorkspacePk}; use serde::{Deserialize, Serialize}; @@ -26,12 +27,6 @@ pub struct AuthReconnectResponse { pub workspace: Workspace, } -#[derive(Debug, Serialize, Deserialize)] -struct AuthApiErrBody { - pub kind: String, - pub message: String, -} - #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AuthApiUser { diff --git a/lib/sdf-server/src/server/service/session/refresh_workspace_members.rs b/lib/sdf-server/src/server/service/session/refresh_workspace_members.rs new file mode 100644 index 0000000000..07fb485a08 --- /dev/null +++ b/lib/sdf-server/src/server/service/session/refresh_workspace_members.rs @@ -0,0 +1,76 @@ +use super::{SessionError, SessionResult}; +use crate::server::extract::{AccessBuilder, HandlerContext, RawAccessToken}; +use crate::service::session::AuthApiErrBody; +use axum::Json; +use dal::User; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct RefreshWorkspaceMembersRequest { + pub workspace_id: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WorkspaceMember { + pub user_id: String, + pub email: String, + pub nickname: String, + pub role: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RefreshWorkspaceMembersResponse { + pub success: bool, +} + +pub async fn refresh_workspace_members( + HandlerContext(builder): HandlerContext, + AccessBuilder(access_builder): AccessBuilder, + RawAccessToken(raw_access_token): RawAccessToken, + Json(request): Json, +) -> SessionResult> { + let client = reqwest::Client::new(); + let auth_api_url = match option_env!("LOCAL_AUTH_STACK") { + Some(_) => "http://localhost:9001", + None => "https://auth-api.systeminit.com", + }; + + let res = client + .get(format!( + "{}/workspace/{}/members", + auth_api_url, + request.workspace_id.clone() + )) + .bearer_auth(&raw_access_token) + .send() + .await?; + + if res.status() != reqwest::StatusCode::OK { + let res_err_body = res + .json::() + .await + .map_err(|err| SessionError::AuthApiError(err.to_string()))?; + println!("code exchange failed = {:?}", res_err_body.message); + return Err(SessionError::AuthApiError(res_err_body.message)); + } + + let workspace_members = res.json::>().await?; + + let ctx = builder.build_head(access_builder).await?; + let members = User::list_members_for_workspace(&ctx, request.workspace_id.clone()).await?; + let member_ids: Vec<_> = workspace_members.into_iter().map(|w| w.user_id).collect(); + let users_to_remove: Vec<_> = members + .into_iter() + .filter(|u| !member_ids.contains(&u.pk().to_string())) + .collect(); + + for remove in users_to_remove { + println!("Removing User: {}", remove.pk().clone()); + User::delete_user_from_workspace(&ctx, remove.pk(), request.workspace_id.clone()).await?; + } + + Ok(Json(RefreshWorkspaceMembersResponse { success: true })) +}