mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-07-07 12:57:54 +02:00
add campaign company resource handling
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
@@ -1,25 +1,44 @@
|
||||
<script>
|
||||
import { AppStateService } from '$lib/service/appState';
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { showIsLoading } from '$lib/store/loading';
|
||||
import { resourceContext } from '$lib/store/resourceContext';
|
||||
|
||||
let context = {
|
||||
current: '',
|
||||
companyName: ''
|
||||
companyName: '',
|
||||
companyID: null
|
||||
};
|
||||
|
||||
let resource = {
|
||||
resourceType: null,
|
||||
resourceCompanyID: null,
|
||||
resourceCompanyName: null,
|
||||
isActive: false
|
||||
};
|
||||
|
||||
const appState = AppStateService.instance;
|
||||
|
||||
let unsubAppState;
|
||||
let unsubResource;
|
||||
|
||||
onMount(() => {
|
||||
const unsub = appState.subscribe((s) => {
|
||||
unsubAppState = appState.subscribe((s) => {
|
||||
context = {
|
||||
current: s.context.current,
|
||||
companyName: s.context.companyName
|
||||
companyName: s.context.companyName,
|
||||
companyID: s.context.companyID
|
||||
};
|
||||
});
|
||||
return () => {
|
||||
unsub();
|
||||
};
|
||||
|
||||
unsubResource = resourceContext.subscribe((r) => {
|
||||
resource = { ...r };
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (unsubAppState) unsubAppState();
|
||||
if (unsubResource) unsubResource();
|
||||
});
|
||||
|
||||
// exit to global context
|
||||
@@ -30,43 +49,125 @@
|
||||
location.reload();
|
||||
}
|
||||
|
||||
// switch to resource's context
|
||||
function switchToResourceContext() {
|
||||
showIsLoading();
|
||||
if (resource.resourceCompanyID) {
|
||||
// switch to company context
|
||||
appState.setCompanyContext(resource.resourceCompanyID, resource.resourceCompanyName);
|
||||
localStorage.setItem(
|
||||
'context',
|
||||
JSON.stringify({ id: resource.resourceCompanyID, name: resource.resourceCompanyName })
|
||||
);
|
||||
} else {
|
||||
// switch to global context
|
||||
appState.clearContext();
|
||||
localStorage.setItem('context', '');
|
||||
}
|
||||
location.reload();
|
||||
}
|
||||
|
||||
$: isCompanyView = context.current === AppStateService.CONTEXT.COMPANY && context.companyName;
|
||||
$: isResourceActive = resource.isActive;
|
||||
$: isResourceGlobal = isResourceActive && !resource.resourceCompanyID;
|
||||
$: isResourceInDifferentCompany =
|
||||
isResourceActive &&
|
||||
resource.resourceCompanyID &&
|
||||
context.companyID !== resource.resourceCompanyID;
|
||||
$: hasContextMismatch =
|
||||
isResourceActive &&
|
||||
((isCompanyView && isResourceGlobal) ||
|
||||
(!isCompanyView && resource.resourceCompanyID) ||
|
||||
isResourceInDifferentCompany);
|
||||
|
||||
// determine banner style and content
|
||||
$: bannerStyle = 'bg-active-blue dark:bg-active-blue';
|
||||
</script>
|
||||
|
||||
{#if isCompanyView}
|
||||
{#if isCompanyView || hasContextMismatch}
|
||||
<!-- top banner -->
|
||||
<div class="w-full h-9 bg-active-blue dark:bg-active-blue z-30 company-banner">
|
||||
<div class="w-full h-9 {bannerStyle} z-30 company-banner">
|
||||
<div class="h-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-center gap-4 h-full">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-white/70 font-medium text-sm"> Viewing as </span>
|
||||
<span class="text-white font-semibold text-sm">
|
||||
{context.companyName}
|
||||
</span>
|
||||
</div>
|
||||
{#if hasContextMismatch}
|
||||
<!-- context mismatch view -->
|
||||
<div class="flex items-center space-x-2">
|
||||
{#if isCompanyView && isResourceGlobal}
|
||||
<span class="text-white/70 font-medium text-sm">Viewing as</span>
|
||||
<span class="text-white font-semibold text-sm">{context.companyName}</span>
|
||||
<span class="text-white/70 font-medium text-sm">•</span>
|
||||
<span class="text-white/90 font-medium text-sm">
|
||||
This {resource.resourceType || 'resource'} is
|
||||
<strong class="font-bold">global</strong>
|
||||
</span>
|
||||
{:else if !isCompanyView && resource.resourceCompanyID}
|
||||
<span class="text-white/90 font-medium text-sm">
|
||||
This {resource.resourceType || 'resource'} belongs to
|
||||
<strong class="font-bold">{resource.resourceCompanyName || 'a company'}</strong>
|
||||
</span>
|
||||
{:else if isResourceInDifferentCompany}
|
||||
<span class="text-white/70 font-medium text-sm">Viewing as</span>
|
||||
<span class="text-white font-semibold text-sm">{context.companyName}</span>
|
||||
<span class="text-white/70 font-medium text-sm">•</span>
|
||||
<span class="text-white/90 font-medium text-sm">
|
||||
This {resource.resourceType || 'resource'} belongs to
|
||||
<strong class="font-bold"
|
||||
>{resource.resourceCompanyName || 'another company'}</strong
|
||||
>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<!-- exit button -->
|
||||
<button
|
||||
on:click={exitCompanyView}
|
||||
class="flex items-center gap-1 px-2 py-0.5 text-white/50 hover:text-white/80 text-xs transition-colors duration-200"
|
||||
title="Exit company view"
|
||||
>
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- switch button -->
|
||||
<button
|
||||
on:click={switchToResourceContext}
|
||||
class="flex items-center gap-1.5 px-3 py-1 bg-white/20 hover:bg-white/30 text-white rounded text-xs font-semibold transition-colors duration-200"
|
||||
title="Switch to {isResourceGlobal
|
||||
? 'global'
|
||||
: resource.resourceCompanyName || 'company'} context"
|
||||
>
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"
|
||||
></path>
|
||||
</svg>
|
||||
Switch
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- normal company view -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-white/70 font-medium text-sm">Viewing as</span>
|
||||
<span class="text-white font-semibold text-sm">
|
||||
{context.companyName}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- exit button -->
|
||||
<button
|
||||
on:click={exitCompanyView}
|
||||
class="flex items-center gap-1 px-2 py-0.5 text-white/50 hover:text-white/80 text-xs transition-colors duration-200"
|
||||
title="Exit company view"
|
||||
>
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- border frame around entire viewport when in company view -->
|
||||
{#if isCompanyView}
|
||||
<!-- border frame around entire viewport when in company view or context mismatch -->
|
||||
{#if isCompanyView || hasContextMismatch}
|
||||
<div class="company-view-frame"></div>
|
||||
{/if}
|
||||
|
||||
@@ -81,7 +182,7 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border: 3px solid #1e3fa8;
|
||||
border: 3px solid;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
/**
|
||||
* store for tracking the current resource being viewed
|
||||
* this allows the company banner to show context mismatch information
|
||||
*/
|
||||
function createResourceContextStore() {
|
||||
const { subscribe, set, update } = writable({
|
||||
resourceType: null, // e.g., 'campaign', 'template', 'email'
|
||||
resourceCompanyID: null,
|
||||
resourceCompanyName: null,
|
||||
isActive: false
|
||||
});
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
/**
|
||||
* set the current resource context
|
||||
* @param {string} type - resource type (e.g., 'campaign', 'template')
|
||||
* @param {string|null} companyID - company id the resource belongs to (null for global)
|
||||
* @param {string|null} companyName - company name (null for global)
|
||||
*/
|
||||
setResource: (type, companyID, companyName) => {
|
||||
set({
|
||||
resourceType: type,
|
||||
resourceCompanyID: companyID,
|
||||
resourceCompanyName: companyName,
|
||||
isActive: true
|
||||
});
|
||||
},
|
||||
/**
|
||||
* clear the resource context (call when leaving a resource page)
|
||||
*/
|
||||
clear: () => {
|
||||
set({
|
||||
resourceType: null,
|
||||
resourceCompanyID: null,
|
||||
resourceCompanyName: null,
|
||||
isActive: false
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const resourceContext = createResourceContextStore();
|
||||
@@ -37,6 +37,7 @@
|
||||
import CopyCell from '$lib/components/table/CopyCell.svelte';
|
||||
import TextFieldSelectWithType from '$lib/components/form/TextFieldSelectWithType.svelte';
|
||||
import ConditionalDisplay from '$lib/components/ConditionalDisplay.svelte';
|
||||
import { resourceContext } from '$lib/store/resourceContext';
|
||||
|
||||
// services
|
||||
const appStateService = AppStateService.instance;
|
||||
@@ -135,8 +136,11 @@
|
||||
}
|
||||
hideIsLoading();
|
||||
})();
|
||||
|
||||
// cleanup resource context when leaving page
|
||||
return () => {
|
||||
tableURLParams.unsubscribe();
|
||||
resourceContext.clear();
|
||||
};
|
||||
});
|
||||
|
||||
@@ -403,14 +407,13 @@
|
||||
|
||||
const closeModal = () => {
|
||||
isModalVisible = false;
|
||||
form?.reset();
|
||||
formValues = {
|
||||
id: null,
|
||||
templateType: 'Email',
|
||||
name: null,
|
||||
domain: null,
|
||||
landingPage: null,
|
||||
landingPageType: 'page',
|
||||
landingPageType: 'page', // 'page' or 'proxy'
|
||||
beforeLandingPage: null,
|
||||
afterLandingPage: null,
|
||||
afterLandingPageRedirectURL: '',
|
||||
@@ -421,6 +424,10 @@
|
||||
stateIdentifier: 'session',
|
||||
urlPath: ''
|
||||
};
|
||||
modalError = '';
|
||||
showAdvancedOptions = false;
|
||||
// clear resource context when closing modal
|
||||
resourceContext.clear();
|
||||
};
|
||||
|
||||
/** @param {string} id */
|
||||
@@ -432,10 +439,26 @@
|
||||
const r = globalButtonDisabledAttributes(template, contextCompanyID);
|
||||
if (r.disabled) {
|
||||
hideIsLoading();
|
||||
resourceContext.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
assignTemplate(template);
|
||||
|
||||
// if company exists but name is missing, fetch it
|
||||
if (template.companyID && !template.company?.name) {
|
||||
try {
|
||||
const companyRes = await api.company.getByID(template.companyID);
|
||||
if (companyRes.success && companyRes.data) {
|
||||
template.company = companyRes.data;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('failed to load company details', e);
|
||||
}
|
||||
}
|
||||
|
||||
// set resource context for banner
|
||||
resourceContext.setResource('template', template.companyID, template.company?.name);
|
||||
isModalVisible = true;
|
||||
} catch (e) {
|
||||
addToast('Failed to load campaign template', 'Error');
|
||||
@@ -452,6 +475,21 @@
|
||||
const template = await getTemplate(id);
|
||||
assignTemplate(template);
|
||||
formValues.id = null;
|
||||
|
||||
// if company exists but name is missing, fetch it
|
||||
if (template.companyID && !template.company?.name) {
|
||||
try {
|
||||
const companyRes = await api.company.getByID(template.companyID);
|
||||
if (companyRes.success && companyRes.data) {
|
||||
template.company = companyRes.data;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('failed to load company details', e);
|
||||
}
|
||||
}
|
||||
|
||||
// set resource context for banner
|
||||
resourceContext.setResource('template', template.companyID, template.company?.name);
|
||||
isModalVisible = true;
|
||||
} catch (e) {
|
||||
addToast('Failed to load campaign template', 'Error');
|
||||
@@ -513,8 +551,9 @@
|
||||
</script>
|
||||
|
||||
<HeadTitle title="Campaigns templates" />
|
||||
|
||||
<main>
|
||||
<Headline>Campaign templates</Headline>
|
||||
<Headline>Campaigns templates</Headline>
|
||||
<BigButton on:click={openCreateModal}>New template</BigButton>
|
||||
|
||||
<Table
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
import papaparse from 'papaparse';
|
||||
import FormFooter from '$lib/components/FormFooter.svelte';
|
||||
import TextFieldSelect from '$lib/components/TextFieldSelect.svelte';
|
||||
import { resourceContext } from '$lib/store/resourceContext';
|
||||
|
||||
// services
|
||||
const appStateService = AppStateService.instance;
|
||||
@@ -76,7 +77,9 @@
|
||||
recipientGroups: [],
|
||||
events: [],
|
||||
eventTypesIDToNameMap: {},
|
||||
notableEventName: ''
|
||||
notableEventName: '',
|
||||
companyID: null,
|
||||
company: null
|
||||
};
|
||||
let allowedFilter = null;
|
||||
let campaignRecipients = [];
|
||||
@@ -167,6 +170,7 @@
|
||||
if (context) {
|
||||
contextCompanyID = context.companyID;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
await refresh();
|
||||
recipientTableUrlParams.onChange(refreshRecipients);
|
||||
@@ -176,9 +180,12 @@
|
||||
await refreshRecipientsTimes();
|
||||
await refreshCampaignEventsSince();
|
||||
})();
|
||||
|
||||
// cleanup resource context when leaving page
|
||||
return () => {
|
||||
recipientTableUrlParams.unsubscribe();
|
||||
eventsTableURLParams.unsubscribe();
|
||||
resourceContext.clear();
|
||||
};
|
||||
});
|
||||
|
||||
@@ -240,6 +247,21 @@
|
||||
campaign.denyPage = t.denyPage;
|
||||
campaign.evasionPage = t.evasionPage;
|
||||
campaign.webhookID = t.webhookID;
|
||||
campaign.companyID = t.companyID;
|
||||
campaign.company = t.company;
|
||||
|
||||
// if company exists but name is missing, fetch it
|
||||
if (campaign.companyID && !campaign.company?.name) {
|
||||
try {
|
||||
const companyRes = await api.company.getByID(campaign.companyID);
|
||||
if (companyRes.success && companyRes.data) {
|
||||
campaign.company = companyRes.data;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('failed to load company details', e);
|
||||
}
|
||||
}
|
||||
|
||||
// fetch the full template object
|
||||
if (t.templateID) {
|
||||
const templateRes = await api.campaignTemplate.getByID(t.templateID, true);
|
||||
@@ -255,6 +277,9 @@
|
||||
if (campaign.allowDeny && campaign.allowDeny[0]) {
|
||||
allowedFilter = campaign.allowDeny[0].allowed;
|
||||
}
|
||||
|
||||
// set resource context for banner
|
||||
resourceContext.setResource('campaign', campaign.companyID, campaign.company?.name);
|
||||
} catch (e) {
|
||||
addToast('Failed to load campaign', 'Error');
|
||||
console.error('failed to load campaign', e);
|
||||
|
||||
Reference in New Issue
Block a user