mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-07-25 13:10:50 +02:00
add auto remove orphans
fix orphans in dynamic groups not included Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
@@ -1241,6 +1241,30 @@ export class API {
|
||||
*/
|
||||
delete: async (id) => {
|
||||
return await deleteJSON(this.getPath(`/company/${id}`));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the auto-prune orphaned recipients setting for a company.
|
||||
* Returns only the per-company enabled flag.
|
||||
*
|
||||
* @param {string} id - company ID
|
||||
* @returns {Promise<ApiResponse>}
|
||||
*/
|
||||
getAutoPrune: async (id) => {
|
||||
return await getJSON(this.getPath(`/company/${id}/option/auto-prune`));
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the auto-prune orphaned recipients setting for a company.
|
||||
*
|
||||
* @param {string} id - company ID
|
||||
* @param {boolean} enabled
|
||||
* @returns {Promise<ApiResponse>}
|
||||
*/
|
||||
setAutoPrune: async (id, enabled) => {
|
||||
return await postJSON(this.getPath(`/company/${id}/option/auto-prune`), {
|
||||
enabled: enabled
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2267,6 +2291,27 @@ export class API {
|
||||
return await getJSON(this.getPath(`/option/${key}`));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the global auto-prune orphaned recipients setting.
|
||||
* Returns the full option including per-company entries.
|
||||
*
|
||||
* @returns {Promise<ApiResponse>}
|
||||
*/
|
||||
getAutoPrune: async () => {
|
||||
return await getJSON(this.getPath(`/option/auto-prune`));
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the global auto-prune orphaned recipients setting.
|
||||
* The full option object (including per-company entries) must be supplied.
|
||||
*
|
||||
* @param {{ enabled: boolean, companies?: string[] }} option
|
||||
* @returns {Promise<ApiResponse>}
|
||||
*/
|
||||
setAutoPrune: async (option) => {
|
||||
return await postJSON(this.getPath(`/option/auto-prune`), option);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set setting by key and value.
|
||||
*
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
|
||||
// bindings
|
||||
let form = null;
|
||||
let companyAutoPruneEnabled = false;
|
||||
let companyAutoPruneEnabledOriginal = false;
|
||||
const formValues = {
|
||||
name: null,
|
||||
comment: null
|
||||
@@ -151,6 +153,9 @@
|
||||
modalError = res.error;
|
||||
return;
|
||||
}
|
||||
if (companyAutoPruneEnabled !== companyAutoPruneEnabledOriginal) {
|
||||
await saveCompanyAutoPrune(formValues.id, companyAutoPruneEnabled);
|
||||
}
|
||||
addToast('Company updated', 'Success');
|
||||
closeUpdateModal();
|
||||
} catch (e) {
|
||||
@@ -160,6 +165,18 @@
|
||||
refreshCompanies();
|
||||
};
|
||||
|
||||
const saveCompanyAutoPrune = async (id, enabled) => {
|
||||
try {
|
||||
const res = await api.company.setAutoPrune(id, enabled);
|
||||
if (!res.success) {
|
||||
addToast('Failed to save auto-prune setting', 'Error');
|
||||
}
|
||||
} catch (e) {
|
||||
addToast('Failed to save auto-prune setting', 'Error');
|
||||
console.error('failed to save company auto-prune', e);
|
||||
}
|
||||
};
|
||||
|
||||
const openDeleteAlert = async (company) => {
|
||||
isDeleteAlertVisible = true;
|
||||
deleteValues.id = company.id;
|
||||
@@ -216,6 +233,13 @@
|
||||
formValues.id = company.id;
|
||||
formValues.name = company.name;
|
||||
formValues.comment = company.comment || null;
|
||||
try {
|
||||
const optRes = await api.company.getAutoPrune(id);
|
||||
companyAutoPruneEnabled = optRes.success && optRes.data?.enabled === true;
|
||||
companyAutoPruneEnabledOriginal = companyAutoPruneEnabled;
|
||||
} catch (_) {
|
||||
companyAutoPruneEnabled = false;
|
||||
}
|
||||
isModalVisible = true;
|
||||
} catch (e) {
|
||||
addToast('Failed to get company', 'Error');
|
||||
@@ -232,6 +256,8 @@
|
||||
formValues.id = null;
|
||||
formValues.name = null;
|
||||
formValues.comment = null;
|
||||
companyAutoPruneEnabled = false;
|
||||
companyAutoPruneEnabledOriginal = false;
|
||||
form.reset();
|
||||
};
|
||||
|
||||
@@ -383,11 +409,61 @@
|
||||
id="company-comment"
|
||||
bind:value={formValues.comment}
|
||||
maxlength={1000000}
|
||||
rows="20"
|
||||
rows="8"
|
||||
placeholder="Add notes about this company..."
|
||||
class="w-full p-4 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 resize-y dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center mb-2">
|
||||
<p class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Auto-Prune Orphaned Recipients
|
||||
</p>
|
||||
</div>
|
||||
<div class="inline-flex flex-col space-y-2 min-w-64 mb-4">
|
||||
<label
|
||||
class="flex items-start gap-3 p-3 border rounded-lg cursor-pointer transition-colors {companyAutoPruneEnabled
|
||||
? 'bg-blue-50 dark:bg-blue-900/20 border-blue-500 dark:border-blue-600'
|
||||
: 'border-gray-300 dark:border-gray-600'}"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
checked={companyAutoPruneEnabled}
|
||||
on:change={() => (companyAutoPruneEnabled = true)}
|
||||
class="mt-0.5 w-4 h-4 text-blue-600 bg-gray-100 dark:bg-gray-700 border-gray-300 dark:border-gray-600 focus:ring-blue-500 focus:ring-2"
|
||||
/>
|
||||
<div class="text-left flex-1">
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-gray-100 block"
|
||||
>Enabled</span
|
||||
>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400 block mt-0.5"
|
||||
>Orphaned recipients are deleted automatically each hour</span
|
||||
>
|
||||
</div>
|
||||
</label>
|
||||
<label
|
||||
class="flex items-start gap-3 p-3 border rounded-lg cursor-pointer transition-colors {!companyAutoPruneEnabled
|
||||
? 'bg-blue-50 dark:bg-blue-900/20 border-blue-500 dark:border-blue-600'
|
||||
: 'border-gray-300 dark:border-gray-600'}"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
checked={!companyAutoPruneEnabled}
|
||||
on:change={() => (companyAutoPruneEnabled = false)}
|
||||
class="mt-0.5 w-4 h-4 text-blue-600 bg-gray-100 dark:bg-gray-700 border-gray-300 dark:border-gray-600 focus:ring-blue-500 focus:ring-2"
|
||||
/>
|
||||
<div class="text-left flex-1">
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-gray-100 block"
|
||||
>Disabled</span
|
||||
>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400 block mt-0.5"
|
||||
>Orphaned recipients are kept until manually deleted</span
|
||||
>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormError message={modalError} />
|
||||
|
||||
@@ -424,6 +424,7 @@
|
||||
<div class="flex gap-3">
|
||||
<BigButton on:click={openCreateModal}>New recipient</BigButton>
|
||||
<BigButton on:click={openImportModal}>Import from CSV</BigButton>
|
||||
<BigButton on:click={() => goto('/recipient/orphaned/')}>View Orphaned</BigButton>
|
||||
</div>
|
||||
<Table
|
||||
isGhost={isRecipientsTableLoading}
|
||||
|
||||
@@ -341,7 +341,6 @@
|
||||
<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
|
||||
columns={[
|
||||
|
||||
@@ -62,6 +62,11 @@
|
||||
let isSSOEnabled = false;
|
||||
let isSSODeleteAlertVisible = false;
|
||||
|
||||
// auto-prune settings
|
||||
let autoPruneOption = { enabled: false, companies: [] };
|
||||
let autoPruneEnabled = false;
|
||||
let autoPruneError = '';
|
||||
|
||||
// Import functionality
|
||||
let importError = '';
|
||||
let isImportSubmitting = false;
|
||||
@@ -108,6 +113,7 @@
|
||||
await refreshUpdateCached();
|
||||
await refreshBackupList();
|
||||
await refreshDisplayMode();
|
||||
await refreshAutoPrune();
|
||||
if (!ssoSettingsFormValues.redirectURL) {
|
||||
ssoSettingsFormValues.redirectURL = `${location.origin}/api/v1/sso/entra-id/auth`;
|
||||
}
|
||||
@@ -117,6 +123,37 @@
|
||||
});
|
||||
|
||||
// component logic
|
||||
async function refreshAutoPrune() {
|
||||
try {
|
||||
const res = await api.option.getAutoPrune();
|
||||
if (res.success) {
|
||||
autoPruneOption = res.data;
|
||||
autoPruneEnabled = res.data.enabled === true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('failed to load auto-prune setting', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function setAutoPruneValue(enabled) {
|
||||
autoPruneError = '';
|
||||
// read-modify-write: preserve per-company entries
|
||||
const updated = { ...autoPruneOption, enabled };
|
||||
try {
|
||||
const res = await api.option.setAutoPrune(updated);
|
||||
if (!res.success) {
|
||||
autoPruneError = res.error;
|
||||
return;
|
||||
}
|
||||
autoPruneOption = updated;
|
||||
autoPruneEnabled = enabled;
|
||||
addToast('Auto-prune setting saved', 'Success');
|
||||
} catch (e) {
|
||||
autoPruneError = 'Failed to save auto-prune setting';
|
||||
console.error('failed to set auto-prune setting', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshSettings() {
|
||||
try {
|
||||
const res = immediateResponseHandler(await api.option.get('max_file_upload_size_mb'));
|
||||
@@ -579,6 +616,67 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Auto-Prune Recipients Card -->
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm dark:shadow-gray-900/50 border border-gray-100 dark:border-gray-700 min-h-[300px] flex flex-col transition-colors duration-200"
|
||||
>
|
||||
<h2
|
||||
class="text-xl font-semibold text-gray-700 dark:text-gray-200 mb-6 transition-colors duration-200"
|
||||
>
|
||||
Auto-Prune Recipients
|
||||
</h2>
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="space-y-4">
|
||||
<p class="text-gray-600 dark:text-gray-300 text-sm transition-colors duration-200">
|
||||
Automatically delete orphaned recipients (not in any group) on a hourly schedule.
|
||||
</p>
|
||||
<div class="space-y-3">
|
||||
<label
|
||||
class="flex items-start gap-3 p-3 border rounded-lg cursor-pointer transition-colors {autoPruneEnabled
|
||||
? 'bg-blue-50 dark:bg-blue-900/20 border-blue-500 dark:border-blue-600'
|
||||
: 'border-gray-300 dark:border-gray-600'}"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
checked={autoPruneEnabled}
|
||||
on:change={() => setAutoPruneValue(true)}
|
||||
class="mt-0.5 w-4 h-4 text-blue-600 bg-gray-100 dark:bg-gray-700 border-gray-300 dark:border-gray-600 focus:ring-blue-500 focus:ring-2"
|
||||
/>
|
||||
<div class="text-left flex-1">
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-gray-100 block">
|
||||
Enabled
|
||||
</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400 block mt-0.5">
|
||||
Orphaned recipients are deleted automatically each hour
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
<label
|
||||
class="flex items-start gap-3 p-3 border rounded-lg cursor-pointer transition-colors {!autoPruneEnabled
|
||||
? 'bg-blue-50 dark:bg-blue-900/20 border-blue-500 dark:border-blue-600'
|
||||
: 'border-gray-300 dark:border-gray-600'}"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
checked={!autoPruneEnabled}
|
||||
on:change={() => setAutoPruneValue(false)}
|
||||
class="mt-0.5 w-4 h-4 text-blue-600 bg-gray-100 dark:bg-gray-700 border-gray-300 dark:border-gray-600 focus:ring-blue-500 focus:ring-2"
|
||||
/>
|
||||
<div class="text-left flex-1">
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-gray-100 block">
|
||||
Disabled
|
||||
</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400 block mt-0.5">
|
||||
Orphaned recipients are kept until manually deleted
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<FormError message={autoPruneError} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- General Settings Card -->
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm dark:shadow-gray-900/50 border border-gray-100 dark:border-gray-700 min-h-[300px] flex flex-col transition-colors duration-200"
|
||||
|
||||
Reference in New Issue
Block a user