mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-08-13 14:10:19 +02:00
add company settings link to settings if in company view
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { displayMode, DISPLAY_MODE } from '$lib/store/displayMode';
|
||||
import { AppStateService } from '$lib/service/appState';
|
||||
import HeadTitle from '$lib/components/HeadTitle.svelte';
|
||||
import Headline from '$lib/components/Headline.svelte';
|
||||
import General from './panels/General.svelte';
|
||||
import Access from './panels/Access.svelte';
|
||||
import Scim from './panels/Scim.svelte';
|
||||
import Data from './panels/Data.svelte';
|
||||
import Reports from './panels/Reports.svelte';
|
||||
import RedTeam from './panels/RedTeam.svelte';
|
||||
@@ -14,6 +16,7 @@
|
||||
$: tabs = [
|
||||
{ id: 'general', label: 'General', component: General },
|
||||
{ id: 'access', label: 'Access', component: Access },
|
||||
{ id: 'scim', label: 'SCIM', component: Scim },
|
||||
{ id: 'data', label: 'Data', component: Data },
|
||||
{ id: 'reports', label: 'Reports', component: Reports },
|
||||
...($displayMode === DISPLAY_MODE.BLACKBOX
|
||||
@@ -26,11 +29,26 @@
|
||||
|
||||
$: ActiveComponent = (tabs.find((t) => t.id === active) || tabs[0]).component;
|
||||
|
||||
// company context: when viewing as a company, link to that company's settings
|
||||
const appState = AppStateService.instance;
|
||||
let companyContext = { isCompany: false, companyID: null };
|
||||
let unsubAppState;
|
||||
|
||||
onMount(() => {
|
||||
const hash = window.location.hash.replace('#', '');
|
||||
if (tabs.some((t) => t.id === hash)) {
|
||||
active = hash;
|
||||
}
|
||||
unsubAppState = appState.subscribe((s) => {
|
||||
companyContext = {
|
||||
isCompany: s.context.current === AppStateService.CONTEXT.COMPANY && !!s.context.companyID,
|
||||
companyID: s.context.companyID
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (unsubAppState) unsubAppState();
|
||||
});
|
||||
|
||||
const selectTab = (id) => {
|
||||
@@ -44,18 +62,42 @@
|
||||
<Headline>Settings</Headline>
|
||||
|
||||
<nav class="mt-4 mb-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex">
|
||||
{#each tabs as tab}
|
||||
<button
|
||||
on:click={() => selectTab(tab.id)}
|
||||
class="px-6 py-3 text-sm font-medium border-b-2 transition-colors
|
||||
{active === tab.id
|
||||
? 'border-cta-blue dark:border-highlight-blue text-cta-blue dark:text-highlight-blue'
|
||||
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'}"
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex">
|
||||
{#each tabs as tab}
|
||||
<button
|
||||
on:click={() => selectTab(tab.id)}
|
||||
class="px-6 py-3 text-sm font-medium border-b-2 transition-colors
|
||||
{active === tab.id
|
||||
? 'border-cta-blue dark:border-highlight-blue text-cta-blue dark:text-highlight-blue'
|
||||
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'}"
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{#if companyContext.isCompany}
|
||||
<a
|
||||
href="/company/{companyContext.companyID}"
|
||||
class="flex items-center gap-1.5 px-4 py-3 text-sm font-medium text-cta-blue dark:text-highlight-blue hover:opacity-80 transition-opacity whitespace-nowrap"
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
{/each}
|
||||
Company settings
|
||||
<svg
|
||||
class="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
import FormFooter from '$lib/components/FormFooter.svelte';
|
||||
import TextField from '$lib/components/TextField.svelte';
|
||||
import PasswordField from '$lib/components/PasswordField.svelte';
|
||||
import TextFieldSelect from '$lib/components/TextFieldSelect.svelte';
|
||||
import Form from '$lib/components/Form.svelte';
|
||||
|
||||
let loaded = false;
|
||||
let ssoForm = null;
|
||||
@@ -31,94 +29,17 @@
|
||||
clientSecret: null
|
||||
};
|
||||
|
||||
// SCIM provisioning: single global domain that serves the SCIM endpoints
|
||||
let scimDomain = '';
|
||||
let scimDomainOptions = [{ value: '', label: '— Disabled —' }];
|
||||
// retention window (days) before a SCIM-disabled recipient is pruned
|
||||
let scimRetentionDays = 30;
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await refreshSSO();
|
||||
if (!ssoSettingsFormValues.redirectURL) {
|
||||
ssoSettingsFormValues.redirectURL = `${location.origin}/api/v1/sso/entra-id/auth`;
|
||||
}
|
||||
await refreshScimDomain();
|
||||
await refreshScimRetention();
|
||||
} finally {
|
||||
loaded = true;
|
||||
}
|
||||
});
|
||||
|
||||
async function refreshScimRetention() {
|
||||
try {
|
||||
const res = await api.option.getScimRetentionDays();
|
||||
if (res.success) {
|
||||
scimRetentionDays = res.data.days;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('failed to load SCIM retention setting', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function setScimRetention() {
|
||||
try {
|
||||
const days = parseInt(scimRetentionDays, 10);
|
||||
if (isNaN(days) || days < 0) {
|
||||
addToast('Retention days must be zero or positive', 'Error');
|
||||
await refreshScimRetention();
|
||||
return;
|
||||
}
|
||||
const res = await api.option.setScimRetentionDays(days);
|
||||
if (res.success) {
|
||||
addToast('SCIM retention updated', 'Success');
|
||||
} else {
|
||||
addToast(res.error || 'Failed to update SCIM retention', 'Error');
|
||||
await refreshScimRetention();
|
||||
}
|
||||
} catch (e) {
|
||||
addToast('Failed to update SCIM retention', 'Error');
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshScimDomain() {
|
||||
try {
|
||||
// only normal global domains may serve SCIM — exclude AiTM proxy domains
|
||||
const [current, domains] = await Promise.all([
|
||||
api.option.getScimDomain(),
|
||||
api.domain.getAllSubsetWithoutProxies({ perPage: 1000 }, null)
|
||||
]);
|
||||
if (current.success) {
|
||||
scimDomain = current.data.domain || '';
|
||||
}
|
||||
if (domains.success) {
|
||||
const names = (domains.data.rows || []).map((d) => d.name);
|
||||
scimDomainOptions = [
|
||||
{ value: '', label: '— Disabled —' },
|
||||
...names.map((n) => ({ value: n, label: n }))
|
||||
];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('failed to load SCIM domain settings', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function setScimDomain() {
|
||||
try {
|
||||
const res = await api.option.setScimDomain(scimDomain);
|
||||
if (res.success) {
|
||||
addToast(scimDomain ? 'SCIM domain updated' : 'SCIM serving disabled', 'Success');
|
||||
} else {
|
||||
addToast(res.error || 'Failed to update SCIM domain', 'Error');
|
||||
await refreshScimDomain();
|
||||
}
|
||||
} catch (e) {
|
||||
addToast('Failed to update SCIM domain', 'Error');
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshSSO() {
|
||||
try {
|
||||
const res = await api.option.get('sso_login');
|
||||
@@ -221,34 +142,6 @@
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</SettingsCard>
|
||||
|
||||
<SettingsCard title="SCIM Provisioning">
|
||||
<p class="text-gray-600 dark:text-gray-300 text-sm transition-colors duration-200">
|
||||
Global domain that serves SCIM provisioning. Must be publicly reachable on 443 with a valid
|
||||
certificate; prefer a dedicated domain not used for campaigns.
|
||||
</p>
|
||||
<Form>
|
||||
<TextFieldSelect
|
||||
id="scimDomain"
|
||||
bind:value={scimDomain}
|
||||
onSelect={setScimDomain}
|
||||
options={scimDomainOptions}>SCIM domain</TextFieldSelect
|
||||
>
|
||||
<TextField
|
||||
id="scimRetentionDays"
|
||||
type="number"
|
||||
min="0"
|
||||
width="small"
|
||||
bind:value={scimRetentionDays}
|
||||
onBlur={setScimRetention}
|
||||
toolTipText="Days a disabled recipient is kept before removal. 0 removes on the next prune."
|
||||
>Retention (days)</TextField
|
||||
>
|
||||
</Form>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-300 text-sm transition-colors duration-200">
|
||||
How long deprovisioned recipients are kept (disabled) before being permanently removed.
|
||||
</p>
|
||||
</SettingsCard>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { api } from '$lib/api/apiProxy.js';
|
||||
import { addToast } from '$lib/store/toast';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import SettingsLoading from '$lib/components/SettingsLoading.svelte';
|
||||
import Form from '$lib/components/Form.svelte';
|
||||
import TextField from '$lib/components/TextField.svelte';
|
||||
import TextFieldSelect from '$lib/components/TextFieldSelect.svelte';
|
||||
|
||||
let loaded = false;
|
||||
|
||||
// SCIM provisioning: single global domain that serves the SCIM endpoints
|
||||
let scimDomain = '';
|
||||
let scimDomainOptions = [{ value: '', label: '— Disabled —' }];
|
||||
// retention window (days) before a SCIM-disabled recipient is pruned
|
||||
let scimRetentionDays = 30;
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await refreshScimDomain();
|
||||
await refreshScimRetention();
|
||||
} finally {
|
||||
loaded = true;
|
||||
}
|
||||
});
|
||||
|
||||
async function refreshScimRetention() {
|
||||
try {
|
||||
const res = await api.option.getScimRetentionDays();
|
||||
if (res.success) {
|
||||
scimRetentionDays = res.data.days;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('failed to load SCIM retention setting', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function setScimRetention() {
|
||||
try {
|
||||
const days = parseInt(scimRetentionDays, 10);
|
||||
if (isNaN(days) || days < 0) {
|
||||
addToast('Retention days must be zero or positive', 'Error');
|
||||
await refreshScimRetention();
|
||||
return;
|
||||
}
|
||||
const res = await api.option.setScimRetentionDays(days);
|
||||
if (res.success) {
|
||||
addToast('SCIM retention updated', 'Success');
|
||||
} else {
|
||||
addToast(res.error || 'Failed to update SCIM retention', 'Error');
|
||||
await refreshScimRetention();
|
||||
}
|
||||
} catch (e) {
|
||||
addToast('Failed to update SCIM retention', 'Error');
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshScimDomain() {
|
||||
try {
|
||||
// only normal global domains may serve SCIM — exclude AiTM proxy domains
|
||||
const [current, domains] = await Promise.all([
|
||||
api.option.getScimDomain(),
|
||||
api.domain.getAllSubsetWithoutProxies({ perPage: 1000 }, null)
|
||||
]);
|
||||
if (current.success) {
|
||||
scimDomain = current.data.domain || '';
|
||||
}
|
||||
if (domains.success) {
|
||||
const names = (domains.data.rows || []).map((d) => d.name);
|
||||
scimDomainOptions = [
|
||||
{ value: '', label: '— Disabled —' },
|
||||
...names.map((n) => ({ value: n, label: n }))
|
||||
];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('failed to load SCIM domain settings', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function setScimDomain() {
|
||||
try {
|
||||
const res = await api.option.setScimDomain(scimDomain);
|
||||
if (res.success) {
|
||||
addToast(scimDomain ? 'SCIM domain updated' : 'SCIM serving disabled', 'Success');
|
||||
} else {
|
||||
addToast(res.error || 'Failed to update SCIM domain', 'Error');
|
||||
await refreshScimDomain();
|
||||
}
|
||||
} catch (e) {
|
||||
addToast('Failed to update SCIM domain', 'Error');
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !loaded}
|
||||
<SettingsLoading />
|
||||
{:else}
|
||||
<div class="flex flex-wrap gap-6">
|
||||
<SettingsCard title="SCIM Provisioning">
|
||||
<p class="text-gray-600 dark:text-gray-300 text-sm transition-colors duration-200">
|
||||
Global domain that serves SCIM provisioning. Must be publicly reachable on 443 with a valid
|
||||
certificate; prefer a dedicated domain not used for campaigns.
|
||||
</p>
|
||||
<Form>
|
||||
<TextFieldSelect
|
||||
id="scimDomain"
|
||||
bind:value={scimDomain}
|
||||
onSelect={setScimDomain}
|
||||
options={scimDomainOptions}>SCIM domain</TextFieldSelect
|
||||
>
|
||||
<TextField
|
||||
id="scimRetentionDays"
|
||||
type="number"
|
||||
min="0"
|
||||
width="small"
|
||||
bind:value={scimRetentionDays}
|
||||
onBlur={setScimRetention}
|
||||
toolTipText="Days a disabled recipient is kept before removal. 0 removes on the next prune."
|
||||
>Retention (days)</TextField
|
||||
>
|
||||
</Form>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-300 text-sm transition-colors duration-200">
|
||||
How long deprovisioned recipients are kept (disabled) before being permanently removed.
|
||||
</p>
|
||||
</SettingsCard>
|
||||
</div>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user