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 <ruben@windmill.dev>
This commit is contained in:
HugoCasa
2024-04-24 11:50:56 +02:00
committed by GitHub
parent ce59a6719d
commit ba5bd7361e
5 changed files with 320 additions and 226 deletions
@@ -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"
}
@@ -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"
}
+34
View File
@@ -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",
@@ -157,6 +157,7 @@
jobKindsCat="jobs"
jobKinds="all"
user={null}
label={null}
folder={null}
success="running"
argFilter={undefined}
@@ -113,6 +113,9 @@
}
let nbInviteDisplayed = 50
let showInvites = false
$: showInvites = invites?.length > 0 || (auto_invite_domain != undefined && !autoAdd)
</script>
<SearchItems
@@ -122,18 +125,22 @@
f={(x) => x.email + ' ' + x.name + ' ' + x.company}
/>
<div class="flex flex-col gap-4 my-8">
<div class="flex flex-col gap-1">
<div class=" text-primary text-lg font-semibold"> Members & Invites </div>
<div class="text-tertiary text-xs">
Add members to your workspace and manage their roles. You can also invite or auto-invites users to join your workspace.
<a href="https://www.windmill.dev/docs/core_concepts/roles_and_permissions" target="_blank" class="text-blue-500">Learn more</a>.
<div class="flex flex-col gap-1">
<div class="text-tertiary text-xs">
Add members to your workspace and manage their roles. You can also auto-add users to join your
workspace.
<a
href="https://www.windmill.dev/docs/core_concepts/roles_and_permissions"
target="_blank"
class="text-blue-500">Learn more</a
>.
</div>
</div>
</div>
</div>
<div class="flex flex-row justify-between items-center">
<PageHeader
title="Members ({filteredUsers?.length ?? users?.length ?? ''})"
primary={false}
primary={true}
tooltip="Manage users manually or enable SSO authentication."
documentationLink="https://www.windmill.dev/docs/core_concepts/authentification"
/>
@@ -142,7 +149,135 @@
<input placeholder="Filter members" bind:value={userFilter} class="input !pl-8" />
<Search class="absolute left-2" size={14} />
<AddUser on:new={listUsers} />
<Popup
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
>
<svelte:fragment slot="button">
<Button
color={auto_invite_domain != undefined ? 'green' : 'red'}
variant="border"
size="xs"
nonCaptureEvent={true}
startIcon={{ icon: Mails }}
>Auto-{showInvites && !autoAdd ? 'invite' : 'add'}: {auto_invite_domain != undefined
? 'ON'
: 'OFF'}
</Button>
</svelte:fragment>
<div class="flex flex-col items-start">
<span class="text-sm leading-6 font-semibold">
{isCloudHosted()
? `Auto-add anyone from ${
auto_invite_domain != undefined ? auto_invite_domain : domain
}`
: `Auto-add anyone joining the instance`}
</span>
{#if showInvites}
<span class="text-xs mb-1 leading-6 pt-2"
>Mode <Tooltip>Whether to invite or add users directly to the workspace.</Tooltip>
</span>
<ToggleButtonGroup
selected={autoAdd ? 'add' : 'invite'}
on:selected={async (e) => {
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'
}
}}
>
<ToggleButton value="invite" size="xs" label="Auto-invite" />
<ToggleButton value="add" size="xs" label="Auto-add" />
</ToggleButtonGroup>
{/if}
<span class="text-xs mb-1 leading-6 pt-2"
>Role <Tooltip>Role of the auto-added users</Tooltip></span
>
<ToggleButtonGroup
selected={operatorOnly ? 'operator' : 'developer'}
on:selected={async (e) => {
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'
}
}}
>
<ToggleButton
value="operator"
size="xs"
label="Operator"
tooltip="An operator can only execute and view scripts/flows/apps from your workspace, and only those that he has visibility on."
/>
<ToggleButton
value="developer"
size="xs"
label="Developer"
tooltip="A Developer can execute and view scripts/flows/apps, but they can also create new ones and edit those they are allowed to by their path (either u/ or Writer or Admin of their folder found at /f)."
/>
</ToggleButtonGroup>
<div class="pt-2">
<Toggle
size="xs"
checked={auto_invite_domain != undefined}
on:change={async (e) => {
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'
}}
/>
</div>
{#if isCloudHosted() && !allowedAutoDomain}
<div class="text-red-400 text-xs">{domain} domain not allowed for auto-add</div>
{/if}
</div>
</Popup>
<AddUser
on:new={() => {
listUsers()
listInvites()
}}
/>
</div>
</div>
@@ -175,11 +310,16 @@
</Head>
<tbody class="divide-y bg-surface">
{#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)}
<tr class="!hover:bg-surface-hover">
<Cell first>{truncate(email, 20)}</Cell>
<Cell>{truncate(username, 30)}</Cell>
<Cell>{#if usage?.[email] != undefined}{usage?.[email]}{:else}<Loader2 size={14} class="animate-spin" />{/if}</Cell>
<Cell
>{#if usage?.[email] != undefined}{usage?.[email]}{:else}<Loader2
size={14}
class="animate-spin"
/>{/if}</Cell
>
<Cell>
<div class="flex gap-1">
{#if disabled}
@@ -297,232 +437,113 @@
</DataTable>
</div>
<PageHeader
title="Invites ({invites.length ?? ''})"
primary={false}
tooltip="Manage invites on your workspace."
documentationLink="https://www.windmill.dev/docs/core_concepts/authentification#adding-users-to-a-workspace"
>
<div class="flex gap-2 items-center">
<Popup
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
>
<svelte:fragment slot="button">
<Button
color={auto_invite_domain != undefined ? 'green' : 'red'}
variant="border"
size="xs"
nonCaptureEvent={true}
startIcon={{ icon: Mails }}
>Auto-invite: {auto_invite_domain != undefined ? 'ON' : 'OFF'}
</Button>
</svelte:fragment>
<div class="flex flex-col items-start">
<!-- <span class="text-xs mb-1 leading-6">Who </span>
<span class="text-sm" /> -->
{#if showInvites}
<PageHeader
title="Invites ({invites.length ?? ''})"
primary={false}
tooltip="Manage invites on your workspace."
documentationLink="https://www.windmill.dev/docs/core_concepts/authentification#adding-users-to-a-workspace"
>
<div class="flex gap-2 items-center">
<InviteUser on:new={listInvites} />
</div>
</PageHeader>
<span class="text-sm leading-6 font-semibold">
{isCloudHosted()
? `Auto-invite anyone from ${
auto_invite_domain != undefined ? auto_invite_domain : domain
}`
: 'Auto-invite anyone joining the instance'}
</span>
<span class="text-xs mb-1 leading-6 pt-2"
>Mode <Tooltip>Whether to invite or add users directly to the workspace.</Tooltip>
</span>
<ToggleButtonGroup
selected={autoAdd ? 'add' : 'invite'}
on:selected={async (e) => {
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'
}
}}
>
<ToggleButton value="invite" size="xs" label="Auto-invite" />
<ToggleButton value="add" size="xs" label="Auto-add" />
</ToggleButtonGroup>
<div>
<DataTable>
<Head>
<tr>
<Cell head first>Email</Cell>
<Cell head>Role</Cell>
<Cell head last><span class="sr-only">Actions</span></Cell>
</tr>
</Head>
<tbody class="divide-y bg-surface">
{#if invites?.length > 0}
{#each invites.slice(0, nbInviteDisplayed) as { email, is_admin, operator }}
<Row>
<Cell first>{email}</Cell>
<Cell>
<div>
<ToggleButtonGroup
selected={is_admin ? 'admin' : operator ? 'operator' : 'developer'}
on:selected={async (e) => {
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()
}}
>
<ToggleButton
value="operator"
size="xs"
label="Operator"
tooltip="An operator can only execute and view scripts/flows/apps from your workspace, and only those that he has visibility on."
/>
<span class="text-xs mb-1 leading-6 pt-2"
>Role <Tooltip>Role of the auto-invited users</Tooltip></span
>
<ToggleButtonGroup
selected={operatorOnly ? 'operator' : 'developer'}
on:selected={async (e) => {
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'
}
}}
>
<ToggleButton
value="operator"
size="xs"
label="Operator"
tooltip="An operator can only execute and view scripts/flows/apps from your workspace, and only those that he has visibility on."
/>
<ToggleButton
value="developer"
size="xs"
label="Developer"
tooltip="A Developer can execute and view scripts/flows/apps, but they can also create new ones and edit those they are allowed to by their path (either u/ or Writer or Admin of their folder found at /f)."
/>
</ToggleButtonGroup>
<div class="pt-2">
<Toggle
size="xs"
checked={auto_invite_domain != undefined}
on:change={async (e) => {
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'
}}
/>
</div>
{#if isCloudHosted() && !allowedAutoDomain}
<div class="text-red-400 text-xs">{domain} domain not allowed for auto-invite</div>
{/if}
</div>
</Popup>
<InviteUser on:new={listInvites} />
</div>
</PageHeader>
<ToggleButton
value="developer"
size="xs"
label="Developer"
tooltip="A Developer can execute and view scripts/flows/apps, but they can also create new ones and edit those they are allowed to by their path (either u/ or Writer or Admin of their folder found at /f)."
/>
<div>
<DataTable>
<Head>
<tr>
<Cell head first>Email</Cell>
<Cell head>Role</Cell>
<Cell head last><span class="sr-only">Actions</span></Cell>
</tr>
</Head>
<tbody class="divide-y bg-surface">
{#if invites?.length > 0}
{#each invites.slice(0, nbInviteDisplayed) as { email, is_admin, operator }}
<Row>
<Cell first>{email}</Cell>
<Cell>
<div>
<ToggleButtonGroup
selected={is_admin ? 'admin' : operator ? 'operator' : 'developer'}
on:selected={async (e) => {
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({
<ToggleButton
value="admin"
size="xs"
label="Admin"
tooltip="An admin has full control over a specific Windmill workspace, including the ability to manage users, edit entities, and control permissions within the workspace."
/>
</ToggleButtonGroup>
</div>
</Cell>
<Cell last>
<button
class="ml-2 text-red-500"
on:click={async () => {
await WorkspaceService.deleteInvite({
workspace: $workspaceStore ?? '',
requestBody: {
email,
...body
is_admin,
operator
}
})
listUsers()
listInvites()
}}
>
<ToggleButton
value="operator"
size="xs"
label="Operator"
tooltip="An operator can only execute and view scripts/flows/apps from your workspace, and only those that he has visibility on."
/>
<ToggleButton
value="developer"
size="xs"
label="Developer"
tooltip="A Developer can execute and view scripts/flows/apps, but they can also create new ones and edit those they are allowed to by their path (either u/ or Writer or Admin of their folder found at /f)."
/>
<ToggleButton
value="admin"
size="xs"
label="Admin"
tooltip="An admin has full control over a specific Windmill workspace, including the ability to manage users, edit entities, and control permissions within the workspace."
/>
</ToggleButtonGroup>
</div>
</Cell>
<Cell last>
<button
class="ml-2 text-red-500"
on:click={async () => {
await WorkspaceService.deleteInvite({
workspace: $workspaceStore ?? '',
requestBody: {
email,
is_admin,
operator
}
})
listInvites()
}}
>
Cancel
</button>
</Cell>
</Row>
{/each}
{:else}
<tr>
<td colspan="3" class="text-center py-8">
<div class="text-xs text-secondary"> No invites yet </div>
</td>
</tr>
{/if}
</tbody>
</DataTable>
{#if invites && invites?.length > 50 && nbInviteDisplayed < invites.length}
<span class="text-xs"
>{nbInviteDisplayed} invites out of {invites.length}
<button class="ml-4" on:click={() => (nbInviteDisplayed += 50)}>load 50 more</button></span
>
{/if}
</div>
Cancel
</button>
</Cell>
</Row>
{/each}
{:else}
<tr>
<td colspan="3" class="text-center py-8">
<div class="text-xs text-secondary"> No invites yet </div>
</td>
</tr>
{/if}
</tbody>
</DataTable>
{#if invites && invites?.length > 50 && nbInviteDisplayed < invites.length}
<span class="text-xs"
>{nbInviteDisplayed} invites out of {invites.length}
<button class="ml-4" on:click={() => (nbInviteDisplayed += 50)}>load 50 more</button></span
>
{/if}
</div>
{/if}
<ConfirmationModal
open={Boolean(deleteConfirmedCallback)}