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 @@
+Refresh auth
+
+
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 }))
+}