mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-07-10 06:08:42 +02:00
change access directive and add management for proxy allow list
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user