change access directive and add management for proxy allow list

Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
Ronni Skansing
2025-10-23 12:55:51 +02:00
parent 847a3552b1
commit d6a717ebfa
16 changed files with 961 additions and 443 deletions
+25
View File
@@ -2897,6 +2897,31 @@ export class API {
}
};
/**
* ipAllowList is the API for IP Allow List related operations.
*/
ipAllowList = {
/**
* Get IP allow list entries for a specific proxy configuration.
*
* @param {string} proxyConfigID
* @returns {Promise<ApiResponse>}
*/
getForProxyConfig: async (proxyConfigID) => {
return await getJSON(this.getPath(`/ip-allow-list/proxy-config/${proxyConfigID}`));
},
/**
* Clear all entries for a specific proxy configuration.
*
* @param {string} proxyConfigID
* @returns {Promise<ApiResponse>}
*/
clearForProxyConfig: async (proxyConfigID) => {
return await deleteJSON(this.getPath(`/ip-allow-list/clear-proxy-config/${proxyConfigID}`));
}
};
/**
* import is for importing assets, landing pages and etc
*/
+1 -1
View File
@@ -327,7 +327,7 @@
<img class="w-full" src="/close-white.svg" alt="" />
</button>
</div>
<div class="px-8 overflow-y-auto {scrollBarClassesVertical}">
<div class="px-8 overflow-y-auto overflow-x-visible {scrollBarClassesVertical}">
<slot />
</div>
</section>
@@ -26,6 +26,7 @@
let inputElement;
let dropdownElement;
let justSelected = false;
let dropdownPosition = { top: 0, left: 0, width: 0 };
// Simple function to filter options based on input
const filterOptions = (searchValue) => {
@@ -51,6 +52,7 @@
showDropdown = true;
hasTyped = false; // Reset typing flag to show all options
allOptions = [...optionsArray]; // Show all options on focus
updateDropdownPosition();
}
justSelected = false; // Reset the flag
};
@@ -61,6 +63,7 @@
showDropdown = true;
hasTyped = true; // User has typed, enable filtering
allOptions = filterOptions(value);
updateDropdownPosition();
};
// Select an option
@@ -84,6 +87,18 @@
hasTyped = false; // Reset typing flag when closing
};
// Update dropdown position for fixed positioning
const updateDropdownPosition = () => {
if (inputElement) {
const rect = inputElement.getBoundingClientRect();
dropdownPosition = {
top: rect.bottom + 4,
left: rect.left,
width: rect.width
};
}
};
// Handle blur to close dropdown when tabbing out
const handleBlur = (e) => {
// Use setTimeout to allow click events on options to complete first
@@ -187,10 +202,14 @@
value = value || defaultValue;
const unsubscribe = activeFormElementSubscribe(_id, closeDropdown);
document.addEventListener('click', handleOutsideClick);
window.addEventListener('scroll', updateDropdownPosition, true);
window.addEventListener('resize', updateDropdownPosition);
return () => {
unsubscribe();
document.removeEventListener('click', handleOutsideClick);
window.removeEventListener('scroll', updateDropdownPosition, true);
window.removeEventListener('resize', updateDropdownPosition);
};
});
@@ -215,6 +234,8 @@
parentForm.removeEventListener('reset', parentFormResetListener);
}
document.removeEventListener('click', handleOutsideClick);
window.removeEventListener('scroll', updateDropdownPosition, true);
window.removeEventListener('resize', updateDropdownPosition);
});
// Generate unique IDs for accessibility
@@ -323,15 +344,14 @@
{#if showDropdown}
<div
bind:this={dropdownElement}
class="absolute top-10 z-50"
class:w-28={size == 'small'}
class:w-60={size == 'normal'}
class="fixed z-[9999]"
style="top: {dropdownPosition.top}px; left: {dropdownPosition.left}px; width: {dropdownPosition.width}px;"
>
<ul
id={listboxId}
role="listbox"
aria-labelledby={labelId}
class="bg-gray-100 dark:bg-gray-900 list-none mt-4 z-[999] rounded-md min-w-fit shadow-md border border-gray-200 dark:border-gray-700/60 max-h-40 overflow-y-scroll transition-colors duration-200"
class="bg-gray-100 dark:bg-gray-900 list-none mt-4 z-[9999] rounded-md min-w-fit shadow-lg border border-gray-200 dark:border-gray-700/60 max-h-40 overflow-y-scroll transition-colors duration-200"
>
{#if allOptions.length}
{#each allOptions as option, index}
+183 -198
View File
@@ -45,22 +45,20 @@
let form = null;
let formValues = {
id: null,
templateType: null,
templateType: 'Email',
name: null,
domain: null,
landingPage: null,
landingPageType: 'page', // 'page' or 'proxy'
beforeLandingPage: null,
beforeLandingPageType: 'page', // 'page' or 'proxy'
afterLandingPage: null,
afterLandingPageType: 'page', // 'page' or 'proxy'
afterLandingPageRedirectURL: null,
afterLandingPageRedirectURL: '',
email: null,
smtpConfiguration: null,
apiSender: null,
urlIdentifier: 'id',
stateIdentifier: 'session',
urlPath: null
urlPath: ''
};
let contextCompanyID = null;
@@ -97,6 +95,7 @@
id: null,
name: null
};
let showAdvancedOptions = false;
$: {
modalText = getModalText('template', modalMode);
@@ -304,26 +303,14 @@
formValues.landingPageType === 'proxy'
? landingProxyMap.byValueOrNull(formValues.landingPage)
: null,
beforeLandingPageID:
formValues.beforeLandingPageType === 'page'
? beforeLandingPageMap.byValueOrNull(formValues.beforeLandingPage)
: null,
beforeLandingProxyID:
formValues.beforeLandingPageType === 'proxy'
? beforeLandingProxyMap.byValueOrNull(formValues.beforeLandingPage)
: null,
afterLandingPageID:
formValues.afterLandingPageType === 'page'
? afterLandingPageMap.byValueOrNull(formValues.afterLandingPage)
: null,
afterLandingProxyID:
formValues.afterLandingPageType === 'proxy'
? afterLandingProxyMap.byValueOrNull(formValues.afterLandingPage)
: null,
afterLandingPageRedirectURL: formValues.afterLandingPageRedirectURL,
beforeLandingPageID: beforeLandingPageMap.byValueOrNull(formValues.beforeLandingPage),
beforeLandingProxyID: null,
afterLandingPageID: afterLandingPageMap.byValueOrNull(formValues.afterLandingPage),
afterLandingProxyID: null,
afterLandingPageRedirectURL: formValues.afterLandingPageRedirectURL || '',
urlIdentifierID: identifierMap.byValueOrNull(formValues.urlIdentifier),
stateIdentifierID: identifierMap.byValueOrNull(formValues.stateIdentifier),
urlPath: formValues.urlPath,
urlPath: formValues.urlPath || '',
companyID: contextCompanyID
});
if (!res.success) {
@@ -356,26 +343,14 @@
formValues.landingPageType === 'proxy'
? landingProxyMap.byValueOrNull(formValues.landingPage)
: null,
beforeLandingPageID:
formValues.beforeLandingPageType === 'page'
? beforeLandingPageMap.byValueOrNull(formValues.beforeLandingPage)
: null,
beforeLandingProxyID:
formValues.beforeLandingPageType === 'proxy'
? beforeLandingProxyMap.byValueOrNull(formValues.beforeLandingPage)
: null,
afterLandingPageID:
formValues.afterLandingPageType === 'page'
? afterLandingPageMap.byValueOrNull(formValues.afterLandingPage)
: null,
afterLandingProxyID:
formValues.afterLandingPageType === 'proxy'
? afterLandingProxyMap.byValueOrNull(formValues.afterLandingPage)
: null,
afterLandingPageRedirectURL: formValues.afterLandingPageRedirectURL,
beforeLandingPageID: beforeLandingPageMap.byValueOrNull(formValues.beforeLandingPage),
beforeLandingProxyID: null,
afterLandingPageID: afterLandingPageMap.byValueOrNull(formValues.afterLandingPage),
afterLandingProxyID: null,
afterLandingPageRedirectURL: formValues.afterLandingPageRedirectURL || '',
urlIdentifierID: identifierMap.byValueOrNull(formValues.urlIdentifier),
stateIdentifierID: identifierMap.byValueOrNull(formValues.stateIdentifier),
urlPath: formValues.urlPath
urlPath: formValues.urlPath || ''
});
if (!res.success) {
modalError = res.error;
@@ -422,24 +397,23 @@
form.reset();
formValues = {
id: null,
templateType: null,
templateType: 'Email',
name: null,
domain: null,
landingPage: null,
landingPageType: 'page',
beforeLandingPage: null,
beforeLandingPageType: 'page',
afterLandingPage: null,
afterLandingPageType: 'page',
afterLandingPageRedirectURL: null,
afterLandingPageRedirectURL: '',
email: null,
smtpConfiguration: null,
apiSender: null,
urlIdentifier: 'id',
stateIdentifier: 'session',
urlPath: null
urlPath: ''
};
modalError = '';
showAdvancedOptions = false;
};
/** @param {string} id */
@@ -488,7 +462,7 @@
if (template.smtpConfigurationID) {
formValues.templateType = 'Email';
} else {
formValues.templateType = 'External API';
formValues.templateType = 'API Sender';
}
formValues.domain = domainMap.byKey(template.domainID);
formValues.email = emailMap.byKey(template.emailID);
@@ -502,28 +476,32 @@
formValues.landingPageType = 'proxy';
}
// handle before landing page (page or proxy)
// handle before landing page (only pages, no proxy)
if (template.beforeLandingPageID) {
formValues.beforeLandingPage = beforeLandingPageMap.byKey(template.beforeLandingPageID);
formValues.beforeLandingPageType = 'page';
} else if (template.beforeLandingProxyID) {
formValues.beforeLandingPage = beforeLandingProxyMap.byKey(template.beforeLandingProxyID);
formValues.beforeLandingPageType = 'proxy';
}
// handle after landing page (page or proxy)
// handle after landing page (only pages, no proxy)
if (template.afterLandingPageID) {
formValues.afterLandingPage = afterLandingPageMap.byKey(template.afterLandingPageID);
formValues.afterLandingPageType = 'page';
} else if (template.afterLandingProxyID) {
formValues.afterLandingPage = afterLandingProxyMap.byKey(template.afterLandingProxyID);
formValues.afterLandingPageType = 'proxy';
}
formValues.afterLandingPageRedirectURL = template.afterLandingPageRedirectURL;
formValues.afterLandingPageRedirectURL = template.afterLandingPageRedirectURL || '';
formValues.urlIdentifier = identifierMap.byKey(template.urlIdentifierID);
formValues.stateIdentifier = identifierMap.byKey(template.stateIdentifierID);
formValues.urlPath = template.urlPath;
formValues.urlPath = template.urlPath || '';
// set advanced options visibility based on template configuration
showAdvancedOptions = !!(
(
(template.urlPath && template.urlPath !== '') ||
(template.afterLandingPageRedirectURL && template.afterLandingPageRedirectURL !== '') ||
(template.urlIdentifierID && identifierMap.byKey(template.urlIdentifierID) !== 'id') ||
(template.stateIdentifierID &&
identifierMap.byKey(template.stateIdentifierID) !== 'session') ||
template.apiSenderID
) // Show advanced if using External API
);
};
</script>
@@ -660,7 +638,7 @@
</a>
{/if}
</TableCell>
<TableCellCheck value={template.isComplete} />
<TableCellCheck value={template.isUsable} />
{#if contextCompanyID}
<TableCellScope companyID={template.companyID} />
{/if}
@@ -794,69 +772,10 @@ Simulation URLs to allow:\n${allowListingData.simulationUrl}\n
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<TextField
required
minLength={1}
maxLength={64}
bind:value={formValues.name}
placeholder="Intranet login">Name</TextField
<TextField required minLength={1} maxLength={64} bind:value={formValues.name}
>Name</TextField
>
</div>
<div>
<div class="w-full">
<SelectSquare
label="Delivery Type"
options={[
{ icon: '✉️', value: 'Email', label: 'Email' },
{ icon: '🔌', value: 'External API', label: 'API' }
]}
bind:value={formValues.templateType}
/>
</div>
</div>
</div>
</div>
<!-- Delivery Configuration Section -->
<div class="w-full">
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">
Delivery Configuration
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
{#if formValues.templateType === 'Email' || !formValues.templateType}
<TextFieldSelect
id="smtpConfig"
required
bind:value={formValues.smtpConfiguration}
options={smtpConfigurationMap.values()}>SMTP Configuration</TextFieldSelect
>
{:else if formValues.templateType === 'External API'}
<TextFieldSelect
required
id="apiSender"
bind:value={formValues.apiSender}
options={apiSenderMap.values()}>API Sender</TextFieldSelect
>
{/if}
</div>
<div>
<TextFieldSelect
required
id="email"
bind:value={formValues.email}
options={emailMap.values()}>Email</TextFieldSelect
>
</div>
</div>
</div>
<!-- Domain & URL Configuration Section -->
<div class="w-full">
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">
Domain & URL Configuration
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
<div>
<TextFieldSelect
required
@@ -865,32 +784,41 @@ Simulation URLs to allow:\n${allowListingData.simulationUrl}\n
options={domainMap.values()}>Domain</TextFieldSelect
>
</div>
</div>
</div>
<!-- Delivery Configuration Section -->
<!-- Email/API Configuration -->
<div class="w-full">
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">
Email Configuration
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
{#if formValues.templateType === 'Email' || !formValues.templateType}
<div>
<TextFieldSelect
required
id="smtpConfiguration"
bind:value={formValues.smtpConfiguration}
options={smtpConfigurationMap.values()}>SMTP Configuration</TextFieldSelect
>
</div>
{:else if formValues.templateType === 'External API'}
<div>
<TextFieldSelect
required
id="apiSender"
bind:value={formValues.apiSender}
options={apiSenderMap.values()}>API Sender</TextFieldSelect
>
</div>
{/if}
<div>
<TextField
toolTipText="Path after the domain name."
<TextFieldSelect
id="email"
bind:value={formValues.email}
optional
minLength={1}
maxLength={1024}
bind:value={formValues.urlPath}
placeholder="/employee/login">URL Path</TextField
>
</div>
<div>
<TextFieldSelect
id="urlIdentifier"
toolTipText="This is the query param key used in the phishing URL."
required
bind:value={formValues.urlIdentifier}
options={identifierMap.values()}>Query param key</TextFieldSelect
>
</div>
<div>
<TextFieldSelect
id="stateIdentifier"
toolTipText="This is the query param key used for state."
required
bind:value={formValues.stateIdentifier}
options={identifierMap.values()}>State param key</TextFieldSelect
options={emailMap.values()}>Email</TextFieldSelect
>
</div>
</div>
@@ -902,13 +830,11 @@ Simulation URLs to allow:\n${allowListingData.simulationUrl}\n
<div class="grid grid-cols-1 md:grid-cols-5 gap-6">
<div class="md:col-span-2 flex flex-col space-y-6">
<!-- Before Landing Page -->
<TextFieldSelectWithType
<TextFieldSelect
id="beforeLandingPage"
bind:value={formValues.beforeLandingPage}
bind:type={formValues.beforeLandingPageType}
pageOptions={beforeLandingPageMap.values()}
proxyOptions={beforeLandingProxyMap.values()}
optional>Before Landing</TextFieldSelectWithType
options={beforeLandingPageMap.values()}
optional>Before Landing</TextFieldSelect
>
<!-- Landing Page -->
@@ -922,24 +848,12 @@ Simulation URLs to allow:\n${allowListingData.simulationUrl}\n
>
<!-- After Landing Page -->
<TextFieldSelectWithType
<TextFieldSelect
id="afterLandingPage"
bind:value={formValues.afterLandingPage}
bind:type={formValues.afterLandingPageType}
pageOptions={afterLandingPageMap.values()}
proxyOptions={afterLandingProxyMap.values()}
optional>After Landing</TextFieldSelectWithType
options={afterLandingPageMap.values()}
optional>After Landing</TextFieldSelect
>
<div>
<TextField
bind:value={formValues.afterLandingPageRedirectURL}
type="url"
minLength={1}
maxLength={255}
placeholder="https://example.com/u-been-phished">POST redirect URL</TextField
>
</div>
</div>
<!-- Visualization - Takes 2 columns on larger screens -->
@@ -961,9 +875,7 @@ Simulation URLs to allow:\n${allowListingData.simulationUrl}\n
<p
class="text-xs font-medium text-gray-900 dark:text-gray-300 transition-colors duration-200"
>
Before Landing {formValues.beforeLandingPageType === 'proxy'
? 'Proxy'
: 'Page'}
Before Landing Page
</p>
<p
class="text-xs text-gray-500 dark:text-gray-400 truncate max-w-[180px] transition-colors duration-200"
@@ -1026,7 +938,7 @@ Simulation URLs to allow:\n${allowListingData.simulationUrl}\n
<p
class="text-xs font-medium text-gray-900 dark:text-gray-300 transition-colors duration-200"
>
After Landing {formValues.afterLandingPageType === 'proxy' ? 'Proxy' : 'Page'}
After Landing Page
</p>
<p
class="text-xs text-gray-500 dark:text-gray-400 truncate max-w-[180px] transition-colors duration-200"
@@ -1036,45 +948,118 @@ Simulation URLs to allow:\n${allowListingData.simulationUrl}\n
</div>
</div>
<!-- Down Arrow -->
<div class="flex">
<div
class="ml-5 w-0.5 h-4 bg-gray-300 dark:bg-gray-600 transition-colors duration-200"
></div>
</div>
{#if showAdvancedOptions}
<!-- Down Arrow -->
<div class="flex">
<div
class="ml-5 w-0.5 h-4 bg-gray-300 dark:bg-gray-600 transition-colors duration-200"
></div>
</div>
<!-- Final Redirect -->
<div class="flex items-center">
<div
class={`w-10 h-10 rounded-lg flex items-center justify-center border mr-3 transition-colors duration-200
${formValues.afterLandingPageRedirectURL ? 'bg-blue-50 dark:bg-blue-900/40 border-blue-300 dark:border-blue-500' : 'bg-gray-100 dark:bg-gray-800/60 border-gray-300 dark:border-gray-600'}`}
>
<span
class={`text-xl transition-colors duration-200 ${formValues.afterLandingPageRedirectURL ? 'text-blue-500 dark:text-blue-300' : 'text-gray-400 dark:text-gray-500'}`}
>4</span
<!-- Final Redirect -->
<div class="flex items-center">
<div
class={`w-10 h-10 rounded-lg flex items-center justify-center border mr-3 transition-colors duration-200
${formValues.afterLandingPageRedirectURL ? 'bg-blue-50 dark:bg-blue-900/40 border-blue-300 dark:border-blue-500' : 'bg-gray-100 dark:bg-gray-800/60 border-gray-300 dark:border-gray-600'}`}
>
<span
class={`text-xl transition-colors duration-200 ${formValues.afterLandingPageRedirectURL ? 'text-blue-500 dark:text-blue-300' : 'text-gray-400 dark:text-gray-500'}`}
>4</span
>
</div>
<div class="flex-1">
<p
class="text-xs font-medium text-gray-900 dark:text-gray-300 transition-colors duration-200"
>
POST Redirect URL
</p>
<p
class="text-xs text-gray-500 dark:text-gray-400 truncate max-w-[180px] transition-colors duration-200"
>
{formValues.afterLandingPageRedirectURL || 'Not set'}
</p>
</div>
</div>
<div class="flex-1">
<p
class="text-xs font-medium text-gray-900 dark:text-gray-300 transition-colors duration-200"
>
POST Redirect URL
</p>
<p
class="text-xs text-gray-500 dark:text-gray-400 truncate max-w-[180px] transition-colors duration-200"
>
{formValues.afterLandingPageRedirectURL || 'Not set'}
</p>
</div>
</div>
{/if}
</div>
</div>
</div>
</div>
<FormError message={modalError} />
</div>
<!-- Advanced Options Section -->
{#if !showAdvancedOptions}
<div class="mt-6 text-center">
<button
type="button"
class="text-cta-blue hover:text-blue-700 dark:text-white dark:hover:text-gray-200 text-sm transition-colors duration-200 underline"
on:click={() => (showAdvancedOptions = true)}
>
Show advanced options
</button>
</div>
{/if}
{#if showAdvancedOptions}
<div class="w-full border-t pt-6 mt-6">
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">
Advanced Options
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<SelectSquare
label="Delivery Type"
width="small"
center={false}
options={[
{ value: 'Email', label: 'Email' },
{ value: 'External API', label: 'External API' }
]}
bind:value={formValues.templateType}
/>
</div>
<div>
<TextField
toolTipText="Path after the domain name."
optional
minLength={1}
maxLength={1024}
bind:value={formValues.urlPath}
placeholder="/employee/login">URL Path</TextField
>
</div>
<div>
<TextFieldSelect
id="urlIdentifier"
toolTipText="This is the query param key used in the phishing URL."
required
bind:value={formValues.urlIdentifier}
options={identifierMap.values()}>Query param key</TextFieldSelect
>
</div>
<div>
<TextFieldSelect
id="stateIdentifier"
toolTipText="This is the query param key used for state."
required
bind:value={formValues.stateIdentifier}
options={identifierMap.values()}>State param key</TextFieldSelect
>
</div>
<div>
<TextField
bind:value={formValues.afterLandingPageRedirectURL}
type="url"
minLength={1}
maxLength={255}
placeholder="https://example.com/u-been-phished">POST redirect URL</TextField
>
</div>
</div>
</div>
{/if}
</div>
<FormFooter {closeModal} {isSubmitting} />
</FormGrid>
</Modal>
+198 -126
View File
@@ -149,6 +149,8 @@
let allowDenyType = 'none';
let allAllowDeny = [];
let showSecurityOptions = false;
let showAdvancedOptionsStep3 = false;
let showAdvancedOptionsStep4 = false;
// reactive statement to enable security options when deny page is set
$: if (formValues.denyPageValue && formValues.denyPageValue.trim() !== '') {
@@ -261,6 +263,7 @@
let isSubmitting = false;
let isTableLoading = false;
let modalText = '';
let isValidatingName = false;
let weekDaysAvailable = [];
let isDeleteAlertVisible = false;
@@ -334,7 +337,7 @@
});
const nextStep = async () => {
if (validateCurrentStep()) {
if (await validateCurrentStep()) {
currentStep = Math.min(currentStep + 1, campaignSteps.length);
modalError = '';
// reset tab focus after dom update - only for explicit step navigation
@@ -372,10 +375,10 @@
}, 0);
};
const validateCurrentStep = () => {
const validateCurrentStep = async () => {
switch (currentStep) {
case 1:
return validateBasicInfo();
return await validateBasicInfo();
case 2:
return validateRecipients();
case 3:
@@ -402,8 +405,31 @@
return true;
};
const validateBasicInfo = () => {
return checkCurrentStepValidity();
const validateBasicInfo = async () => {
if (!checkCurrentStepValidity()) {
return false;
}
// check if campaign name exists
if (formValues.name?.length) {
isValidatingName = true;
try {
const nameExists = await campaignNameExists(formValues.name);
if (nameExists) {
/** @type {HTMLInputElement} */
const ele = document.querySelector('#campaignName');
if (ele) {
ele.setCustomValidity('Name is used by another campaign');
ele.reportValidity();
}
return false;
}
} finally {
isValidatingName = false;
}
}
return true;
};
const validateRecipients = () => {
@@ -733,23 +759,22 @@
};
/** @param {string} name */
const campaignNameExits = async (name) => {
const campaignNameExists = async (name) => {
if (!name?.length) return false;
try {
const res = await api.campaign.getByName(name, contextCompanyID);
/** @type {HTMLInputElement} */
const ele = document.querySelector('#campaignName');
if (
res.data &&
(modalMode === 'create' || modalMode === 'copy' || res.data.id !== formValues.id)
) {
ele.setCustomValidity('Name is used by another campaign');
ele.reportValidity();
} else {
ele.setCustomValidity('');
return true;
}
return false;
} catch (e) {
addToast('Failed to check if campaign name is used', 'Error');
console.error('Failed to check if campaign name is used', e);
return false;
}
};
@@ -795,6 +820,8 @@
allowDenyType = 'none';
spreadOption = SPREAD_MANUAL;
modalError = '';
showAdvancedOptionsStep3 = false;
showAdvancedOptionsStep4 = false;
};
const onChangeScheduleType = () => {
@@ -816,6 +843,7 @@
modalMode = null;
isModalVisible = false;
currentStep = 1;
isValidatingName = false;
if (form) form.reset();
resetFormValues();
};
@@ -868,17 +896,23 @@
name: copyMode ? `${campaign.name} (Copy)` : campaign.name,
sortField: sortField.byValue(campaign.sortField),
sortOrder: sortOrder.byValue(campaign.sortOrder),
sendStartAt: campaign.sendStartAt,
sendEndAt: campaign.sendEndAt,
scheduledStartAt: campaign.sendStartAt
? local_yyyy_mm_dd(new Date(campaign.sendStartAt))
: null,
scheduledEndAt: campaign.sendEndAt ? local_yyyy_mm_dd(new Date(campaign.sendEndAt)) : null,
constraintWeekDays: weekDayBinaryToAvailable(campaign.constraintWeekDays),
contraintStartTime: utcTimeToLocal(campaign.constraintStartTime),
contraintEndTime: utcTimeToLocal(campaign.constraintEndTime),
closeAt: campaign.closeAt,
anonymizeAt: campaign.anonymizeAt,
sendStartAt: copyMode ? null : campaign.sendStartAt,
sendEndAt: copyMode ? null : campaign.sendEndAt,
scheduledStartAt: copyMode
? null
: campaign.sendStartAt
? local_yyyy_mm_dd(new Date(campaign.sendStartAt))
: null,
scheduledEndAt: copyMode
? null
: campaign.sendEndAt
? local_yyyy_mm_dd(new Date(campaign.sendEndAt))
: null,
constraintWeekDays: copyMode ? [] : weekDayBinaryToAvailable(campaign.constraintWeekDays),
contraintStartTime: copyMode ? null : utcTimeToLocal(campaign.constraintStartTime),
contraintEndTime: copyMode ? null : utcTimeToLocal(campaign.constraintEndTime),
closeAt: copyMode ? null : campaign.closeAt,
anonymizeAt: copyMode ? null : campaign.anonymizeAt,
saveSubmittedData: campaign.saveSubmittedData,
isAnonymous: campaign.isAnonymous,
isTest: campaign.isTest,
@@ -886,19 +920,30 @@
webhookValue: webhookMap.byKey(campaign.webhookID)
};
formValues.recipientGroups = campaign.recipientGroupIDs.map((id) =>
recipientGroupMap.byKey(id)
);
formValues.selectedCount = formValues.recipientGroups.reduce((acc, label) => {
const id = recipientGroupMap.byValue(label);
const group = recipientGroupsByID[id];
return acc + group.recipientCount;
}, 0);
if (copyMode) {
// reset recipient groups when copying
formValues.recipientGroups = [];
formValues.selectedCount = 0;
} else {
formValues.recipientGroups = campaign.recipientGroupIDs.map((id) =>
recipientGroupMap.byKey(id)
);
formValues.selectedCount = formValues.recipientGroups.reduce((acc, label) => {
const id = recipientGroupMap.byValue(label);
const group = recipientGroupsByID[id];
return acc + group.recipientCount;
}, 0);
}
if (!formValues.sendStartAt && !formValues.sendEndAt) {
scheduleType = 'self-managed';
}
// reset schedule type to basic when copying since delivery times are cleared
if (copyMode) {
scheduleType = 'basic';
}
if (campaign.allowDeny.length > 0) {
allowDenyType = campaign.allowDeny[0].allowed ? 'allow' : 'deny';
} else {
@@ -911,6 +956,16 @@
formValues.denyPageValue = campaign.denyPage.name;
}
// set advanced options visibility based on campaign configuration
showAdvancedOptionsStep3 = !!(campaign.closeAt || campaign.anonymizeAt);
showAdvancedOptionsStep4 = !!(
campaign.webhookID ||
campaign.denyPage ||
campaign.evasionPage ||
campaign.allowDeny?.length
);
if (campaign.evasionPage) {
formValues.evasionPageValue = campaign.evasionPage.name;
}
@@ -1176,25 +1231,15 @@
const ele = document.querySelector('#campaignName');
ele.setCustomValidity('');
}}
onBlur={() => {
formValues.name.length && campaignNameExits(formValues.name);
}}>Name</TextField
>
Name
</TextField>
<TextFieldSelect
required
id="template"
bind:value={formValues.template}
options={Array.from(templateMap.values())}>Template</TextFieldSelect
>
<div class="py-2">
<SelectSquare
label="Type"
width="small"
toolTipText={'Tests are not included in statistics'}
options={testOptions}
bind:value={formValues.isTest}
/>
</div>
</FormColumn>
</FormColumns>
{:else if currentStep === 2}
@@ -1318,22 +1363,6 @@
</button>
{/if}
</div>
<TextFieldSelect
id="sortField"
bind:value={formValues.sortField}
required
toolTipText="Choose which recipient field determines the delivery order"
options={Array.from(sortField.keys())}>Delivery sort by</TextFieldSelect
>
<TextFieldSelect
id="sortOrder"
bind:value={formValues.sortOrder}
toolTipText="Choose how recipients will be ordered for delivery"
required
options={Array.from(sortOrder.keys())}>Delivery sort order</TextFieldSelect
>
{:else if scheduleType === 'schedule'}
<p class="font-semibold text-slate-600 py-4">Delivery start and end</p>
<div class="flex">
@@ -1363,22 +1392,6 @@
>
</div>
<TextFieldSelect
id="sortField"
bind:value={formValues.sortField}
required
toolTipText="Choose which recipient field determines the delivery order"
options={Array.from(sortField.keys())}>Delivery by</TextFieldSelect
>
<TextFieldSelect
id="sortOrder"
bind:value={formValues.sortOrder}
toolTipText="Choose how recipients will be ordered for delivery"
required
options={Array.from(sortOrder.keys())}>Delivery order</TextFieldSelect
>
<div class="mt-4">
<p class="font-semibold text-slate-600 py-2">Delivery days</p>
<div class="grid grid-cols-4 gap-2">
@@ -1456,29 +1469,59 @@
{/if}
<div class="mt-6">
<DateTimeField
bind:value={formValues.closeAt}
min={formValues.sendEndAt
? new Date(formValues.sendEndAt)
: formValues.sendStartAt
? new Date(formValues.sendStartAt)
: new Date()}
optional
toolTipText="After this time, no more events are saved."
>Close Campaign</DateTimeField
>
{#if !showAdvancedOptionsStep3}
<div class="mt-4">
<button
type="button"
class="text-cta-blue hover:text-blue-700 dark:text-white dark:hover:text-gray-200 text-sm transition-colors duration-200 underline"
on:click={() => (showAdvancedOptionsStep3 = true)}
>
Show advanced options
</button>
</div>
{/if}
<DateTimeField
bind:value={formValues.anonymizeAt}
min={formValues.closeAt
? new Date(formValues.closeAt)
: formValues.sendEndAt
{#if showAdvancedOptionsStep3}
<TextFieldSelect
id="sortField"
bind:value={formValues.sortField}
required
toolTipText="Choose which recipient field determines the delivery order"
options={Array.from(sortField.keys())}>Delivery sort by</TextFieldSelect
>
<TextFieldSelect
id="sortOrder"
bind:value={formValues.sortOrder}
toolTipText="Choose how recipients will be ordered for delivery"
required
options={Array.from(sortOrder.keys())}>Delivery order</TextFieldSelect
>
<DateTimeField
bind:value={formValues.closeAt}
min={formValues.sendEndAt
? new Date(formValues.sendEndAt)
: new Date()}
optional
toolTipText="When reached, the campaign will close and a"
>Anonymize Data</DateTimeField
>
: formValues.sendStartAt
? new Date(formValues.sendStartAt)
: new Date()}
optional
toolTipText="After this time, no more events are saved."
>Close Campaign</DateTimeField
>
<DateTimeField
bind:value={formValues.anonymizeAt}
min={formValues.closeAt
? new Date(formValues.closeAt)
: formValues.sendEndAt
? new Date(formValues.sendEndAt)
: new Date()}
optional
toolTipText="When reached, the campaign will close and a"
>Anonymize Data</DateTimeField
>
{/if}
</div>
</div>
</FormColumn>
@@ -1486,6 +1529,16 @@
{:else if currentStep === 4}
<FormColumns id={'step-4'}>
<FormColumn>
<div class="mb-6">
<SelectSquare
label="Type"
width="small"
toolTipText={'Tests are not included in statistics'}
options={testOptions}
bind:value={formValues.isTest}
/>
</div>
<div class="mb-6">
<SelectSquare
optional
@@ -1496,36 +1549,50 @@
/>
</div>
<div class="mb-6">
<TextFieldSelect
id="webhook"
bind:value={formValues.webhookValue}
optional
options={Array.from(webhookMap.values())}>Webhook</TextFieldSelect
>
</div>
{#if !showAdvancedOptionsStep4}
<div class="mt-4">
<button
type="button"
class="text-cta-blue hover:text-blue-700 dark:text-white dark:hover:text-gray-200 text-sm transition-colors duration-200 underline"
on:click={() => (showAdvancedOptionsStep4 = true)}
>
Show advanced options
</button>
</div>
{/if}
<div class="mb-6">
<SelectSquare
optional
label="Security Configuration"
options={[
{ value: false, label: 'Disabled' },
{ value: true, label: 'Enabled' }
]}
bind:value={showSecurityOptions}
onChange={() => {
if (!showSecurityOptions) {
formValues.denyPageValue = '';
formValues.evasionPageValue = '';
allowDenyType = 'none';
formValues.allowDeny = [];
}
}}
/>
</div>
{#if showAdvancedOptionsStep4}
<div class="mb-6">
<TextFieldSelect
id="webhook"
bind:value={formValues.webhookValue}
optional
options={Array.from(webhookMap.values())}>Webhook</TextFieldSelect
>
</div>
{#if showSecurityOptions}
<div class="mb-6">
<SelectSquare
optional
label="Security Configuration"
options={[
{ value: false, label: 'Disabled' },
{ value: true, label: 'Enabled' }
]}
bind:value={showSecurityOptions}
onChange={() => {
if (!showSecurityOptions) {
formValues.denyPageValue = '';
formValues.evasionPageValue = '';
allowDenyType = 'none';
formValues.allowDeny = [];
}
}}
/>
</div>
{/if}
{#if showAdvancedOptionsStep4 && showSecurityOptions}
<div class="mb-6">
<TextFieldSelect
id="deny-page"
@@ -1901,10 +1968,15 @@
{#if currentStep < 5}
<button
type="button"
class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={isValidatingName}
on:click={nextStep}
>
Next
{#if isValidatingName}
Checking...
{:else}
Next
{/if}
<svg
class="ml-2 h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
+110
View File
@@ -63,6 +63,11 @@
name: null
};
let isIPAllowListModalVisible = false;
let ipAllowListEntries = [];
let selectedProxyForIPList = null;
let isLoadingIPAllowList = false;
const currentExample = `version: "0.0"
proxy: "My Proxy Campaign"
@@ -366,6 +371,55 @@ portal.example.com:
formValues.startURL = proxyItem.startURL;
formValues.proxyConfig = proxyItem.proxyConfig;
};
const openIPAllowListModal = async (proxy) => {
selectedProxyForIPList = proxy;
isLoadingIPAllowList = true;
isIPAllowListModalVisible = true;
console.log('Opening IP allow list for proxy:', proxy.id);
try {
const res = await api.ipAllowList.getForProxyConfig(proxy.id);
console.log('API response:', res);
if (res.success) {
ipAllowListEntries = res.data || [];
} else {
console.error('API error:', res.error);
addToast(`Failed to load IP allow list: ${res.error}`, 'Error');
ipAllowListEntries = [];
}
} catch (e) {
console.error('Network error:', e);
addToast('Failed to load IP allow list', 'Error');
ipAllowListEntries = [];
} finally {
isLoadingIPAllowList = false;
}
};
const closeIPAllowListModal = () => {
isIPAllowListModalVisible = false;
selectedProxyForIPList = null;
ipAllowListEntries = [];
};
const clearIPAllowList = async () => {
if (!selectedProxyForIPList) return;
try {
const res = await api.ipAllowList.clearForProxyConfig(selectedProxyForIPList.id);
if (res.success) {
addToast('IP allow list cleared', 'Success');
ipAllowListEntries = [];
} else {
addToast('Failed to clear IP allow list', 'Error');
}
} catch (e) {
addToast('Failed to clear IP allow list', 'Error');
console.error('failed to clear IP allow list', e);
}
};
</script>
<HeadTitle title="Proxies" />
@@ -427,6 +481,13 @@ portal.example.com:
{...globalButtonDisabledAttributes(proxy, contextCompanyID)}
/>
<TableCopyButton title={'Copy'} on:click={() => openCopyModal(proxy.id)} />
<button
class="w-full px py-1 text-slate-600 dark:text-gray-200 hover:bg-highlight-blue dark:hover:bg-highlight-blue/50 hover:text-white cursor-pointer text-left transition-colors duration-200"
on:click={() => openIPAllowListModal(proxy)}
title="View IP Allow List"
>
<p class="ml-2 text-left">View IP Allow List</p>
</button>
<TableDeleteButton
on:click={() => openDeleteAlert(proxy)}
{...globalButtonDisabledAttributes(proxy, contextCompanyID)}
@@ -509,4 +570,53 @@ portal.example.com:
onClick={() => onClickDelete(deleteValues.id)}
bind:isVisible={isDeleteAlertVisible}
></DeleteAlert>
<!-- IP Allow List Modal -->
<Modal
headerText={`IP Allow List - ${selectedProxyForIPList?.name || ''}`}
visible={isIPAllowListModalVisible}
onClose={closeIPAllowListModal}
isSubmitting={false}
>
<FormGrid>
<div class="col-span-3 w-full overflow-y-auto px-6 py-4 space-y-6">
<div class="flex justify-between items-center">
{#if !isLoadingIPAllowList && ipAllowListEntries && ipAllowListEntries.length > 0}
<BigButton on:click={clearIPAllowList}>Clear All</BigButton>
{/if}
</div>
{#if isLoadingIPAllowList}
<div class="flex items-center justify-center py-8">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span class="ml-2 text-gray-600 dark:text-gray-400">Loading...</span>
</div>
{:else if !ipAllowListEntries || ipAllowListEntries.length === 0}
<div class="text-center py-8 text-gray-500 dark:text-gray-400">
No IP addresses are allow listed
</div>
{:else}
<Table
columns={[
{ column: 'IP Address', size: 'medium' },
{ column: 'Added At', size: 'medium' },
{ column: 'Expires At', size: 'medium' }
]}
hasData={ipAllowListEntries.length > 0}
plural="entries"
>
{#each ipAllowListEntries as entry}
<TableRow>
<TableCell>{entry.ip}</TableCell>
<TableCell>{new Date(entry.createdAt).toLocaleString()}</TableCell>
<TableCell>{new Date(entry.expiresAt).toLocaleString()}</TableCell>
<TableCellEmpty />
<TableCellEmpty />
</TableRow>
{/each}
</Table>
{/if}
</div>
</FormGrid>
</Modal>
</main>