mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-08-10 12:40:39 +02:00
Merge branch 'develop' into feat-scim
This commit is contained in:
@@ -2516,6 +2516,25 @@ export class API {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a dynamic recipient group.
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {string|null} companyID
|
||||
* @param {string} filterField - recipient attribute to filter on (position, department, city, country, misc)
|
||||
* @param {string} filterValue - value to match against the filter field
|
||||
* @returns {Promise<ApiResponse>}
|
||||
*/
|
||||
createDynamicGroup: async (name, companyID, filterField, filterValue) => {
|
||||
return await postJSON(this.getPath('/recipient/group'), {
|
||||
name: name,
|
||||
companyID: companyID,
|
||||
isDynamic: true,
|
||||
filterField: filterField,
|
||||
filterValue: filterValue
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a recipient group - not the recipients in the group.
|
||||
*
|
||||
@@ -2532,6 +2551,25 @@ export class API {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a dynamic recipient group.
|
||||
*
|
||||
* @param {object} group
|
||||
* @param {string} group.id
|
||||
* @param {string} [group.name]
|
||||
* @param {string} [group.filterField]
|
||||
* @param {string} [group.filterValue]
|
||||
* @returns {Promise<ApiResponse>}
|
||||
*/
|
||||
updateDynamicGroup: async ({ id, name, filterField, filterValue }) => {
|
||||
return await patchJSON(this.getPath(`/recipient/group/${id}`), {
|
||||
name: name,
|
||||
isDynamic: true,
|
||||
filterField: filterField,
|
||||
filterValue: filterValue
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all recipient groups using pagination.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<span
|
||||
title="Dynamic group"
|
||||
class="select-none px-1 border-2 border-gray-400 dark:border-gray-500 relative -top-1 rounded-lg text-xs text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 transition-colors duration-200"
|
||||
>
|
||||
dynamic
|
||||
</span>
|
||||
@@ -6,6 +6,8 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import Headline from '$lib/components/Headline.svelte';
|
||||
import TextField from '$lib/components/TextField.svelte';
|
||||
import DynamicLabel from '$lib/components/DynamicLabel.svelte';
|
||||
import TextFieldSelect from '$lib/components/TextFieldSelect.svelte';
|
||||
import TableRow from '$lib/components/table/TableRow.svelte';
|
||||
import TableCell from '$lib/components/table/TableCell.svelte';
|
||||
import TableCellLink from '$lib/components/table/TableCellLink.svelte';
|
||||
@@ -33,15 +35,29 @@
|
||||
// services
|
||||
const appStateService = AppStateService.instance;
|
||||
|
||||
// the recipient fields that can be used as a dynamic filter
|
||||
const dynamicFilterFields = ['position', 'department', 'city', 'country', 'misc'];
|
||||
|
||||
// data
|
||||
let form = null;
|
||||
let dynamicForm = null;
|
||||
let modalError = '';
|
||||
let dynamicModalError = '';
|
||||
|
||||
let formValues = {
|
||||
name: null,
|
||||
companyID: null,
|
||||
recipients: []
|
||||
};
|
||||
|
||||
let dynamicFormValues = {
|
||||
name: null,
|
||||
filterField: dynamicFilterFields[0],
|
||||
filterValue: null
|
||||
};
|
||||
|
||||
let isModalVisible = false;
|
||||
let isDynamicModalVisible = false;
|
||||
let isSubmitting = false;
|
||||
let isTableLoading = false;
|
||||
const tableURLParams = newTableURLParams();
|
||||
@@ -57,6 +73,17 @@
|
||||
name: null
|
||||
};
|
||||
|
||||
// dynamic update modal state
|
||||
let isDynamicUpdateModalVisible = false;
|
||||
let dynamicUpdateForm = null;
|
||||
let dynamicUpdateError = '';
|
||||
let dynamicUpdateValues = {
|
||||
id: null,
|
||||
name: null,
|
||||
filterField: dynamicFilterFields[0],
|
||||
filterValue: null
|
||||
};
|
||||
|
||||
$: {
|
||||
modalText = modalMode === 'create' ? 'New group' : 'Update group';
|
||||
}
|
||||
@@ -162,6 +189,54 @@
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmitDynamicCreate = async () => {
|
||||
try {
|
||||
isSubmitting = true;
|
||||
const res = await api.recipient.createDynamicGroup(
|
||||
dynamicFormValues.name,
|
||||
contextCompanyID,
|
||||
dynamicFormValues.filterField,
|
||||
dynamicFormValues.filterValue
|
||||
);
|
||||
if (!res.success) {
|
||||
dynamicModalError = res.error;
|
||||
return;
|
||||
}
|
||||
addToast('Dynamic group created', 'Success');
|
||||
refreshGroups();
|
||||
closeDynamicModal();
|
||||
} catch (e) {
|
||||
addToast('Failed to create dynamic group', 'Error');
|
||||
console.error('failed to create dynamic group', e);
|
||||
} finally {
|
||||
isSubmitting = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmitDynamicUpdate = async () => {
|
||||
try {
|
||||
isSubmitting = true;
|
||||
const res = await api.recipient.updateDynamicGroup({
|
||||
id: dynamicUpdateValues.id,
|
||||
name: dynamicUpdateValues.name,
|
||||
filterField: dynamicUpdateValues.filterField,
|
||||
filterValue: dynamicUpdateValues.filterValue
|
||||
});
|
||||
if (!res.success) {
|
||||
dynamicUpdateError = res.error;
|
||||
return;
|
||||
}
|
||||
addToast('Dynamic group updated', 'Success');
|
||||
refreshGroups();
|
||||
closeDynamicUpdateModal();
|
||||
} catch (err) {
|
||||
addToast('Failed to update dynamic group', 'Error');
|
||||
console.error('failed to update dynamic group', err);
|
||||
} finally {
|
||||
isSubmitting = false;
|
||||
}
|
||||
};
|
||||
|
||||
const openDeleteAlert = async (group) => {
|
||||
isDeleteAlertVisible = true;
|
||||
deleteValues.id = group.id;
|
||||
@@ -195,6 +270,16 @@
|
||||
isModalVisible = true;
|
||||
};
|
||||
|
||||
const openDynamicCreateModal = () => {
|
||||
dynamicFormValues = {
|
||||
name: null,
|
||||
filterField: dynamicFilterFields[0],
|
||||
filterValue: null
|
||||
};
|
||||
dynamicModalError = '';
|
||||
isDynamicModalVisible = true;
|
||||
};
|
||||
|
||||
/** @param {string} id */
|
||||
const openUpdateModal = async (id) => {
|
||||
modalMode = 'update';
|
||||
@@ -212,11 +297,42 @@
|
||||
}
|
||||
};
|
||||
|
||||
/** @param {string} id */
|
||||
const openDynamicUpdateModal = async (id) => {
|
||||
try {
|
||||
showIsLoading();
|
||||
const group = await loadGroup(id);
|
||||
dynamicUpdateValues.id = id;
|
||||
dynamicUpdateValues.name = group.name;
|
||||
dynamicUpdateValues.filterField = group.filterField ?? dynamicFilterFields[0];
|
||||
dynamicUpdateValues.filterValue = group.filterValue ?? '';
|
||||
dynamicUpdateError = '';
|
||||
isDynamicUpdateModalVisible = true;
|
||||
} catch (e) {
|
||||
addToast('Failed to load group', 'Error');
|
||||
console.error('failed to load group', e);
|
||||
} finally {
|
||||
hideIsLoading();
|
||||
}
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
isModalVisible = false;
|
||||
form.reset();
|
||||
modalError = '';
|
||||
};
|
||||
|
||||
const closeDynamicModal = () => {
|
||||
isDynamicModalVisible = false;
|
||||
if (dynamicForm) dynamicForm.reset();
|
||||
dynamicModalError = '';
|
||||
};
|
||||
|
||||
const closeDynamicUpdateModal = () => {
|
||||
isDynamicUpdateModalVisible = false;
|
||||
if (dynamicUpdateForm) dynamicUpdateForm.reset();
|
||||
dynamicUpdateError = '';
|
||||
};
|
||||
</script>
|
||||
|
||||
<HeadTitle title="Groups" />
|
||||
@@ -224,6 +340,7 @@
|
||||
<Headline>Groups</Headline>
|
||||
<div class="flex gap-3">
|
||||
<BigButton on:click={openCreateModal}>New group</BigButton>
|
||||
<BigButton on:click={openDynamicCreateModal}>New dynamic group</BigButton>
|
||||
<BigButton on:click={() => goto('/recipient/orphaned/')}>View Orphaned</BigButton>
|
||||
</div>
|
||||
<Table
|
||||
@@ -242,6 +359,9 @@
|
||||
{#each groups as group}
|
||||
<TableRow>
|
||||
<TableCellLink href={`/recipient/group/${group.id}`} title={group.name}>
|
||||
{#if group.isDynamic}
|
||||
<DynamicLabel />
|
||||
{/if}
|
||||
{group.name}
|
||||
{#if group.scimEnabled}
|
||||
<span
|
||||
@@ -255,6 +375,7 @@
|
||||
<TableCellLink href={`/recipient/group/${group.id}`} title={group.recipientCount}>
|
||||
{group.recipientCount}
|
||||
</TableCellLink>
|
||||
|
||||
{#if contextCompanyID}
|
||||
<TableCellScope companyID={group.companyID} />
|
||||
{/if}
|
||||
@@ -266,10 +387,17 @@
|
||||
on:click={() => gotoEditGroupRecipients(group.id)}
|
||||
{...globalButtonDisabledAttributes(group, contextCompanyID)}
|
||||
/>
|
||||
<TableUpdateButton
|
||||
on:click={() => openUpdateModal(group.id)}
|
||||
{...globalButtonDisabledAttributes(group, contextCompanyID)}
|
||||
/>
|
||||
{#if group.isDynamic}
|
||||
<TableUpdateButton
|
||||
on:click={() => openDynamicUpdateModal(group.id)}
|
||||
{...globalButtonDisabledAttributes(group, contextCompanyID)}
|
||||
/>
|
||||
{:else}
|
||||
<TableUpdateButton
|
||||
on:click={() => openUpdateModal(group.id)}
|
||||
{...globalButtonDisabledAttributes(group, contextCompanyID)}
|
||||
/>
|
||||
{/if}
|
||||
<TableDeleteButton
|
||||
on:click={() => openDeleteAlert(group)}
|
||||
{...globalButtonDisabledAttributes(group, contextCompanyID)}
|
||||
@@ -280,6 +408,7 @@
|
||||
{/each}
|
||||
</Table>
|
||||
|
||||
<!-- static group modal -->
|
||||
<Modal headerText={modalText} bind:visible={isModalVisible} onClose={closeModal} {isSubmitting}>
|
||||
<FormGrid on:submit={onSubmit} bind:bindTo={form} {isSubmitting}>
|
||||
<FormColumns>
|
||||
@@ -291,21 +420,91 @@
|
||||
bind:value={formValues.name}
|
||||
placeholder="Marketing">Name</TextField
|
||||
>
|
||||
<section>
|
||||
{#each formValues.recipients as recipient}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
{recipient.email}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
</FormColumn>
|
||||
</FormColumns>
|
||||
<FormError message={modalError} />
|
||||
<FormFooter {closeModal} {isSubmitting} />
|
||||
</FormGrid>
|
||||
</Modal>
|
||||
|
||||
<!-- dynamic group create modal -->
|
||||
<Modal
|
||||
headerText="New dynamic group"
|
||||
bind:visible={isDynamicModalVisible}
|
||||
onClose={closeDynamicModal}
|
||||
{isSubmitting}
|
||||
>
|
||||
<FormGrid on:submit={onSubmitDynamicCreate} bind:bindTo={dynamicForm} {isSubmitting}>
|
||||
<FormColumns>
|
||||
<FormColumn>
|
||||
<TextField
|
||||
required
|
||||
minLength={1}
|
||||
maxLength={127}
|
||||
bind:value={dynamicFormValues.name}
|
||||
placeholder="Marketing">Name</TextField
|
||||
>
|
||||
<TextFieldSelect
|
||||
id="dynamic-group-filter-field"
|
||||
required
|
||||
bind:value={dynamicFormValues.filterField}
|
||||
options={dynamicFilterFields}
|
||||
>
|
||||
Filter field
|
||||
</TextFieldSelect>
|
||||
<TextField
|
||||
required
|
||||
minLength={1}
|
||||
maxLength={127}
|
||||
bind:value={dynamicFormValues.filterValue}
|
||||
placeholder="e.g. Engineering">Filter value</TextField
|
||||
>
|
||||
</FormColumn>
|
||||
</FormColumns>
|
||||
<FormError message={dynamicModalError} />
|
||||
<FormFooter closeModal={closeDynamicModal} {isSubmitting} />
|
||||
</FormGrid>
|
||||
</Modal>
|
||||
|
||||
<!-- dynamic group update modal -->
|
||||
<Modal
|
||||
headerText="Update dynamic group"
|
||||
bind:visible={isDynamicUpdateModalVisible}
|
||||
onClose={closeDynamicUpdateModal}
|
||||
{isSubmitting}
|
||||
>
|
||||
<FormGrid on:submit={onSubmitDynamicUpdate} bind:bindTo={dynamicUpdateForm} {isSubmitting}>
|
||||
<FormColumns>
|
||||
<FormColumn>
|
||||
<TextField
|
||||
required
|
||||
minLength={1}
|
||||
maxLength={127}
|
||||
bind:value={dynamicUpdateValues.name}
|
||||
placeholder="Marketing">Name</TextField
|
||||
>
|
||||
<TextFieldSelect
|
||||
id="dynamic-group-filter-field-a"
|
||||
required
|
||||
bind:value={dynamicUpdateValues.filterField}
|
||||
options={dynamicFilterFields}
|
||||
>
|
||||
Filter field
|
||||
</TextFieldSelect>
|
||||
<TextField
|
||||
required
|
||||
minLength={1}
|
||||
maxLength={127}
|
||||
bind:value={dynamicUpdateValues.filterValue}
|
||||
placeholder="e.g. Engineering">Filter value</TextField
|
||||
>
|
||||
</FormColumn>
|
||||
</FormColumns>
|
||||
<FormError message={dynamicUpdateError} />
|
||||
<FormFooter closeModal={closeDynamicUpdateModal} {isSubmitting} />
|
||||
</FormGrid>
|
||||
</Modal>
|
||||
|
||||
<DeleteAlert
|
||||
list={[
|
||||
'All assets will be deleted',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import DynamicLabel from '$lib/components/DynamicLabel.svelte';
|
||||
import { api } from '$lib/api/apiProxy.js';
|
||||
import { newTableURLParams } from '$lib/service/tableURLParams.js';
|
||||
import { page } from '$app/stores';
|
||||
@@ -45,7 +46,10 @@
|
||||
let groupValues = {
|
||||
id: null,
|
||||
name: null,
|
||||
companyID: null
|
||||
companyID: null,
|
||||
isDynamic: false,
|
||||
filterField: null,
|
||||
filterValue: null
|
||||
};
|
||||
let recipientSearch = '';
|
||||
let searchOptions = [];
|
||||
@@ -112,6 +116,9 @@
|
||||
groupValues.id = res.data.id;
|
||||
groupValues.name = res.data.name;
|
||||
groupValues.companyID = res.data.companyID;
|
||||
groupValues.isDynamic = res.data.isDynamic ?? false;
|
||||
groupValues.filterField = res.data.filterField ?? null;
|
||||
groupValues.filterValue = res.data.filterValue ?? null;
|
||||
} catch (err) {
|
||||
addToast('Failed to load group', 'Error');
|
||||
console.error('failed to load group', err);
|
||||
@@ -395,16 +402,35 @@
|
||||
<HeadTitle title="Group ({groupValues.name}" />
|
||||
<main>
|
||||
<Headline>Group Recipients</Headline>
|
||||
<SubHeadline>{groupValues.name}</SubHeadline>
|
||||
<BigButton
|
||||
on:click={openAddRecipientsModal}
|
||||
{...globalButtonDisabledAttributes(groupValues, contextCompanyID)}>Add Recipients</BigButton
|
||||
>
|
||||
<BigButton
|
||||
on:click={openImportModal}
|
||||
{...globalButtonDisabledAttributes(groupValues, contextCompanyID)}>Import from CSV</BigButton
|
||||
>
|
||||
<SubHeadline>Recipients</SubHeadline>
|
||||
<SubHeadline>
|
||||
{#if groupValues.isDynamic}
|
||||
<DynamicLabel />
|
||||
{/if}
|
||||
{groupValues.name}
|
||||
</SubHeadline>
|
||||
{#if groupValues.isDynamic}
|
||||
<p class="text-sm font-semibold text-slate-600 dark:text-gray-400 mt-2">Dynamic group</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mb-8">
|
||||
Recipients where
|
||||
<span class="font-mono text-xs bg-gray-200 dark:bg-gray-700 px-1 rounded"
|
||||
>{groupValues.filterField}</span
|
||||
>
|
||||
equals
|
||||
<span class="font-mono text-xs bg-gray-200 dark:bg-gray-700 px-1 rounded"
|
||||
>{groupValues.filterValue}</span
|
||||
>
|
||||
are included automatically.
|
||||
</p>
|
||||
{:else}
|
||||
<BigButton
|
||||
on:click={openAddRecipientsModal}
|
||||
{...globalButtonDisabledAttributes(groupValues, contextCompanyID)}>Add Recipients</BigButton
|
||||
>
|
||||
<BigButton
|
||||
on:click={openImportModal}
|
||||
{...globalButtonDisabledAttributes(groupValues, contextCompanyID)}>Import from CSV</BigButton
|
||||
>
|
||||
{/if}
|
||||
<Table
|
||||
columns={[
|
||||
{ column: 'Email', size: 'large' },
|
||||
@@ -456,10 +482,12 @@
|
||||
<TableCellEmpty />
|
||||
<TableCellAction>
|
||||
<TableDropDownEllipsis>
|
||||
<TableDeleteButton
|
||||
on:click={() => openDeleteAlert(recipient)}
|
||||
{...globalButtonDisabledAttributes(recipient, contextCompanyID)}
|
||||
></TableDeleteButton>
|
||||
{#if !groupValues.isDynamic}
|
||||
<TableDeleteButton
|
||||
on:click={() => openDeleteAlert(recipient)}
|
||||
{...globalButtonDisabledAttributes(groupValues, contextCompanyID)}
|
||||
></TableDeleteButton>
|
||||
{/if}
|
||||
</TableDropDownEllipsis>
|
||||
</TableCellAction>
|
||||
</TableRow>
|
||||
|
||||
Reference in New Issue
Block a user