From ba5bd7361eb97c36ec70556ed00b80fbfec9f38e Mon Sep 17 00:00:00 2001 From: HugoCasa Date: Wed, 24 Apr 2024 11:50:56 +0200 Subject: [PATCH] fix: solve invite add conflict + deprecate invites (#3594) * fix: solver invite add conflict + deprecate invites * fix: sqlx build * fix: npm run check * patch: deprecate invite from cloud as well --------- Co-authored-by: Ruben Fiszel --- ...416cbe222f2c67ccc8aa92e651c2bea4c2d7b.json | 15 + ...6ac6e4e981ea219c173146088faf1ad9f3822.json | 23 + backend/windmill-api/src/workspaces.rs | 34 ++ .../src/lib/components/SavedInputs.svelte | 1 + .../settings/WorkspaceUserSettings.svelte | 473 +++++++++--------- 5 files changed, 320 insertions(+), 226 deletions(-) create mode 100644 backend/.sqlx/query-0d7ba88a9810e434aa00fd63bbf416cbe222f2c67ccc8aa92e651c2bea4c2d7b.json create mode 100644 backend/.sqlx/query-7142222bee1f60bc56752e377b96ac6e4e981ea219c173146088faf1ad9f3822.json diff --git a/backend/.sqlx/query-0d7ba88a9810e434aa00fd63bbf416cbe222f2c67ccc8aa92e651c2bea4c2d7b.json b/backend/.sqlx/query-0d7ba88a9810e434aa00fd63bbf416cbe222f2c67ccc8aa92e651c2bea4c2d7b.json new file mode 100644 index 0000000000..eade62579b --- /dev/null +++ b/backend/.sqlx/query-0d7ba88a9810e434aa00fd63bbf416cbe222f2c67ccc8aa92e651c2bea4c2d7b.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM workspace_invite WHERE workspace_id = $1 AND email = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "0d7ba88a9810e434aa00fd63bbf416cbe222f2c67ccc8aa92e651c2bea4c2d7b" +} diff --git a/backend/.sqlx/query-7142222bee1f60bc56752e377b96ac6e4e981ea219c173146088faf1ad9f3822.json b/backend/.sqlx/query-7142222bee1f60bc56752e377b96ac6e4e981ea219c173146088faf1ad9f3822.json new file mode 100644 index 0000000000..d3743e3dad --- /dev/null +++ b/backend/.sqlx/query-7142222bee1f60bc56752e377b96ac6e4e981ea219c173146088faf1ad9f3822.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS (SELECT 1 FROM usr WHERE workspace_id = $1 AND email = $2)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "7142222bee1f60bc56752e377b96ac6e4e981ea219c173146088faf1ad9f3822" +} diff --git a/backend/windmill-api/src/workspaces.rs b/backend/windmill-api/src/workspaces.rs index 058af770df..ee542d9907 100644 --- a/backend/windmill-api/src/workspaces.rs +++ b/backend/windmill-api/src/workspaces.rs @@ -722,6 +722,16 @@ async fn edit_auto_invite( for user in users_to_auto_add.as_ref().unwrap() { auto_add_user(&user.email, &w_id, &operator, &mut tx).await?; + send_email_if_possible( + &format!("Added to Windmill's workspace: {w_id}"), + &format!( + "You have been granted access to Windmill's workspace {w_id} by {email}. + + Access the workspace at {}/?workspace={w_id}", + BASE_URL.read().await.clone() + ), + &user.email, + ); } } else { sqlx::query!( @@ -1936,6 +1946,22 @@ async fn invite_user( let mut tx = db.begin().await?; + let already_in_workspace = sqlx::query_scalar!( + "SELECT EXISTS (SELECT 1 FROM usr WHERE workspace_id = $1 AND email = $2)", + &w_id, + nu.email + ) + .fetch_one(&mut *tx) + .await? + .unwrap_or(false); + + if already_in_workspace { + return Err(Error::BadRequest(format!( + "user with email {} already exists in workspace {}", + nu.email, w_id + ))); + } + sqlx::query!( "INSERT INTO workspace_invite (workspace_id, email, is_admin, operator) @@ -2046,6 +2072,14 @@ async fn add_user( .execute(&mut *tx) .await?; + sqlx::query!( + "DELETE FROM workspace_invite WHERE workspace_id = $1 AND email = $2", + &w_id, + nu.email + ) + .execute(&mut *tx) + .await?; + sqlx::query_as!( Group, "INSERT INTO usr_to_group (workspace_id, usr, group_) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING", diff --git a/frontend/src/lib/components/SavedInputs.svelte b/frontend/src/lib/components/SavedInputs.svelte index 1af6a54cef..86a83498f0 100644 --- a/frontend/src/lib/components/SavedInputs.svelte +++ b/frontend/src/lib/components/SavedInputs.svelte @@ -157,6 +157,7 @@ jobKindsCat="jobs" jobKinds="all" user={null} + label={null} folder={null} success="running" argFilter={undefined} diff --git a/frontend/src/lib/components/settings/WorkspaceUserSettings.svelte b/frontend/src/lib/components/settings/WorkspaceUserSettings.svelte index 1764729b8d..2b821be8c0 100644 --- a/frontend/src/lib/components/settings/WorkspaceUserSettings.svelte +++ b/frontend/src/lib/components/settings/WorkspaceUserSettings.svelte @@ -113,6 +113,9 @@ } let nbInviteDisplayed = 50 + + let showInvites = false + $: showInvites = invites?.length > 0 || (auto_invite_domain != undefined && !autoAdd) x.email + ' ' + x.name + ' ' + x.company} />
-
-
Members & Invites
-
- Add members to your workspace and manage their roles. You can also invite or auto-invites users to join your workspace. - Learn more. +
+
+ Add members to your workspace and manage their roles. You can also auto-add users to join your + workspace. + Learn more. +
-
@@ -142,7 +149,135 @@ - + + + + +
+ + {isCloudHosted() + ? `Auto-add anyone from ${ + auto_invite_domain != undefined ? auto_invite_domain : domain + }` + : `Auto-add anyone joining the instance`} + + + {#if showInvites} + Mode Whether to invite or add users directly to the workspace. + + { + if (auto_invite_domain != undefined) { + await removeAllInvitesFromDomain() + await WorkspaceService.editAutoInvite({ + workspace: $workspaceStore ?? '', + requestBody: { + operator: operatorOnly ?? false, + invite_all: !isCloudHosted(), + auto_add: e.detail === 'add' + } + }) + loadSettings() + listInvites() + listUsers() + } else { + autoAdd = e.detail === 'add' + } + }} + > + + + + {/if} + + Role Role of the auto-added users + { + if (auto_invite_domain != undefined) { + await removeAllInvitesFromDomain() + await WorkspaceService.editAutoInvite({ + workspace: $workspaceStore ?? '', + requestBody: { + operator: e.detail === 'operator', + invite_all: !isCloudHosted(), + auto_add: showInvites ? autoAdd ?? false : true + } + }) + loadSettings() + listInvites() + listUsers() + } else { + operatorOnly = e.detail === 'operator' + } + }} + > + + + +
+ { + await removeAllInvitesFromDomain() + await WorkspaceService.editAutoInvite({ + workspace: $workspaceStore ?? '', + requestBody: e.detail + ? { + operator: operatorOnly ?? false, + invite_all: !isCloudHosted(), + auto_add: showInvites ? autoAdd ?? false : true + } + : { operator: undefined, auto_add: undefined } + }) + loadSettings() + listInvites() + listUsers() + }} + disabled={isCloudHosted() && !allowedAutoDomain} + options={{ + right: 'Enabled' + }} + /> +
+ {#if isCloudHosted() && !allowedAutoDomain} +
{domain} domain not allowed for auto-add
+ {/if} +
+
+ { + listUsers() + listInvites() + }} + />
@@ -175,11 +310,16 @@ {#if filteredUsers} - {#each filteredUsers.slice(0, nbDisplayed) as { email, username, is_admin, operator, disabled } (email)} + {#each filteredUsers.slice(0, nbDisplayed) as { email, username, is_admin, operator, disabled } (email)} {truncate(email, 20)} {truncate(username, 30)} - {#if usage?.[email] != undefined}{usage?.[email]}{:else}{/if} + {#if usage?.[email] != undefined}{usage?.[email]}{:else}{/if}
{#if disabled} @@ -297,232 +437,113 @@
- -
- - - - -
- +{#if showInvites} + +
+ +
+
- - {isCloudHosted() - ? `Auto-invite anyone from ${ - auto_invite_domain != undefined ? auto_invite_domain : domain - }` - : 'Auto-invite anyone joining the instance'} - - Mode Whether to invite or add users directly to the workspace. - - { - if (auto_invite_domain != undefined) { - await removeAllInvitesFromDomain() - await WorkspaceService.editAutoInvite({ - workspace: $workspaceStore ?? '', - requestBody: { - operator: operatorOnly ?? false, - invite_all: !isCloudHosted(), - auto_add: e.detail === 'add' - } - }) - loadSettings() - listInvites() - listUsers() - } else { - autoAdd = e.detail === 'add' - } - }} - > - - - +
+ + + + Email + Role + Actions + + + + {#if invites?.length > 0} + {#each invites.slice(0, nbInviteDisplayed) as { email, is_admin, operator }} + + {email} + +
+ { + const body = + e.detail == 'admin' + ? { is_admin: true, operator: false } + : e.detail == 'operator' + ? { is_admin: false, operator: true } + : { is_admin: false, operator: false } + await WorkspaceService.inviteUser({ + workspace: $workspaceStore ?? '', + requestBody: { + email, + ...body + } + }) + listUsers() + }} + > + - Role Role of the auto-invited users - { - if (auto_invite_domain != undefined) { - await removeAllInvitesFromDomain() - await WorkspaceService.editAutoInvite({ - workspace: $workspaceStore ?? '', - requestBody: { - operator: e.detail === 'operator', - invite_all: !isCloudHosted(), - auto_add: autoAdd ?? false - } - }) - loadSettings() - listInvites() - listUsers() - } else { - operatorOnly = e.detail === 'operator' - } - }} - > - - - -
- { - await removeAllInvitesFromDomain() - await WorkspaceService.editAutoInvite({ - workspace: $workspaceStore ?? '', - requestBody: e.detail - ? { - operator: operatorOnly ?? false, - invite_all: !isCloudHosted(), - auto_add: autoAdd ?? false - } - : { operator: undefined, auto_add: undefined } - }) - loadSettings() - listInvites() - listUsers() - }} - disabled={isCloudHosted() && !allowedAutoDomain} - options={{ - right: 'Enabled' - }} - /> -
- {#if isCloudHosted() && !allowedAutoDomain} -
{domain} domain not allowed for auto-invite
- {/if} -
- - -
- + -
- - - - Email - Role - Actions - - - - {#if invites?.length > 0} - {#each invites.slice(0, nbInviteDisplayed) as { email, is_admin, operator }} - - {email} - -
- { - const body = - e.detail == 'admin' - ? { is_admin: true, operator: false } - : e.detail == 'operator' - ? { is_admin: false, operator: true } - : { is_admin: false, operator: false } - await WorkspaceService.inviteUser({ + + +
+
+ +
- - - - - - {/each} - {:else} - - -
No invites yet
- - - {/if} - - - {#if invites && invites?.length > 50 && nbInviteDisplayed < invites.length} - {nbInviteDisplayed} invites out of {invites.length} - - {/if} -
+ Cancel + + + + {/each} + {:else} + + +
No invites yet
+ + + {/if} + + + {#if invites && invites?.length > 50 && nbInviteDisplayed < invites.length} + {nbInviteDisplayed} invites out of {invites.length} + + {/if} +
+{/if}