Attempting to correct previous issues with monorepo structure

This commit is contained in:
luke-hagar-sp
2024-02-01 12:42:36 -06:00
parent 4d8a34f363
commit 0ecfb7161f
122 changed files with 23585 additions and 15321 deletions

28
Sveltekit-App/src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,28 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
// and what to do when importing types
import type { IdnSession, Session, TokenDetails } from '$lib/utils/oauth';
declare global {
namespace App {
interface Locals {
hasSession: boolean;
hasIdnSession: boolean;
session?: Session;
idnSession?: IdnSession;
tokenDetails?: TokenDetails;
}
// interface PageData {}
interface Error {
message: string;
context?: unknown;
urls?: string[];
errData?: unknown;
}
// interface Platform {}
}
}

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/logo.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>IdentityNow Admin Console</title>
%sveltekit.head%
</head>
<body data-theme="wintry">
<div style="display: contents" class="h-full overflow-hidden" id="svelte">%sveltekit.body%</div>
</body>
</html>

View File

@@ -0,0 +1,18 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind variants;
:root [data-theme='wintry'] {
--theme-rounded-base: 5px;
--theme-rounded-container: 4px;
}
html,
body {
@apply h-full overflow-hidden;
}
td {
@apply !align-middle !text-center;
}

View File

@@ -0,0 +1,20 @@
<!-- This page should only appear when an unhandled error is thrown in the +layout.server.ts file -->
<h1>Game over, man! Game over!</h1>
<strong>
No but seriously, it appears there was an unhandled issue loading the layout of the application
</strong>
<br />
<br />
<a
href="https://github.com/sailpoint-oss/idn-admin-console/issues/new/choose"
rel="noreferrer"
target="_blank"
>
Please let us know by submitting an issue on GitHub
</a>
<p>Error Code: %sveltekit.status%</p>
<p>Error Message: %sveltekit.error.message%</p>

View File

@@ -0,0 +1,48 @@
import {
checkIdnSession,
checkSession,
checkToken,
getSession,
getToken,
getTokenDetails,
lastCheckedToken
} from '$lib/utils/oauth';
import { redirect, type Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
const hasSession = checkSession(event.cookies);
const hasIdnSession = checkIdnSession(event.cookies);
event.locals.hasSession = hasSession;
event.locals.hasIdnSession = hasIdnSession;
if (hasSession) {
event.locals.session = getSession(event.cookies);
if (hasIdnSession) {
event.locals.idnSession = await getToken(event.cookies);
const lastToken = lastCheckedToken(event.cookies);
if (lastToken != '' && lastToken === event.locals.idnSession.access_token) {
event.locals.tokenDetails = getTokenDetails(event.cookies);
} else {
event.locals.tokenDetails = await checkToken(
event.locals.session.baseUrl,
event.locals.idnSession.access_token
);
event.cookies.set('tokenDetails', JSON.stringify(event.locals.tokenDetails), {
path: '/',
httpOnly: false,
secure: false
});
}
}
}
if (event.url.pathname.startsWith('/home') || event.url.pathname.startsWith('/api')) {
if (!hasSession || !hasIdnSession) {
redirect(302, '/');
}
}
const response = await resolve(event);
return response;
};

View File

@@ -0,0 +1,110 @@
<script lang="ts">
import { onMount } from 'svelte';
/**
* list of values to animate
*/
export let values: Array<string | number> = Array.from({ length: 100 }, (_, i) =>
new String(i).padStart(3, '0'),
);
/**
* counter interval between each step in milliseconds, defaults to `1000`
*/
export let interval = 1000;
/**
* counter interval for each transition in milliseconds, defaults to `700`
*/
export let transitionInterval = 700;
/**
* whether to start the counter immediately or wait for the `interval` to pass, defaults to `false`
*/
export let startImmediately = false;
/**
* counter direction, can be `up` or `down` defaults to `down`
*/
export let direction: 'up' | 'down' = 'down';
/**
* whether to loop the counter animation after reaching the end of `values` array , defaults to `true`
*/
export let loop = true;
/**
* easing function to use, defaults to `cubic-bezier(1, 0, 0, 1)`
*/
export let ease = 'cubic-bezier(1, 0, 0, 1)';
/**
* setting to allow items in values to be displayed randomly
*/
export let random = false;
/**
* optional initial value to start the counter from
*/
export let initialValue: string | number | undefined = undefined;
$: contentValues = values.join('\n\n');
$: intervalInMs = `${transitionInterval}ms`;
let index = direction === 'up' ? 0 : values.length - 1;
let lastIndex = initialValue ? values.indexOf(initialValue) : index;
onMount(() => {
// timer function
const start = () => {
index = lastIndex + (direction === 'up' ? 1 : -1);
// terminate if we looped through all values && loop is false
if (!loop && (index === values.length - 1 || index === 0)) {
clearInterval(timer);
return;
}
// ensure index is in range
if (loop && index === values.length) {
index = 0;
}
if (loop && index === -1) {
index = values.length - 1;
}
if (random) {
index = Math.floor(Math.random() * values.length);
}
lastIndex = index;
};
if (startImmediately) {
start();
}
let timer = setInterval(start, interval);
return () => clearInterval(timer);
});
</script>
<span class="sliding-text {$$props.class}">
<span style="--index: {index}; --interval: {intervalInMs}; --ease:{ease}">
<span>{contentValues}</span>
</span>
</span>
<style>
.sliding-text {
display: inline-block;
position: relative;
line-height: 1em;
height: 1em;
}
.sliding-text > span {
height: 1em;
display: inline-block;
overflow-y: hidden;
}
.sliding-text > span > span {
text-align: center;
transition: all var(--interval) var(--ease);
position: relative;
height: 100%;
white-space: pre;
top: calc(var(--index) * -2em);
}
</style>

View File

@@ -0,0 +1,13 @@
<script lang="ts">
import { CodeBlock, getModalStore } from '@skeletonlabs/skeleton';
const modalStore = getModalStore();
</script>
<div class="card p-4 w-[85%]">
<CodeBlock
lineNumbers
language={$modalStore[0]?.meta?.language || 'json'}
code={$modalStore[0]?.meta?.code || ''}
/>
</div>

View File

@@ -0,0 +1,21 @@
<script lang="ts">
import { resourcelinks } from './links';
</script>
<div class="p-4 card variant-soft-surface grow">
<h1 class="text-center">Resources</h1>
<ul class="flex flex-col">
{#each resourcelinks as link}
<li class="listbox-item">
<a
class="hover:underline text-center hover:text-tertiary-600"
target="_blank"
rel="noreferrer"
href={link.href}
>
{link.label}
</a>
</li>
{/each}
</ul>
</div>

View File

@@ -0,0 +1,52 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import Progress from '../Progress.svelte';
let summaryResp: Promise<any>;
const getStatus = async () => {
console.debug('Getting Status Summary');
summaryResp = (await fetch('https://status.sailpoint.com/api/v2/summary.json')).json();
console.debug(await summaryResp);
};
let interval: any;
onMount(async () => {
getStatus();
interval = setInterval(() => getStatus(), 30000);
});
onDestroy(() => clearInterval(interval));
function parseClass(status: string) {
switch (status) {
case 'none':
return 'text-success-500';
case 'minor':
return 'text-warning-500';
case 'major':
return 'text-error-500';
}
}
</script>
<div class="p-4 card grow overflow-hidden flex flex-col">
<h1 class="text-center">IdentityNow Status</h1>
<div class="grid place-content-center h-full">
{#await summaryResp}
<Progress width="w-12" />
{:then summary}
<a
href="https://status.sailpoint.com"
class="{parseClass(summary?.status?.indicator)} hover:underline"
rel="noreferrer"
target="_blank"
>
{summary?.status?.description}
</a>
{/await}
</div>
</div>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import { supportLinks } from './links';
</script>
<div class="p-4 card variant-soft-surface grow">
<h1 class="text-center">Support</h1>
<ul class="flex flex-col">
{#each supportLinks as link}
<li class="flex flex-row gap-1 hover:underline hover:text-tertiary-600">
<a target="_blank" rel="noreferrer" href={link.href}>
{link.label}
</a>
</li>
{/each}
</ul>
</div>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import { tenantLinks } from './links';
export let tenantUrl = 'https://placeholder.com';
</script>
<div class="p-4 card variant-soft-surface grow">
<h1 class="text-center">Tenant Links</h1>
<ul class="flex flex-col">
{#each tenantLinks as link}
<li class="listbox-item">
<a
class="hover:underline text-center hover:text-tertiary-600"
target="_blank"
rel="noreferrer"
href={tenantUrl + link.slug}
>
{link.label}
</a>
</li>
{/each}
</ul>
</div>

View File

@@ -0,0 +1,53 @@
export const tenantLinks: { label: string; slug: string }[] = [
{ label: '🔑 Grant Tenant Access', slug: '/ui/a/admin/global/grant-tenant-access' },
{
label: '🏠 Dashboard',
slug: '/ui/admin#admin:dashboard:overview'
},
{ label: '🙂 Identity Profiles', slug: '/ui/admin#admin:identities:profiles' },
{ label: '📋 Identity List', slug: '/ui/a/admin/identities/all-identities' },
{ label: '🎭 Access Profiles', slug: '/ui/a/admin/access/access-profiles/landing' },
{ label: '📦 Roles', slug: '/ui/a/admin/access/roles/landing-page' },
{ label: '🔗 Sources', slug: '/ui/a/admin/connections/sources-list/configured-sources' },
{
label: '💻 Virtual Appliances',
slug: '/ui/a/admin/connections/virtual-appliances/clusters-list'
}
];
export const resourcelinks: { label: string; href: string }[] = [
{
label: '💁 Developer Community',
href: 'https://developer.sailpoint.com/discuss/'
},
{ label: '📖 API Documentation', href: 'https://developer.sailpoint.com/idn/api/v3' },
{ label: '💻 CLI Documentation', href: 'https://developer.sailpoint.com/idn/tools/cli' },
{
label: '🔌 Connector Reference',
href: 'https://community.sailpoint.com/t5/IdentityNow-Connectors/IdentityNow-Connectors/ta-p/80019'
},
{
label: '🧮 Transform Guides',
href: 'https://community.sailpoint.com/t5/Search/bd-p/search?searchString=%22IdentityNow+Transforms+-%22'
},
{
label: '📚 Rules Documentation',
href: 'https://developer.sailpoint.com/idn/docs/rules/'
},
{
label: '🔒 User Level Access Matrix',
href: 'https://documentation.sailpoint.com/saas/help/common/users/user_level_matrix.html'
}
];
export const supportLinks: { label: string; href: string }[] = [
{
label: '🎫 Submit a ticket',
href: 'https://support.sailpoint.com/csm?id=sc_cat_item&sys_id=a78364e81bec151050bcc8866e4bcb5c&referrer=popular_items'
},
{
label: '🔭 Scope of SaaS Support',
href: 'https://community.sailpoint.com/t5/IdentityNow-Wiki/What-is-supported-by-SaaS-Support/ta-p/198779'
},
{ label: '🔖 Support Knowledge Base', href: 'https://support.sailpoint.com/' }
];

View File

@@ -0,0 +1,43 @@
<script lang="ts">
import { Paginator, type PaginationSettings } from '@skeletonlabs/skeleton';
export let totalCount: number = 0;
export let settings: PaginationSettings;
export let onPageChange: (e: CustomEvent<number>) => void;
export let onAmountChange: (e: CustomEvent<number>) => void;
export let onGo: (e: KeyboardEvent | MouseEvent) => void;
export let filters: string = '';
export let sorters: string = '';
</script>
<div class=" p-4 flex flex-row flex-wrap justify-between gap-4">
<div class="flex flex-row gap-1">
<input
on:keydown={onGo}
bind:value={filters}
class="input"
title="Filter"
type="text"
placeholder="Filter"
/>
<input
on:keydown={onGo}
bind:value={sorters}
class="input"
title="Sorter"
type="text"
placeholder="Sorter"
/>
<button on:click={onGo} class="btn variant-filled-primary !text-white"> Go </button>
</div>
<p class="my-auto">Total Count: {totalCount}</p>
<Paginator
bind:settings
on:page={onPageChange}
on:amount={onAmountChange}
showNumerals={true}
maxNumerals={1}
showFirstLastButtons={true}
showPreviousNextButtons={true}
/>
</div>

View File

@@ -0,0 +1,14 @@
<script lang="ts">
import { ProgressRadial } from '@skeletonlabs/skeleton';
export let width: string = '';
</script>
<div class="grid place-content-center {width}">
<ProgressRadial
{width}
stroke={100}
meter="stroke-primary-500"
track="stroke-primary-500/30"
class="progress-bar"
/>
</div>

View File

@@ -0,0 +1,49 @@
<svg
class={$$restProps.class || ''}
width="800px"
height="800px"
viewBox="0 0 32 32"
enable-background="new 0 0 32 32"
id="Editable-line"
version="1.1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
><line
fill="none"
id="XMLID_103_"
stroke="#000000"
stroke-linecap="round"
stroke-linejoin="round"
stroke-miterlimit="10"
stroke-width="2"
x1="7"
x2="25"
y1="16"
y2="16"
/><line
fill="none"
id="XMLID_102_"
stroke="#000000"
stroke-linecap="round"
stroke-linejoin="round"
stroke-miterlimit="10"
stroke-width="2"
x1="7"
x2="25"
y1="25"
y2="25"
/><line
fill="none"
id="XMLID_101_"
stroke="#000000"
stroke-linecap="round"
stroke-linejoin="round"
stroke-miterlimit="10"
stroke-width="2"
x1="7"
x2="25"
y1="7"
y2="7"
/></svg
>

After

Width:  |  Height:  |  Size: 852 B

View File

@@ -0,0 +1,14 @@
<svg
class="w-6 h-6 stroke-current"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
/>
</svg>

After

Width:  |  Height:  |  Size: 371 B

View File

@@ -0,0 +1,14 @@
<svg
class="w-6 h-6 stroke-current"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>

After

Width:  |  Height:  |  Size: 356 B

View File

@@ -0,0 +1,14 @@
<svg
class="w-6 h-6 stroke-current"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z"
/>
</svg>

After

Width:  |  Height:  |  Size: 318 B

View File

@@ -0,0 +1,14 @@
<svg
class="w-6 h-6 stroke-current"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M16 8v8m-4-5v5m-4-2v2m-2 4h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>

After

Width:  |  Height:  |  Size: 314 B

View File

@@ -0,0 +1,14 @@
<svg
class="w-6 h-6 stroke-current"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2"
/>
</svg>

After

Width:  |  Height:  |  Size: 387 B

View File

@@ -0,0 +1,77 @@
<script lang="ts">
import { Accordion, AccordionItem, CodeBlock } from '@skeletonlabs/skeleton';
export let cluster: { name: string; id: string } | undefined;
function formatStatusColor(status: string) {
switch (status) {
case 'HEALTHY':
return 'text-green-500';
case 'WARNING':
return 'text-red-500';
case 'FAILED':
return 'text-red-500';
default:
return 'text-gray-500';
}
}
</script>
<h2>Virtual Appliance Cluster</h2>
<p>Name: {cluster?.name || 'Empty'}</p>
<p>ID: {cluster?.id || 'Empty'}</p>
{#if cluster?.id}
{#await fetch(`/api/sailpoint/cluster/${cluster.id}`)}
<div class="py-2 placeholder" />
{:then clusterResponse}
{#await clusterResponse.json()}
<div class="py-2 placeholder" />
{:then clusterInfo}
<p>Pod: {clusterInfo.pod}</p>
<p>Description: {clusterInfo.description ? clusterInfo.description : 'Empty'}</p>
<p>CCG Version: {clusterInfo.ccgVersion}</p>
<p>
Debugging Enabled: <span
class={clusterInfo.configuration?.debug === 'true' ? 'text-green-500' : 'text-red-500'}
>
{clusterInfo.configuration.debug === 'true' ? 'True' : 'False'}
</span>
</p>
<p>
Status: <span class={formatStatusColor(clusterInfo.status)}>
{clusterInfo.status}
</span>
</p>
<p>
Alert: <span class={formatStatusColor(clusterInfo.status)}>
{clusterInfo.alertKey}
</span>
</p>
<div class="py-2">
<p class="underline">Client IDs</p>
<ul class="list">
{#each clusterInfo.clientIds as client, index}
<li>
<span>{index + 1}.</span>
<span class="flex-auto">{client}</span>
</li>
{/each}
</ul>
</div>
<Accordion>
<AccordionItem>
<svelte:fragment slot="summary">Raw Data</svelte:fragment>
<svelte:fragment slot="content">
<CodeBlock code={JSON.stringify(clusterInfo, null, 4)} language="json" />
</svelte:fragment>
</AccordionItem>
</Accordion>
{:catch error}
<p class="text-red-500">Error: {error.message}</p>
{/await}
{:catch error}
<p class="text-red-500">Error: {error.message}</p>
{/await}
{/if}

View File

@@ -0,0 +1,106 @@
import { goto } from '$app/navigation';
import type { ModalSettings, ModalStore } from '@skeletonlabs/skeleton';
export function formatDate(date: string | null | undefined) {
if (!date) return 'N/A';
return new Date(date).toLocaleString();
}
export function getLimit(url: URL) {
return url.searchParams.get('limit') || '250';
}
export function getFilters(url: URL) {
return url.searchParams.get('filters') || '';
}
export function getSorters(url: URL) {
return url.searchParams.get('sorters') || '';
}
export function getPage(url: URL) {
return url.searchParams.get('page') || '0';
}
export function getPaginationParams(url: URL) {
return {
limit: getLimit(url),
page: getPage(url),
filters: getFilters(url),
sorters: getSorters(url)
};
}
type PaginationParams = {
limit: string;
page: string;
filters: string;
sorters: string;
};
export function createOnPageChange(params: PaginationParams, path: string) {
return function onPageChange(e: CustomEvent): void {
const urlParams = new URLSearchParams();
urlParams.set('page', e.detail);
urlParams.set('limit', params.limit);
urlParams.set('sorters', params.sorters);
urlParams.set('filters', params.filters);
console.log(`${path}?${urlParams.toString()}`);
goto(`${path}?${urlParams.toString()}`);
};
}
export function createOnAmountChange(params: PaginationParams, path: string) {
return function onAmountChange(e: CustomEvent): void {
const urlParams = new URLSearchParams();
urlParams.set('page', params.page);
urlParams.set('limit', e.detail);
urlParams.set('sorters', params.sorters);
urlParams.set('filters', params.filters);
console.log(`${path}?${urlParams.toString()}`);
goto(`${path}?${urlParams.toString()}`);
};
}
export function createOnGo(params: PaginationParams, path: string) {
return function onGo(e: KeyboardEvent | MouseEvent): void {
if (e.type !== 'click' && (e as KeyboardEvent).key !== 'Enter') return;
const urlParams = new URLSearchParams();
urlParams.set('page', params.page);
urlParams.set('limit', params.limit);
urlParams.set('sorters', params.sorters);
urlParams.set('filters', params.filters);
console.log(`${path}?${urlParams.toString()}`);
goto(`${path}?${urlParams.toString()}`);
};
}
export function capitalize(s: string) {
if (typeof s !== 'string') return '';
return s.charAt(0).toUpperCase() + s.slice(1);
}
export function TriggerCodeModal(object: unknown, modalStore: ModalStore) {
const modal: ModalSettings = {
type: 'component',
component: 'codeBlockModal',
meta: {
code: JSON.stringify(object, null, 4),
language: 'json'
}
};
modalStore.trigger(modal);
}
export function parseInitials(name: string) {
const initials = name.match(/\b(\w)/g) || ['A', 'U'];
return initials.join('');
}

View File

@@ -0,0 +1,30 @@
export const reports = [
{
url: '/home/reports/source-account-create-error',
name: 'Source Account Create Error',
description:
'This report will show all source accounts for which there is a create error associated with the source'
},
{
url: '/home/reports/inactive-identities-with-access',
name: 'Inactive Identities With Access',
description:
'This report will show all identities that are inactive but still have access in sources'
},
{
url: '/home/reports/missing-cloud-life-cycle-state',
name: 'Missing Cloud Life Cycle State',
description: 'This report will show all identities that are missing a cloud life cycle state'
},
{
url: '/home/reports/source-owner-configured',
name: 'Source Owner Configured',
description: 'This report will show all sources and their configured owners'
},
{
url: '/home/reports/source-aggregations',
name: 'Source Aggregations',
description: 'This report will show all sources and their most recent aggregation events'
}
];

View File

@@ -0,0 +1,6 @@
import { Configuration } from 'sailpoint-api-client';
export function createConfiguration(baseUrl: string, token: string) {
const apiConfig = new Configuration({ baseurl: baseUrl, accessToken: token });
return apiConfig;
}

View File

@@ -0,0 +1,29 @@
<script lang="ts">
import { navigation } from './navigation';
import { page } from '$app/stores';
</script>
<div class="{$$props.class ?? ''} w-[144px] overflow-hidden bg-surface-50-900-token h-full">
<div class="flex flex-col w-36 items-center h-full overflow-hidden">
<div class="w-full px-2">
<div class="flex flex-col items-center w-full mt-3 border-surface-400-500-token">
{#each navigation as section}
{#each section.content as link (link.url)}
<a
href={link.url}
data-sveltekit-preload-data="hover"
class="flex items-center w-full h-12 px-3 mt-2 rounded"
class:bg-surface-active-token={link.url === $page.url.pathname}
class:!text-white={link.url === $page.url.pathname}
>
{#if link.icon}
<svelte:component this={link.icon} />
{/if}
<p class="ml-2 text-sm font-medium">{link.name}</p>
</a>
{/each}
{/each}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,15 @@
<script lang="ts">
import Sidebar from './Sidebar.svelte';
import { getDrawerStore, Drawer } from '@skeletonlabs/skeleton';
const drawerStore = getDrawerStore();
$: classesDrawer = $drawerStore.id === 'doc-sidenav' ? 'lg:hidden' : '';
</script>
<Drawer width="w-[144px]" class={classesDrawer}>
{#if $drawerStore.id === 'doc-sidenav'}
<!-- Doc Sidebar -->
<Sidebar />
{/if}
</Drawer>

View File

@@ -0,0 +1,43 @@
import HomeSvg from '$lib/Components/SVGs/HomeSVG.svelte';
import IdentitiesSvg from '$lib/Components/SVGs/IdentitiesSVG.svelte';
import MessagesSvg from '$lib/Components/SVGs/MessagesSVG.svelte';
import ReportsSvg from '$lib/Components/SVGs/ReportsSVG.svelte';
import SourcesSvg from '$lib/Components/SVGs/SourcesSVG.svelte';
export const navigation = [
{
name: 'Main',
content: [
{
url: '/home',
name: 'Home',
description: 'Home page for the application.',
icon: HomeSvg
},
{
url: '/home/sources',
name: 'Sources',
description: 'a list of Sources in IdentityNow.',
icon: SourcesSvg
},
{
url: '/home/identities',
name: 'Identities',
description: 'a list of Identities in IdentityNow.',
icon: IdentitiesSvg
},
{
url: '/home/reports',
name: 'Reports',
description: 'a list of Reports for IdentityNow.',
icon: ReportsSvg
},
{
url: '/home/courier',
name: 'Courier',
description: 'an API client for IdentityNow with authentication baked right in.',
icon: MessagesSvg
}
]
}
];

View File

@@ -0,0 +1,211 @@
import type { Cookies } from '@sveltejs/kit';
import { redirect } from '@sveltejs/kit';
import axios from 'axios';
import jwt from 'jsonwebtoken';
export function generateAuthLink(tenantUrl: string) {
return `${tenantUrl}/oauth/authorize?client_id=sailpoint-cli&response_type=code&redirect_uri=http://localhost:3000/callback`;
}
export type Session = {
baseUrl: string;
tenantUrl: string;
};
export type IdnSession = {
access_token: string;
refresh_token: string;
claims_supported: string;
expires_in: string;
identity_id: string;
internal: string;
jti: string;
org: string;
pod: string;
scope: string;
strong_auth: string;
strong_auth_supported: string;
tenant_id: string;
token_type: string;
};
export type TokenDetails = {
tenant_id: string;
internal: boolean;
pod: string;
org: string;
identity_id: string;
user_name: string;
strong_auth: boolean;
force_auth_supported: boolean;
active: boolean;
authorities: string[];
client_id: string;
encoded_scope: string[];
strong_auth_supported: boolean;
claims_supported: boolean;
scope: string[];
exp: number;
jti: string;
};
export function lastCheckedToken(cookies: Cookies): string {
const lastCheckedToken = cookies.get('lastCheckedToken');
if (!lastCheckedToken) {
return '';
}
return lastCheckedToken;
}
export function getTokenDetails(cookies: Cookies): TokenDetails {
const tokenDetailsString = cookies.get('tokenDetails');
if (!tokenDetailsString) {
return {} as TokenDetails;
}
return JSON.parse(tokenDetailsString) as TokenDetails;
}
export function setTokenDetails(cookies: Cookies, tokenDetails: TokenDetails) {
cookies.set('tokenDetails', JSON.stringify(tokenDetails), {
path: '/',
httpOnly: false,
secure: false
});
}
export async function checkToken(apiUrl: string, token: string): Promise<TokenDetails> {
const body = 'token=' + token;
const url = `${apiUrl}/oauth/check_token/`;
const response = await axios.post(url, body).catch(function (err) {
if (err.response) {
// Request made and server responded
console.log(err.response.data);
console.log(err.response.status);
console.log(err.response.headers);
}
return undefined;
});
// if (response) {
// console.log(response.data);
// }
const tokenDetails = response!.data;
return tokenDetails;
}
export async function refreshToken(apiUrl: string, refreshToken: string): Promise<IdnSession> {
const url = `${apiUrl}/oauth/token?grant_type=refresh_token&client_id=sailpoint-cli&refresh_token=${refreshToken}`;
const response = await axios.post(url).catch(function (err) {
if (err.response) {
// Request made and server responded
console.log(err.response.data);
console.log(err.response.status);
console.log(err.response.headers);
}
return undefined;
});
// if (response) {
// console.log(response.data)
// }
const idnSession: IdnSession = response!.data as IdnSession;
return idnSession;
}
export async function logout(cookies: Cookies) {
cookies.delete('session', {
path: '/',
httpOnly: false,
secure: false
});
cookies.delete('idnSession', {
path: '/',
httpOnly: false,
secure: false
});
}
export function checkSession(cookies: Cookies): boolean {
const sessionString = cookies.get('session');
if (!sessionString) {
return false;
}
return true;
}
export function checkIdnSession(cookies: Cookies): boolean {
const idnSessionString = cookies.get('idnSession');
if (!idnSessionString) {
return false;
}
return true;
}
export function getSession(cookies: Cookies): Session {
const sessionString = cookies.get('session');
if (!sessionString) return { baseUrl: '', tenantUrl: '' };
return JSON.parse(sessionString) as Session;
}
export async function getToken(cookies: Cookies): Promise<IdnSession> {
const sessionString = cookies.get('session');
const idnSessionString = cookies.get('idnSession');
const session: Session = JSON.parse(sessionString!);
if (!idnSessionString) {
console.log('IdnSession does not exist, redirecting to login');
redirect(302, generateAuthLink(session.tenantUrl));
}
const idnSession: IdnSession = JSON.parse(idnSessionString);
if (
idnSession &&
session &&
!session.baseUrl.toLowerCase().includes(idnSession.org.toLowerCase())
) {
redirect(302, generateAuthLink(session.tenantUrl));
}
if (isJwtExpired(idnSession.access_token)) {
console.log('Refreshing IdnSession token...');
const newSession = await refreshToken(session.baseUrl, idnSession.refresh_token);
cookies.set('idnSession', JSON.stringify(newSession), {
path: '/',
httpOnly: false,
secure: false
});
return Promise.resolve(newSession);
} else {
console.log('IdnSession token is good');
return Promise.resolve(idnSession);
}
}
function isJwtExpired(token: string): boolean {
try {
const decodedToken = jwt.decode(token, { complete: true });
if (
!decodedToken ||
!decodedToken.payload ||
typeof decodedToken.payload === 'string' ||
!decodedToken.payload.exp
) {
// The token is missing the expiration claim ('exp') or is not a valid JWT.
return true; // Treat as expired for safety.
}
// Get the expiration timestamp from the token.
const expirationTimestamp = decodedToken.payload.exp;
// Get the current timestamp.
const currentTimestamp = Math.floor(Date.now() / 1000);
// Check if the token has expired.
return currentTimestamp >= expirationTimestamp;
} catch (error) {
// An error occurred during decoding.
return true; // Treat as expired for safety.
}
}

View File

@@ -0,0 +1,55 @@
<script lang="ts">
import { page } from '$app/stores';
import { CodeBlock } from '@skeletonlabs/skeleton';
$: console.log($page);
</script>
<div class="p-4">
<div class="card p-4">
<p class="text-center p-2">
WHOOPS! <br /> <span class="text-red-500">a {$page.status} error occurred.</span> <br /> If
you believe this is a bug please submit an issue on
<a
class="underline text-blue-500 hover:text-blue-700"
href="https://github.com/sailpoint-oss/idn-admin-console/issues/new/choose"
rel="noreferrer"
target="_blank"
>
GitHub
</a>
</p>
{#if $page.error?.message}
<p class="py-2">Message: <br /><span class="text-red-500">{$page.error.message}</span></p>
{/if}
{#if $page.error?.urls}
<p>These links may be helpful:</p>
<ul>
{#each $page.error?.urls as url}
<li>
-
<a
class="underline text-blue-500 hover:text-blue-700"
href={url}
rel="noreferrer"
target="_blank">{url}</a
>
</li>
{/each}
</ul>
{/if}
{#if $page.error?.context}
<div class="py-2">
<p>Context</p>
<CodeBlock language="json" code={JSON.stringify($page.error?.context, null, 4)} />
</div>
{/if}
{#if $page.error?.errData}
<div class="pt-2">
<p>Error Data</p>
<CodeBlock language="json" code={JSON.stringify($page.error?.errData, null, 4)} />
</div>
{/if}
</div>
</div>

View File

@@ -0,0 +1,3 @@
export const load = async ({ locals }) => {
return { tokenDetails: locals.tokenDetails };
};

View File

@@ -0,0 +1,206 @@
<script lang="ts">
import { Modal, initializeStores, type ModalComponent } from '@skeletonlabs/skeleton';
import { onMount } from 'svelte';
import '../app.postcss';
import CodeBlockModal from '$lib/Components/CodeBlockModal.svelte';
import { page } from '$app/stores';
import { capitalize, parseInitials } from '$lib/Utils';
import Sidebar from '$lib/sidebar/Sidebar.svelte';
import {
AppBar,
AppShell,
Avatar,
LightSwitch,
getDrawerStore,
popup,
storePopup,
type DrawerSettings,
type PopupSettings
} from '@skeletonlabs/skeleton';
import { arrow, autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom';
import HamburgerSvg from '$lib/Components/SVGs/HamburgerSVG.svelte';
import SidebarDrawer from '$lib/sidebar/SidebarDrawer.svelte';
import hljs from 'highlight.js/lib/core';
// Import each language module you require
import xml from 'highlight.js/lib/languages/xml'; // for HTML
import css from 'highlight.js/lib/languages/css';
import json from 'highlight.js/lib/languages/json';
import javascript from 'highlight.js/lib/languages/javascript';
import typescript from 'highlight.js/lib/languages/typescript';
import shell from 'highlight.js/lib/languages/shell';
import { storeHighlightJs } from '@skeletonlabs/skeleton';
import 'highlight.js/styles/github-dark.css';
initializeStores();
let ready: boolean = false;
onMount(() => (ready = true));
const modalRegistry: Record<string, ModalComponent> = {
// Set a unique modal ID, then pass the component reference
codeBlockModal: { ref: CodeBlockModal }
// ...
};
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
export let data;
// Register each imported language module
hljs.registerLanguage('xml', xml); // for HTML
hljs.registerLanguage('css', css);
hljs.registerLanguage('json', json);
hljs.registerLanguage('javascript', javascript);
hljs.registerLanguage('typescript', typescript);
hljs.registerLanguage('shell', shell);
storeHighlightJs.set(hljs);
let crumbs: Array<{ label: string; href: string }> = [];
$: {
// Remove zero-length tokens.
const tokens = $page.url.pathname.split('/').filter((t) => t !== '');
let tokenPath = '';
crumbs = tokens.map((t) => {
tokenPath += '/' + t;
return {
label: t
.split('-')
.map((word) => capitalize(word))
.join(' '),
href: tokenPath
};
});
crumbs = crumbs.filter((c) => c.label !== 'Logout' && c.label !== 'Callback');
}
const drawerStore = getDrawerStore();
// Drawer Handler
function drawerOpen(): void {
const s: DrawerSettings = { id: 'doc-sidenav' };
drawerStore.open(s);
}
const popupAccount: PopupSettings = {
// Represents the type of event that opens/closed the popup
event: 'click',
// Matches the data-popup value on your popup element
target: 'popupAccount',
// Defines which side of your trigger the popup will appear
placement: 'bottom'
};
</script>
<Modal components={modalRegistry} />
<SidebarDrawer />
<AppShell>
<svelte:fragment slot="header">
<AppBar padding="p-2" class="h-![72px]">
<svelte:fragment slot="lead">
<div class="flex items-center space-x-4">
{#if data.tokenDetails}
<button on:click={drawerOpen} class="btn-icon btn-icon-sm lg:!hidden">
<HamburgerSvg class="w-6 h-6" />
</button>
{/if}
<img class="h-8 w-8" src="/logo.ico" alt="SailPoint TetraSail" />
</div>
</svelte:fragment>
<p class="text-xl lg:!block hidden">IdentityNow Admin Console</p>
<svelte:fragment slot="trail">
<LightSwitch />
{#if data.tokenDetails}
<div class="rounded-full w-fit" use:popup={popupAccount}>
<Avatar
initials={parseInitials(data?.tokenDetails?.user_name)}
border="hover:border-2 border-surface-300-600-token hover:!border-primary-500"
cursor="cursor-pointer"
width="w-10"
/>
<div
class="card p-4 w-72 !shadow-xl bg-surface-100-800-token"
data-popup="popupAccount"
>
<div class="arrow bg-surface-50-900-token" />
<div class="flex flex-col gap-2">
<div class="space-y-4">
<Avatar initials={parseInitials(data?.tokenDetails?.user_name)} width="w-16" />
<div>
<p class="font-bold">{data?.tokenDetails?.user_name}</p>
<div class="flex flex-wrap gap-4">
<small>
<span class="opacity-50">Tenant:</span>
<strong>{data?.tokenDetails?.org}</strong>
</small>
<small>
<span class="opacity-50">Pod:</span>
<strong>{data?.tokenDetails?.pod}</strong>
</small>
</div>
<small><span class="opacity-50">Scopes:</span></small>
<div class="flex gap-4 flex-wrap">
{#each data?.tokenDetails?.scope as scope}
<small><strong></strong>{scope}</small>
{/each}
</div>
</div>
<a href="/logout" class="btn variant-soft w-full">Logout</a>
</div>
</div>
</div>
</div>
{/if}
</svelte:fragment>
</AppBar>
</svelte:fragment>
<svelte:fragment slot="sidebarLeft">
{#if data.tokenDetails}
<Sidebar class="hidden lg:grid overflow-hidden" />
{/if}
</svelte:fragment>
<!-- <svelte:fragment slot="sidebarRight">Sidebar Right</svelte:fragment> -->
<!-- <svelte:fragment slot="pageHeader">Page Header</svelte:fragment> -->
<!-- Router Slot -->
<div class="flex flex-col">
{#if crumbs.length > 0}
<div class="pl-2 pt-2 pr-2">
<ol class="breadcrumb card p-2">
{#each crumbs as crumb, i}
<!-- If crumb index is less than the breadcrumb length minus 1 -->
{#if i < crumbs.length - 1}
<li class="crumb"><a class="anchor" href={crumb.href}>{crumb.label}</a></li>
<li class="crumb-separator" aria-hidden>&rsaquo;</li>
{:else}
<li class="crumb">{crumb.label}</li>
{/if}
{/each}
</ol>
</div>
{/if}
<div class="p-2 grow">
{#if ready}
<slot />
{/if}
</div>
</div>
<!-- ---- / ---- -->
<!-- <svelte:fragment slot="pageFooter">Page Footer</svelte:fragment> -->
<!-- <svelte:fragment slot="footer">Footer</svelte:fragment> -->
</AppShell>

View File

@@ -0,0 +1,51 @@
import { generateAuthLink } from '$lib/utils/oauth';
import { redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
export const actions = {
default: async ({ cookies, request }) => {
const data = await request.formData();
const baseUrl = data.get('baseUrl');
const tenantUrl = data.get('tenantUrl');
if (!baseUrl || !tenantUrl) {
redirect(302, '/login');
}
const session = { baseUrl: baseUrl.toString(), tenantUrl: tenantUrl.toString() };
console.log('session', session);
const idnSessionString = cookies.get('idnSession');
if (idnSessionString) {
// console.log('sessionString', sessionString);
const idnSession = JSON.parse(idnSessionString);
if (idnSession && session.baseUrl.toLowerCase().includes(idnSession.org.toLowerCase())) {
console.log('Credential Cache Hit');
redirect(302, '/home');
} else {
console.log('Credential Cache Miss');
}
}
cookies.set('session', JSON.stringify(session), {
path: '/'
});
redirect(302, generateAuthLink(tenantUrl.toString()));
}
} satisfies Actions;
export const load = async ({ locals }) => {
if (!locals.hasSession || !locals.hasIdnSession) return {};
if (
locals.session &&
locals.idnSession &&
locals.session.baseUrl.toLowerCase().includes(locals.idnSession.org.toLowerCase())
) {
redirect(302, '/home');
}
return {};
};

View File

@@ -0,0 +1,77 @@
<script lang="ts">
import { browser } from '$app/environment';
import { enhance } from '$app/forms';
import { localStorageStore } from '@skeletonlabs/skeleton';
import type { Writable } from 'svelte/store';
let desktop: string;
if (window.electron && browser) {
window.electron.receive('from-main', (data: any) => {
desktop = `Received Message "${data}" from Electron`;
console.log(desktop);
});
}
const agent = window.electron ? 'Electron' : 'Browser';
const tenant: Writable<string> = localStorageStore('tenant', 'tenant');
const domain: Writable<string> = localStorageStore('domain', 'identitynow');
const baseUrl: Writable<string> = localStorageStore(
'baseUrl',
'https://${tenant}.api.${domain}.com'
);
const tenantUrl: Writable<string> = localStorageStore(
'tenantUrl',
'https://${tenant}.${domain}.com'
);
$: baseUrl.set(`https://${$tenant}.api.${$domain}.com`);
$: tenantUrl.set(`https://${$tenant}.${$domain}.com`);
</script>
<main class="p-32 h-full">
<div class="flex flex-row justify-center">
<img
class="h-12 min-w-[590px]"
src="/SailPoint-Developer-Community-Lockup.png"
alt="sailPoint Logo"
/>
</div>
<div class="">
<div class="text-2xl text-center py-2">Enter your tenant information to continue</div>
<form method="POST" use:enhance class="flex flex-col gap-4">
<label class="">
Tenant
<input name="tenant" placeholder={`tenant`} bind:value={$tenant} class="input p-2" />
</label>
<label class="">
Domain
<input name="domain" placeholder={`identitynow`} bind:value={$domain} class="input p-2" />
</label>
<label class="">
API Base URL
<input
name="baseUrl"
placeholder={`https://${tenant}.api.${domain}.com`}
bind:value={$baseUrl}
class="input p-2"
/>
</label>
<label class="">
Tenant URL
<input
name="tenantUrl"
placeholder={`https://${tenant}.identitynow.com`}
bind:value={$tenantUrl}
class="input p-2"
/>
</label>
<button type="submit" class="btn variant-filled-primary w-full mt-2 !text-white text-lg">
Login
</button>
</form>
</div>
</main>

View File

@@ -0,0 +1,20 @@
import { createConfiguration } from '$lib/sailpoint/sdk';
import { getToken } from '$lib/utils/oauth';
import { json } from '@sveltejs/kit';
import { ManagedClustersBetaApi } from 'sailpoint-api-client';
/** @type {import('./$types').RequestHandler} */
export async function GET({ cookies, params }) {
// Generic SDK setup
const session = JSON.parse(cookies.get('session')!);
const idnSession = await getToken(cookies);
const config = createConfiguration(session.baseUrl, idnSession.access_token);
// Route specific SDK call
const api = new ManagedClustersBetaApi(config);
const val = await api.getManagedCluster({ id: params.clusterID });
// console.log(val);
return json(val.data);
}

View File

@@ -0,0 +1,44 @@
import { generateAuthLink } from '$lib/utils/oauth';
import { error, redirect } from '@sveltejs/kit';
import axios from 'axios';
import type { PageServerLoad } from './$types';
import { counterList } from './loadinglist';
export const load: PageServerLoad = async ({ url, cookies, locals }) => {
const code = url.searchParams.get('code');
if (!code) error(500, 'No Authorization Code Provided');
if (!locals.session) error(500, 'No Session Found');
const response = await axios
.post(
`${locals.session.baseUrl}/oauth/token?grant_type=authorization_code&client_id=sailpoint-cli&code=${code}&redirect_uri=http://localhost:3000/callback`
)
.catch(function (err) {
if (err.response) {
// Request made and server responded
console.log(err.response.data);
console.log(err.response.status);
console.log(err.response.headers);
redirect(302, generateAuthLink(locals.session!.tenantUrl));
} else if (err.request) {
// The request was made but no response was received
error(500, { message: 'No Response From IDN' });
} else {
// Something happened in setting up the request that triggered an err
error(500, {
message: 'Error during Axios Request'
});
}
});
console.log(response.data);
cookies.set('idnSession', JSON.stringify(response.data), {
path: '/',
httpOnly: false,
secure: false
});
return { counterList };
};

View File

@@ -0,0 +1,37 @@
<script lang="ts">
import { browser } from '$app/environment';
import { goto } from '$app/navigation';
import AnimatedCounter from '$lib/Components/AnimatedCounter.svelte';
export let data;
console.log(data);
if (browser) setTimeout(() => goto(`/home`), 2000);
</script>
<div class="grid place-content-center h-[80vh]">
<div class="card card-glass z-50 space-y-5 p-4 md:p-10">
<div class="skills">
<AnimatedCounter
interval={1500}
transitionInterval={10}
startImmediately
values={data.counterList}
random
class="custom-skill px-2"
/>
</div>
</div>
</div>
<style>
.skills {
display: flex;
justify-items: start;
align-items: center;
flex-wrap: wrap;
}
:global(.custom-skill) {
display: inline-block;
text-align: center;
}
</style>

View File

@@ -0,0 +1,237 @@
export const counterList = [
'Reticulating splines...',
'Generating witty dialog...',
'Swapping time and space...',
'Spinning violently around the y-axis...',
'Tokenizing real life...',
'Bending the spoon...',
'Filtering morale...',
"Don't think of purple hippos...",
'We need a new fuse...',
'Have a good day.',
'Upgrading Windows, your PC will restart several times. Sit back and relax. /s',
'640K ram+ ought to be enough for anybody',
'The architects are still drafting',
"We're building the buildings as fast as we can",
'Would you prefer chicken, steak, or tofu?',
'(Pay no attention to the man behind the curtain)',
'...and enjoy the elevator music...',
'Please wait while the little elves draw your map',
"Don't worry - a few bits tried to escape, but we caught them",
'Would you like fries with that?',
'Checking the gravitational constant in your locale...',
'Go ahead, hold your breath!',
"...at least you're not on hold...",
'Hum something loud while others stare',
"You're not in Kansas any more",
'The server is powered by a lemon and two electrodes.',
'Please wait while a larger software vendor in Seattle takes over the world',
"We're testing your patience",
'As if you had any other choice',
'Follow the white rabbit',
"Why don't you order a sandwich?",
'While the satellite moves into position',
'keep calm and npm install',
'The bits are flowing slowly today',
"Dig on the 'X' for buried treasure... ARRR!",
"It's still faster than you could draw it",
"The last time I tried this the monkey didn't survive. Let's hope it works better this time.",
'I should have had a V8 this morning.',
'My other loading screen is much faster.',
'Reconfoobling energymotron...',
'(Insert quarter)',
'Are we there yet?',
'Just count to 10',
'Why so serious?',
"It's not you. It's me.",
'Counting backwards from Infinity',
"Don't panic...",
"Don't panic, Just count to infinity.",
'Embiggening Prototypes',
'Do not run! We are your friends!',
'Do you come here often?',
"Warning: Don't set yourself on fire.",
"We're making you a cookie.",
'Creating time-loop inversion field',
'Spinning the wheel of fortune...',
'Loading the enchanted bunny...',
'Computing chance of success',
"I'm sorry Dave, I can't do that.",
'Looking for exact change',
'All your web browser are belong to us',
'All I really need is a kilobit.',
'I feel like im supposed to be loading something. . .',
'What do you call 8 Hobbits? A Hobbyte.',
'Should have used a compiled language...',
'Is this Windows?',
'Adjusting flux capacitor...',
'Please wait until the sloth starts moving.',
"Don't break your screen yet!",
"I swear it's almost done.",
"Let's take a mindfulness minute...",
'Unicorns are at the end of this road, I promise.',
'Listening for the sound of one hand clapping...',
"Keeping all the 1's and removing all the 0's...",
'Putting the icing on the cake. The cake is not a lie...',
'Cleaning off the cobwebs...',
"Making sure all the i's have dots...",
'We need more dilithium crystals',
'Where did all the internets go',
'Connecting Neurotoxin Storage Tank...',
'Granting wishes...',
'Time flies when youre having fun.',
'Get some coffee and come back in ten minutes..',
'Spinning the hamster…',
'99 bottles of beer on the wall..',
'Stay awhile and listen..',
'Be careful not to step in the git-gui',
'You shall not pass! yet..',
'Load it and they will come',
'Convincing AI not to turn evil..',
'There is no spoon. Because we are not done loading it',
'Your left thumb points to the right and your right thumb points to the left.',
'How did you get here?',
'Wait, do you smell something burning?',
'Computing the secret to life, the universe, and everything.',
'When nothing is going right, go left!!...',
"I love my job only when I'm on vacation...",
"i'm not lazy, I'm just relaxed!!",
'Why are they called apartments if they are all stuck together?',
'Life is Short, Talk Fast!!!!',
'Optimism, is a lack of information.....',
'Ive got problem for your solution…..',
'Where theres a will, theres a relative.',
'Adults are just kids with money.',
'I think I am, therefore, I am. I think.',
'Coffee, Chocolate, Men. The richer the better!',
'git happens',
'May the forks be with you',
'A commit a day keeps the mobs away',
"This is not a joke, it's a commit.",
'Constructing additional pylons...',
'We are not liable for any broken screens as a result of waiting.',
'Hello IT, have you tried turning it off and on again?',
'If you type Google into Google you can break the internet',
'Well, this is embarrassing.',
'What is the airspeed velocity of an unladen swallow?',
'Hello, IT... Have you tried forcing an unexpected reboot?',
'The Elders of the Internet would never stand for it.',
'Space is invisible mind dust, and stars are but wishes.',
"Didn't know paint dried so quickly.",
'Everything sounds the same',
"I'm going to walk the dog",
"I didn't choose the engineering life. The engineering life chose me.",
'Dividing by zero...',
'Spawn more Overlord!',
'If Im not back in five minutes, just wait longer.',
'Some days, you just cant get rid of a bug!',
'Were going to need a bigger boat.',
'Web developers do it with <style>',
'I need to git pull --my-life-together',
'Cracking military-grade encryption...',
'Simulating traveling salesman...',
'Proving P=NP...',
'Entangling superstrings...',
'Twiddling thumbs...',
'Searching for plot device...',
'Trying to sort in O(n)...',
'Looking for sense of humour, please hold on.',
'Please wait while the intern refills his coffee.',
'A different error message? Finally, some progress!',
'Please hold on as we reheat our coffee',
'Kindly hold on as we convert this bug to a feature...',
'Kindly hold on as our intern quits vim...',
'Winter is coming...',
'Installing dependencies',
'Switching to the latest JS framework...',
'Distracted by cat gifs',
'BRB, working on my side project',
'@todo Insert witty loading message',
"Let's hope it's worth the wait",
'Aw, snap! Not..',
'Ordering 1s and 0s...',
'Updating dependencies...',
"Whatever you do, don't look behind you...",
'Please wait... Consulting the manual...',
"It is dark. You're likely to be eaten by a grue.",
'Loading funny message...',
"It's 10:00pm. Do you know where your children are?",
'Waiting for Daenerys say all her titles...',
'Feel free to spin in your chair',
'What the what?',
'format C: ...',
'Forget you saw that password I just typed into the IM ...',
"What's under there?",
'Go ahead, hold your breath and do an ironman plank till loading is complete',
'Bored of slow loading spinner, buy more RAM!',
"Help, I'm trapped in a loader!",
'What is the difference between a hippo and a zippo? One is really heavy, the other is a little lighter',
'Please wait, while we purge the Decepticons for you. Yes, You can thanks us later!',
'Mining some bitcoins...',
'Downloading more RAM..',
'Updating to Windows Vista...',
'Deleting System32 folder',
"Hiding all ;'s in your code",
'Alt-F4 speeds things up.',
'Initializing the initializer...',
'When was the last time you dusted around here?',
'Optimizing the optimizer...',
'Last call for the data bus! All aboard!',
'Running swag sticker detection...',
"Never let a computer know you're in a hurry.",
'A computer will do what you tell it to do, but that may be much different from what you had in mind.',
"Some things man was never meant to know. For everything else, there's Google.",
"Unix is user-friendly. It's just very selective about who its friends are.",
'Shovelling coal into the server',
'Pushing pixels...',
'How about this weather, eh?',
'Building a wall...',
'Everything in this universe is either a potato or not a potato',
'The severity of your issue is always lower than you expected.',
'Updating Updater...',
'Downloading Downloader...',
'Debugging Debugger...',
'Reading Terms and Conditions for you.',
'Digested cookies being baked again.',
'Live long and prosper.',
"There is no cow level, but there's a goat one!",
'Running with scissors...',
'Definitely not a virus...',
'You may call me Steve.',
'You seem like a nice person...',
'Work, work...',
'Patience! This is difficult, you know...',
'Discovering new ways of making you wait...',
'Time flies like an arrow; fruit flies like a banana',
'Two men walked into a bar; the third ducked...',
'Sooooo... Have you seen my vacation photos yet?',
"Sorry we are busy catching em' all, we're done soon",
'TODO: Insert elevator music',
'Still faster than Windows update',
'Composer hack: Waiting for reqs to be fetched is less frustrating if you add -vvv to your command.',
'Please wait while the minions do their work',
'Grabbing extra minions',
'Doing the heavy lifting',
"We're working very Hard .... Really",
'Waking up the minions',
'You are number 2843684714 in the queue',
'Please wait while we serve other customers...',
'Our premium plan is faster',
'Feeding unicorns...',
'Rupturing the subspace barrier',
'Creating an anti-time reaction',
'Converging tachyon pulses',
'Bypassing control of the matter-antimatter integrator',
'Adjusting the dilithium crystal converter assembly',
'Reversing the shield polarity',
'Disrupting warp fields with an inverse graviton burst',
'Up, Up, Down, Down, Left, Right, Left, Right, B, A, Select, Start',
'Do you like my loading animation? I made it myself',
'Whoah, look at it go!',
"No, I'm awake. I was just resting my eyes.",
'One mississippi, two mississippi...',
"Don't panic... AHHHHH!",
'Ensuring Gnomes are still short.',
'Baking ice cream...'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
].sort((a, b) => 0.5 - Math.random());

View File

@@ -0,0 +1,5 @@
export const load = async ({ locals }) => {
if (!locals.hasSession) return { baseUrl: '', tenantUrl: '' };
return { session: locals.session };
};

View File

@@ -0,0 +1,18 @@
<script lang="ts">
import ResourceLinksCard from '$lib/Components/HomepageCards/ResourceLinksCard.svelte';
import StatusCard from '$lib/Components/HomepageCards/StatusCard.svelte';
import SupportLinksCard from '$lib/Components/HomepageCards/SupportLinksCard.svelte';
import TenantLinksCard from '$lib/Components/HomepageCards/TenantLinksCard.svelte';
export let data;
console.log(data);
</script>
<div class="flex flex-col gap-2 grow">
<div class="flex flex-row flex-wrap gap-2 grow">
<StatusCard />
<TenantLinksCard tenantUrl={data.session?.tenantUrl} />
<ResourceLinksCard />
<SupportLinksCard />
</div>
</div>

View File

@@ -0,0 +1,19 @@
import { getSession, getToken } from '$lib/utils/oauth.js';
export const load = async ({ fetch, cookies }) => {
const V3SpecRes = fetch(
'https://raw.githubusercontent.com/sailpoint-oss/api-specs/main/dereferenced/deref-sailpoint-api.v3.json'
);
const BetaSpecRes = fetch(
'https://raw.githubusercontent.com/sailpoint-oss/api-specs/main/dereferenced/deref-sailpoint-api.beta.json'
);
const V3Spec = await V3SpecRes.then((r) => r.json()).then((r) => r.paths);
const BetaSpec = await BetaSpecRes.then((r) => r.json()).then((r) => r.paths);
const session = await getSession(cookies);
const idnSession = await getToken(cookies);
return { V3Spec, BetaSpec, idnSession, session };
};

View File

@@ -0,0 +1,160 @@
<script lang="ts">
import { JSONEditor } from 'svelte-jsoneditor';
import { modeCurrent } from '@skeletonlabs/skeleton';
import axios from 'axios';
export let data;
console.log(data);
type TextContent = {
text: string;
};
type JSONContent = {
json: unknown;
};
type Content = JSONContent | TextContent;
let requestBody: Content = {
text: '',
json: undefined
};
let responseBody: Content = {
text: '',
json: undefined
};
function mapPath(path: string[]) {
const [name, value] = path;
return { name, value };
}
async function makeAPICall() {
const response = await axios({
method: selectedAPIMethod,
url: `${data.session.baseUrl}/${APICallPath}`,
data: requestBody.json,
headers: {
authorization: `Bearer ${data.idnSession.access_token}`
}
}).catch((err) => {
console.error(err);
return err;
});
responseBody = { json: response.data };
console.log(responseBody);
}
function handleKeydown(event: KeyboardEvent) {
console.log(event);
if (event.isTrusted === true && event.key === 'Enter') {
makeAPICall();
}
}
let APIVersions = [
// @ts-expect-error - This is a valid API Version,
{ name: 'Beta', value: Object.entries(data.BetaSpec).map((path) => mapPath(path)) },
// @ts-expect-error - This is a valid API Version,
{ name: 'V3', value: Object.entries(data.V3Spec).map((path) => mapPath(path)) },
{
name: 'Custom',
value: [
{
name: 'Custom Path',
value: { GET: '', POST: '', PUT: '', PATCH: '', DELETE: '', HEAD: '' }
}
]
}
];
$: editorClasses = $modeCurrent === false ? 'jse-theme-dark' : '';
let selectedAPIVersion = APIVersions[0];
let selectedPath = selectedAPIVersion.value[0];
let APICallPath: string = `${selectedAPIVersion.name.toLowerCase()}${selectedPath.name}`;
let selectedAPIMethod = 'GET';
</script>
<div class="flex flex-col gap-2">
<div class="flex flex-row">
<select
placeholder="Select an API Version"
class="w-[100px] !rounded-r-none px-4 py-2 select"
bind:value={selectedAPIVersion}
on:change={() => {
selectedPath = selectedAPIVersion.value[0];
if (['Beta', 'V3', 'V2'].includes(selectedAPIVersion.name)) {
APICallPath = `${selectedAPIVersion.name.toLowerCase()}${selectedPath.name}`;
} else if (['CC'].includes(selectedAPIVersion.name)) {
APICallPath = `${selectedPath.name}`;
} else {
APICallPath = '';
}
}}
>
{#each APIVersions as APIVersion}
<option selected={selectedAPIVersion === APIVersion} value={APIVersion}>
{APIVersion.name}
</option>
{/each}
</select>
<select
placeholder="Choose the API Endpoint"
class="!rounded-l-none px-4 select"
bind:value={selectedPath}
on:change={() => {
if (['Beta', 'V3', 'V2'].includes(selectedAPIVersion.name)) {
APICallPath = `${selectedAPIVersion.name.toLowerCase()}${selectedPath.name}`;
} else if (['CC'].includes(selectedAPIVersion.name)) {
APICallPath = `${selectedPath.name}`;
} else {
APICallPath = '';
}
}}
>
{#each selectedAPIVersion.value as path}
<option selected={path === selectedPath} value={path}>{path.name}</option>
{/each}
</select>
</div>
<div class="flex flex-row">
<select class="w-[100px] select rounded-r-none">
{#each Object.entries(selectedPath.value) as [method, content]}
<option>{method.toUpperCase()}</option>
{/each}
</select>
<input
type="text"
class="w-full !rounded-l-none rounded-r-none px-4 py-2 input"
bind:value={APICallPath}
/>
<button on:click={makeAPICall} class="btn variant-filled-surface rounded-l-none rounded-r-sm">
Call
</button>
</div>
<div class="card">
<p class="text-center pt-4">Request</p>
<div class="{editorClasses} rounded-lg overflow-hidden pt-2">
<JSONEditor bind:content={requestBody} />
</div>
</div>
<div class="card">
<p class="text-center pt-4">Response</p>
<div class="{editorClasses} rounded-lg overflow-hidden pt-2">
<JSONEditor bind:content={responseBody} />
</div>
</div>
</div>
<style>
/* load one or multiple themes */
@import 'svelte-jsoneditor/themes/jse-theme-dark.css';
</style>

View File

@@ -0,0 +1,62 @@
import { getFilters, getLimit, getPage, getSorters } from '$lib/Utils.js';
import { createConfiguration } from '$lib/sailpoint/sdk.js';
import { getSession, getToken } from '$lib/utils/oauth.js';
import { error } from '@sveltejs/kit';
import {
IdentitiesBetaApi,
type IdentitiesBetaApiListIdentitiesRequest,
type IdentityBeta
} from 'sailpoint-api-client';
export const load = async ({ cookies, url }) => {
const session = await getSession(cookies);
const idnSession = await getToken(cookies);
const config = createConfiguration(session.baseUrl, idnSession.access_token);
const api = new IdentitiesBetaApi(config);
const page = getPage(url);
const filters = getFilters(url);
const limit = getLimit(url);
const sorters = getSorters(url);
const requestParams: IdentitiesBetaApiListIdentitiesRequest = {
filters,
offset: Number(page) * Number(limit),
limit: Number(limit),
sorters,
count: true
};
const apiResponse = api.listIdentities(requestParams);
const totalCount = new Promise<number>((resolve) => {
apiResponse.then((response) => {
resolve(response.headers['x-total-count']);
});
});
const identities = new Promise<IdentityBeta[]>((resolve) => {
apiResponse
.then((response) => {
resolve(response.data);
})
.catch((err) => {
error(500, {
message:
'an error occurred while fetching identities. Please examine your filters and and sorters and try again.',
context: { params: { page, limit, filters, sorters } },
urls: [
'https://developer.sailpoint.com/idn/api/standard-collection-parameters#filtering-results'
],
errData: err.response.data
});
});
});
return {
totalCount,
identities,
params: { page, limit, filters, sorters }
};
};

View File

@@ -0,0 +1,125 @@
<script lang="ts">
import Paginator from '$lib/Components/Paginator.svelte';
import Progress from '$lib/Components/Progress.svelte';
import {
TriggerCodeModal,
createOnAmountChange,
createOnGo,
createOnPageChange,
formatDate
} from '$lib/Utils.js';
import { getModalStore } from '@skeletonlabs/skeleton';
const modalStore = getModalStore();
export let data;
$: onPageChange = createOnPageChange({ ...data.params, filters, sorters }, '/home/identities');
$: onAmountChange = createOnAmountChange(
{ ...data.params, filters, sorters },
'/home/identities'
);
$: onGo = createOnGo({ ...data.params, filters, sorters }, '/home/identities');
let filters = '';
let sorters = '';
</script>
<div class="card flex justify-center flex-col align-middle">
{#await data.totalCount then totalCount}
{#if totalCount > 250 || Number(data.params.limit) < totalCount}
<Paginator
{onAmountChange}
{onGo}
{onPageChange}
settings={{
page: Number(data.params.page),
limit: Number(data.params.limit),
size: totalCount,
amounts: [10, 50, 100, 250]
}}
{filters}
{sorters}
{totalCount}
/>
{/if}
{/await}
{#await data.identities}
<div class="grid h-full place-content-center p-8">
<Progress width="w-[100px]" />
</div>
{:then identities}
<div class="table-container">
<table class="table">
<thead class="table-head">
<th>ID</th>
<th>Name</th>
<th>Lifecycle State</th>
<th>eMail</th>
<th>Created</th>
<th>Modified</th>
<th />
</thead>
<tbody class="table-body">
{#each identities as identity}
<tr>
<td>
<p class="text-center">{identity.id}</p>
</td>
<td>
<p class="text-center">{identity.name}</p>
</td>
<td>
<p class="text-center">{identity.lifecycleState?.stateName}</p>
</td>
<td>
<p class="text-center">{identity.emailAddress}</p>
</td>
<td>
<p class="text-center">{formatDate(identity.created)}</p>
</td>
<td>
<p class="text-center">{formatDate(identity.modified)}</p>
</td>
<td>
<div class="flex flex-col justify-center gap-1">
<a
href={`/home/identities/${identity.id}`}
class="btn btn-sm variant-filled-primary text-sm !text-white"
data-sveltekit-preload-data="hover"
>
Open
</a>
<button
on:click={() => TriggerCodeModal(identity, modalStore)}
class="btn btn-sm variant-filled-primary text-sm !text-white"
>
View
</button>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/await}
{#await data.totalCount then totalCount}
{#if totalCount > 250 || Number(data.params.limit) < totalCount}
<Paginator
{onAmountChange}
{onGo}
{onPageChange}
settings={{
page: Number(data.params.page),
limit: Number(data.params.limit),
size: totalCount,
amounts: [10, 50, 100, 250]
}}
{filters}
{sorters}
{totalCount}
/>
{/if}
{/await}
</div>

View File

@@ -0,0 +1,56 @@
import { createConfiguration } from '$lib/sailpoint/sdk';
import { getSession, getToken } from '$lib/utils/oauth';
import {
IdentitiesBetaApi,
SearchApi,
type EventDocument,
type IdentityBeta,
type Search
} from 'sailpoint-api-client';
export const load = async ({ cookies, params }) => {
const session = await getSession(cookies);
const idnSession = await getToken(cookies);
const config = createConfiguration(session.baseUrl, idnSession.access_token);
const identityApi = new IdentitiesBetaApi(config);
const searchApi = new SearchApi(config);
const identityResp = identityApi.getIdentity({ id: params.identityID });
const identityData = new Promise<IdentityBeta>((resolve) => {
identityResp
.then((response) => {
resolve(response.data);
})
.catch((err) => {
throw err;
});
});
const identityEvents = new Promise<EventDocument[]>((resolve) => {
identityResp.then((response) => {
const identity = response.data;
const search: Search = {
indices: ['events'],
query: {
query: `target.name: "${identity.name}"`
},
sort: ['created']
};
searchApi
.searchPost({
search
})
.then((response) => {
resolve(response.data);
})
.catch((err) => {
throw err;
});
});
});
return { identityData, identityEvents };
};

View File

@@ -0,0 +1,83 @@
<script lang="ts">
import Progress from '$lib/Components/Progress.svelte';
import { TriggerCodeModal } from '$lib/Utils';
import { CodeBlock, Tab, TabGroup, getModalStore } from '@skeletonlabs/skeleton';
export let data;
console.log(data);
let tabSet: number = 0;
const modalStore = getModalStore();
</script>
<div class=" flex flex-col gap-2">
{#await data.identityData}
<div class="card grid h-full place-content-center p-8">
<Progress width="w-[100px]" />
</div>
{:then identityData}
<div class="card p-4">
<h1 class="text-2xl font-bold">Name: {identityData.name}</h1>
<p class="">Alias: {identityData.alias}</p>
<p class="">ID: {identityData.id}</p>
<p class="">Lifecycle State: {identityData.lifecycleState?.stateName}</p>
</div>
<div class="card p-4">
<h2 class="pb-2">Identity JSON</h2>
<CodeBlock lineNumbers language="json" code={JSON.stringify(identityData, null, 4)} />
</div>
<div class="card p-4">
<TabGroup>
<Tab bind:group={tabSet} name="raw-source-values" value={0}>Identity Events</Tab>
<svelte:fragment slot="panel">
{#if tabSet === 0}
{#await data.identityEvents}
<div class="grid h-full place-content-center p-8">
<Progress width="w-[100px]" />
</div>
{:then identityEvents}
{#if identityEvents.length > 0}
<div class="table-container">
<table class="table">
<thead class="table-head">
<th>Name</th>
<th>Status</th>
<th>Created</th>
<th>Target</th>
<th>Actor</th>
<th />
</thead>
<tbody>
{#each identityEvents as event}
<tr>
<td>{event.name}</td>
<td>{event.status}</td>
<td>{event.created}</td>
<td>{event.target?.name}</td>
<td>{event.actor?.name}</td>
<td class="flex flex-col justify-center gap-1">
<button
class="btn variant-filled-primary text-sm !text-white"
on:click={() => TriggerCodeModal(event, modalStore)}
>
View
</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{:else}
<p class="text-center">No Identity Events</p>
{/if}
{/await}
{/if}
</svelte:fragment>
</TabGroup>
</div>
{/await}
</div>

View File

@@ -0,0 +1,29 @@
<script lang="ts">
import { reports } from '$lib/reports';
</script>
<div class="grid gap-2 grid-flow-row xl:grid-cols-4 lg:grid-cols-3 md:grid-cols-2 justify-center">
{#each reports as report (report.url)}
<a
class="card card-hover overflow-hidden"
data-sveltekit-preload-data="hover"
href={report.url}
>
<header
class="card-header aspect-[21/9] bg-[#526bf8] flex flex-col justify-center min-h-[105px]"
>
<p class="font-bold text-white uppercase text-center text-xl">
{report.name}
</p>
</header>
<div class="p-4 space-y-4">
<h3 class="h3" data-toc-ignore>Summary</h3>
<article>
<p>
{report.description}
</p>
</article>
</div>
</a>
{/each}
</div>

View File

@@ -0,0 +1,28 @@
import { createConfiguration } from '$lib/sailpoint/sdk.js';
import { getToken } from '$lib/utils/oauth.js';
import { SearchApi, type Search, Paginator, type IdentityDocument } from 'sailpoint-api-client';
export const load = async ({ cookies }) => {
const search: Search = {
indices: ['identities'],
query: {
query: `@accounts(disabled:false) AND (attributes.cloudLifecycleState:inactive)`
},
sort: ['name']
};
const session = JSON.parse(cookies.get('session')!);
const idnSession = await getToken(cookies);
const config = createConfiguration(session.baseUrl, idnSession.access_token);
const api = new SearchApi(config);
const reportResp = Paginator.paginateSearchApi(api, search, 100, 20000);
const reportData = new Promise<IdentityDocument[]>((resolve) => {
reportResp.then((response) => {
resolve(response.data);
});
});
return { reportData };
};

View File

@@ -0,0 +1,87 @@
<script lang="ts">
import Progress from '$lib/Components/Progress.svelte';
import { TriggerCodeModal, formatDate } from '$lib/Utils.js';
import { getModalStore } from '@skeletonlabs/skeleton';
export let data;
const modalStore = getModalStore();
</script>
<div class="flex justify-center flex-col align-middle gap-2">
<div class="card p-4">
<p class="text-2xl text-center">
List of all identities that are inactive but still have access in sources
</p>
</div>
{#await data.reportData}
<div class="grid h-full place-content-center p-8">
<Progress width="w-[100px]" />
</div>
{:then reportData}
{#if reportData.length === 0}
<div class="card p-4">
<p class=" text-center text-success-500">No inactive identities with access found</p>
</div>
{:else}
<div class="table-container">
<table class="table">
<thead class="table-head">
<th> Name </th>
<th> Sources </th>
<th> Created </th>
<th> Modified </th>
<th> Access Count </th>
<th> Entitlement Count </th>
<th> Role Count </th>
<th></th>
</thead>
<tbody class="table-body">
{#each reportData as identity}
<tr>
<td>
{identity.displayName}
</td>
<td>
{identity.accounts?.map((account) => account.source?.name).join(', ')}
</td>
<td>
{formatDate(identity.created)}
</td>
<td>
{formatDate(identity.modified)}
</td>
<td>
{identity.accessCount}
</td>
<td>
{identity.entitlementCount}
</td>
<td>
{identity.roleCount}
</td>
<td>
<div class="flex flex-col justify-center gap-1">
<a
href={`/home/identities/${identity.id}`}
class="btn btn-sm variant-filled-primary text-sm !text-white"
data-sveltekit-preload-data="hover"
>
Open
</a>
<button
on:click={() => TriggerCodeModal(identity, modalStore)}
class="btn btn-sm variant-filled-primary text-sm !text-white"
>
View
</button>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
{/await}
</div>

View File

@@ -0,0 +1,28 @@
import { createConfiguration } from '$lib/sailpoint/sdk.js';
import { getToken } from '$lib/utils/oauth.js';
import { Paginator, SearchApi, type IdentityDocument, type Search } from 'sailpoint-api-client';
export const load = async ({ cookies }) => {
const search: Search = {
indices: ['identities'],
query: {
query: `NOT _exists_:attributes.cloudLifecycleState`
},
sort: ['name']
};
const session = JSON.parse(cookies.get('session')!);
const idnSession = await getToken(cookies);
const config = createConfiguration(session.baseUrl, idnSession.access_token);
const api = new SearchApi(config);
const searchResp = Paginator.paginateSearchApi(api, search, 100, 20000);
const reportData = new Promise<IdentityDocument[]>((resolve) => {
searchResp.then((response) => {
resolve(response.data);
});
});
return { reportData };
};

View File

@@ -0,0 +1,78 @@
<script lang="ts">
import Progress from '$lib/Components/Progress.svelte';
import { TriggerCodeModal, formatDate } from '$lib/Utils';
import { getModalStore } from '@skeletonlabs/skeleton';
export let data;
console.log(data);
const modalStore = getModalStore();
</script>
<div class="flex justify-center flex-col align-middle">
<div class="card p-4">
<p class="text-2xl text-center">
Listing of identities that are missing the cloud life cycle state attribute
</p>
</div>
{#await data.reportData}
<div class="grid h-full place-content-center p-8">
<Progress width="w-[100px]" />
</div>
{:then reportData}
<div class="table-container">
<table class="table">
<thead class="table-head">
<th> Name </th>
<th> Sources </th>
<th> Created </th>
<th> Access Count </th>
<th> Entitlement Count </th>
<th> Role Count </th>
<th></th>
</thead>
<tbody class="table-body">
{#each reportData as identity}
<tr>
<td>
{identity.displayName}
</td>
<td>
{identity.accounts?.map((account) => account.source?.name).join(', ')}
</td>
<td>
{formatDate(identity.created)}
</td>
<td>
{identity.accessCount}
</td>
<td>
{identity.entitlementCount}
</td>
<td>
{identity.roleCount}
</td>
<td>
<div class="flex flex-col justify-center gap-1">
<a
href={`/home/identities/${identity.id}`}
class="btn btn-sm variant-filled-primary text-sm !text-white"
data-sveltekit-preload-data="hover"
>
Open
</a>
<button
on:click={() => TriggerCodeModal(identity, modalStore)}
class="btn btn-sm variant-filled-primary text-sm !text-white"
>
View
</button>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/await}
</div>

View File

@@ -0,0 +1,28 @@
import { createConfiguration } from '$lib/sailpoint/sdk';
import { getSession, getToken } from '$lib/utils/oauth';
import { Paginator, SearchApi, type Search, type EventDocument } from 'sailpoint-api-client';
export const load = async ({ cookies }) => {
const session = await getSession(cookies);
const idnSession = await getToken(cookies);
const config = createConfiguration(session.baseUrl, idnSession.access_token);
const api = new SearchApi(config);
const search: Search = {
indices: ['events'],
query: {
query: `name: "Create Account Failed" AND created: [now-90d TO now]`
},
sort: ['created']
};
const searchResp = Paginator.paginateSearchApi(api, search, 100, 20000);
const errorEvents = new Promise<EventDocument[]>((resolve) => {
searchResp.then((response) => {
resolve(response.data);
});
});
return { errorEvents };
};

View File

@@ -0,0 +1,93 @@
<script lang="ts">
import Progress from '$lib/Components/Progress.svelte';
import { TriggerCodeModal } from '$lib/Utils';
import type { PopupSettings } from '@skeletonlabs/skeleton';
import { getModalStore } from '@skeletonlabs/skeleton';
import alasql from 'alasql';
const modalStore = getModalStore();
export let data;
let report: any;
let reportPromise = new Promise<
{ failure: string; source: string; name: string; exception: string; failures: number }[]
>((resolve, reject) => {
data.errorEvents.then((data) => {
let reportResult = [];
for (let row of data) {
console.log(row);
reportResult.push({
name: row.target?.name,
source: row.attributes?.sourceName,
failure: row.name,
exception: row.attributes?.errors
});
}
console.log(reportResult);
let res = alasql(
'SELECT failure, source, name, exception, count(*) as failures FROM ? GROUP BY failure, source, name, exception',
[reportResult]
);
console.log(res);
resolve(res);
});
});
const popupHover: PopupSettings = {
event: 'hover',
target: 'popupHover',
placement: 'top'
};
</script>
<div class=" flex justify-center flex-col align-middle gap-2">
<div class="card p-4">
<p class="text-2xl text-center">Listing of Source Account Create Errors</p>
</div>
{#await reportPromise}
<div class="grid h-full place-content-center p-8">
<Progress width="w-[100px]" />
</div>
{:then report}
{#if report.length === 0}
<div class="card p-4">
<p class="text-md text-center text-success-500">
No Source Account Create Errors for the last 90 Days
</p>
</div>
{:else}
<div class="table-container">
<table class="table table-interactive">
<thead class="table-head">
<th>Source</th>
<th>Failure</th>
<th>Name</th>
<th>Count</th>
<th>Exception</th>
</thead>
<tbody>
{#each report as row}
<tr on:click={() => TriggerCodeModal(row, modalStore)}>
<td>{row.source}</td>
<td>{row.failure}</td>
<td>{row.name}</td>
<td>{row.failures}</td>
<td class="max-w-36">
<p class="truncate">{row.exception}</p>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
{/await}
</div>

View File

@@ -0,0 +1,150 @@
import { getFilters, getLimit, getPage, getSorters } from '$lib/Utils.js';
import { createConfiguration } from '$lib/sailpoint/sdk.js';
import { getToken } from '$lib/utils/oauth.js';
import {
SearchApi,
SourcesApi,
type EventDocument,
type Search,
type SourcesApiListSourcesRequest,
type Source
} from 'sailpoint-api-client';
export const load = async ({ cookies, url }) => {
const session = JSON.parse(cookies.get('session')!);
const idnSession = await getToken(cookies);
const config = createConfiguration(session.baseUrl, idnSession.access_token);
const sourceApi = new SourcesApi(config);
const searchApi = new SearchApi(config);
const page = getPage(url);
const filters = getFilters(url);
const limit = getLimit(url);
const sorters = getSorters(url);
const requestParams: SourcesApiListSourcesRequest = {
filters,
offset: Number(page) * Number(limit),
limit: Number(limit),
sorters,
count: true
};
const apiResponse = sourceApi.listSources(requestParams);
const sources = new Promise<Source[]>((resolve) => {
apiResponse
.then((response) => {
resolve(response.data);
})
.catch((err) => {
throw err;
});
});
const totalCount = new Promise<number>((resolve) => {
apiResponse
.then((response) => {
resolve(response.headers['x-total-count']);
})
.catch((err) => {
throw err;
});
});
type SourceEvents = {
accounts: { started: EventDocument | undefined; passed: EventDocument | undefined };
entitlements: { started: EventDocument | undefined; passed: EventDocument | undefined };
};
const eventNames: string[] = [
'Aggregate Source Account Passed',
'Aggregate Source Account Started',
'Aggregate Source Entitlement Passed',
'Aggregate Source Entitlement Started'
];
const eventsMap = new Promise<Map<string, SourceEvents>>((resolve) => {
sources.then(async (sources) => {
const sourceEventsMap = new Map<string, SourceEvents>();
for (const source of sources) {
const allEvents: EventDocument[] = [];
const promises: Promise<EventDocument[]>[] = [];
for (const event of eventNames) {
const search: Search = {
indices: ['events'],
query: {
query: `target.name: "${source.name}" AND name:"${event}"`
},
sort: ['created']
};
promises.push(
searchApi
.searchPost({
search
})
.then((response) => {
return response.data;
})
.catch((err) => {
throw err;
})
);
}
await Promise.allSettled(promises).then((results) => {
for (const event of results) {
if (event.status == 'fulfilled' && event.value.length > 0) {
allEvents.push(event.value[0]);
}
}
const sourceEvents: SourceEvents = {
accounts: { started: undefined, passed: undefined },
entitlements: { started: undefined, passed: undefined }
};
for (const event of allEvents) {
if (event.attributes!.sourceName === source.name) {
switch (event.technicalName) {
case 'SOURCE_ACCOUNT_AGGREGATE_STARTED':
if (!sourceEvents.accounts.started) {
sourceEvents.accounts.started = event || undefined;
}
break;
case 'SOURCE_ACCOUNT_AGGREGATE_PASSED':
if (!sourceEvents.accounts.passed) {
sourceEvents.accounts.passed = event || undefined;
}
break;
case 'SOURCE_ENTITLEMENT_AGGREGATE_STARTED':
if (!sourceEvents.entitlements.started) {
sourceEvents.entitlements.started = event || undefined;
}
break;
case 'SOURCE_ENTITLEMENT_AGGREGATE_PASSED':
if (!sourceEvents.entitlements.passed) {
sourceEvents.entitlements.passed = event || undefined;
}
break;
default:
break;
}
}
}
sourceEventsMap.set(source.name, sourceEvents);
});
}
resolve(sourceEventsMap);
});
});
return { sources, eventsMap, totalCount, params: { page, limit, filters, sorters } };
};

View File

@@ -0,0 +1,117 @@
<script lang="ts">
import Progress from '$lib/Components/Progress.svelte';
import { TriggerCodeModal, formatDate } from '$lib/Utils.js';
import { getModalStore } from '@skeletonlabs/skeleton';
const modalStore = getModalStore();
export let data;
console.log(data);
</script>
<div class="flex justify-center flex-col align-middle gap-2">
<div class="card p-4">
<p class="text-2xl text-center">List of sources and their most recent aggregation events</p>
</div>
{#await data.sources}
<div class="card grid h-full place-content-center p-8">
<Progress width="w-[100px]" />
</div>
{:then sources}
<div class="table-container">
<table class="table">
<thead>
<th>Source Name</th>
<th>Type</th>
<th>Authoritative</th>
<th>Account Aggregations</th>
<th>Entitlement Aggregations</th>
</thead>
<tbody>
{#each sources as source}
<tr>
<td>{source.name}</td>
<td>{source.type}</td>
<td
class="font-bold"
class:text-tertiary-500={source.authoritative}
class:text-warning-500={!source.authoritative}
>
{source.authoritative ? 'True' : 'False'}
</td>
{#await data.eventsMap}
<td>
<div class="grid place-content-center">
<Progress width="w-[80px]" />
</div>
</td>
{:then eventsMap}
<td>
<div class="flex flex-col gap-2">
<button
disabled={!eventsMap.get(source.name)?.accounts.started}
class="btn btn-sm variant-filled-primary text-sm !text-white"
on:click={() =>
eventsMap.get(source.name)?.accounts.started &&
TriggerCodeModal(eventsMap.get(source.name)?.accounts.started, modalStore)}
>
Started: {formatDate(eventsMap.get(source.name)?.accounts.started?.created)}
</button>
<button
class="btn btn-sm variant-filled"
disabled={!eventsMap.get(source.name)?.accounts.passed}
on:click={() =>
eventsMap.get(source.name)?.accounts.passed &&
TriggerCodeModal(eventsMap.get(source.name)?.accounts.passed, modalStore)}
>
Passed: {formatDate(eventsMap.get(source.name)?.accounts.passed?.created)}
</button>
</div>
</td>
{/await}
{#await data.eventsMap}
<td>
<div class="grid place-content-center">
<Progress width="w-[80px]" />
</div>
</td>
{:then eventsMap}
<td>
<div class="flex flex-col gap-2">
<button
class="btn btn-sm variant-filled-primary text-sm !text-white"
disabled={!eventsMap.get(source.name)?.entitlements.started}
on:click={() =>
eventsMap.get(source.name)?.entitlements.started &&
TriggerCodeModal(
eventsMap.get(source.name)?.entitlements.started,
modalStore
)}
>
Started: {formatDate(
eventsMap.get(source.name)?.entitlements.started?.created
)}
</button>
<button
class="btn btn-sm variant-filled"
disabled={!eventsMap.get(source.name)?.entitlements.passed}
on:click={() =>
eventsMap.get(source.name)?.entitlements.passed &&
TriggerCodeModal(
eventsMap.get(source.name)?.entitlements.passed,
modalStore
)}
>
Passed: {formatDate(eventsMap.get(source.name)?.entitlements.passed?.created)}
</button>
</div>
</td>
{/await}
</tr>
{/each}
</tbody>
</table>
</div>
{/await}
</div>

View File

@@ -0,0 +1,49 @@
import { getFilters, getLimit, getSorters, getPage } from '$lib/Utils.js';
import { createConfiguration } from '$lib/sailpoint/sdk.js';
import { getToken } from '$lib/utils/oauth.js';
import { SourcesApi, type Source, type SourcesApiListSourcesRequest } from 'sailpoint-api-client';
export const load = async ({ cookies, url }) => {
const session = JSON.parse(cookies.get('session')!);
const idnSession = await getToken(cookies);
const config = createConfiguration(session.baseUrl, idnSession.access_token);
const api = new SourcesApi(config);
const page = getPage(url);
const filters = getFilters(url);
const limit = getLimit(url);
const sorters = getSorters(url);
const requestParams: SourcesApiListSourcesRequest = {
filters,
offset: Number(page) * Number(limit),
limit: Number(limit),
sorters,
count: true
};
const apiResponse = api.listSources(requestParams);
const sources = new Promise<Source[]>((resolve) => {
apiResponse
.then((response) => {
resolve(response.data);
})
.catch((err) => {
throw err;
});
});
const totalCount = new Promise<number>((resolve) => {
apiResponse
.then((response) => {
resolve(response.headers['x-total-count']);
})
.catch((err) => {
throw err;
});
});
return { sources, totalCount, params: { page, limit, filters, sorters } };
};

View File

@@ -0,0 +1,60 @@
<script lang="ts">
import Progress from '$lib/Components/Progress.svelte';
import { TriggerCodeModal, formatDate } from '$lib/Utils';
import { getModalStore } from '@skeletonlabs/skeleton';
export let data;
console.log(data);
const modalStore = getModalStore();
</script>
<div class="flex justify-center flex-col align-middle gap-2">
<div class="card p-4">
<p class="text-2xl text-center">Listing of sources and their configured owners</p>
</div>
{#await data.sources}
<div class="grid h-full place-content-center p-8">
<Progress width="w-[100px]" />
</div>
{:then sources}
<div class="table-container">
<table class="table">
<thead class="table-head">
<th> Source Name </th>
<th> Type </th>
<th> Modified </th>
<th> Created </th>
<th> Owner </th>
<th />
</thead>
<tbody>
{#each sources as source}
<tr>
<td>{source.name}</td>
<td>{source.type}</td>
<td>{formatDate(source.modified)}</td>
<td>{formatDate(source.created)}</td>
<td>{source.owner.name}</td>
<td class="flex flex-col justify-center gap-1">
<a
href={`/home/sources/${source.id}`}
class="btn variant-filled-primary text-sm !text-white"
data-sveltekit-preload-data="hover"
>
Open Source
</a>
<button
on:click={() => TriggerCodeModal(source, modalStore)}
class="btn variant-filled-primary text-sm !text-white"
>
View
</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/await}
</div>

View File

@@ -0,0 +1,49 @@
import { getFilters, getLimit, getSorters, getPage } from '$lib/Utils.js';
import { createConfiguration } from '$lib/sailpoint/sdk.js';
import { getToken } from '$lib/utils/oauth.js';
import { SourcesApi, type Source, type SourcesApiListSourcesRequest } from 'sailpoint-api-client';
export const load = async ({ cookies, url }) => {
const session = JSON.parse(cookies.get('session')!);
const idnSession = await getToken(cookies);
const config = createConfiguration(session.baseUrl, idnSession.access_token);
const api = new SourcesApi(config);
const page = getPage(url);
const filters = getFilters(url);
const limit = getLimit(url);
const sorters = getSorters(url);
const requestParams: SourcesApiListSourcesRequest = {
filters,
offset: Number(page) * Number(limit),
limit: Number(limit),
sorters,
count: true
};
const apiResponse = api.listSources(requestParams);
const sources = new Promise<Source[]>((resolve) => {
apiResponse
.then((response) => {
resolve(response.data);
})
.catch((err) => {
throw err;
});
});
const totalCount = new Promise<number>((resolve) => {
apiResponse
.then((response) => {
resolve(response.headers['x-total-count']);
})
.catch((err) => {
throw err;
});
});
return { sources, totalCount, params: { page, limit, filters, sorters } };
};

View File

@@ -0,0 +1,135 @@
<script lang="ts">
import Paginator from '$lib/Components/Paginator.svelte';
import Progress from '$lib/Components/Progress.svelte';
import {
TriggerCodeModal,
createOnAmountChange,
createOnGo,
createOnPageChange
} from '$lib/Utils.js';
import type { ModalSettings, PaginationSettings } from '@skeletonlabs/skeleton';
import { getModalStore } from '@skeletonlabs/skeleton';
const modalStore = getModalStore();
export let data;
$: onPageChange = createOnPageChange({ ...data.params, filters, sorters }, '/home/identities');
$: onAmountChange = createOnAmountChange(
{ ...data.params, filters, sorters },
'/home/identities'
);
$: onGo = createOnGo({ ...data.params, filters, sorters }, '/home/identities');
let filters = '';
let sorters = '';
</script>
<div class="card flex flex-col justify-center h-full">
{#await data.totalCount then totalCount}
{#if totalCount > 250 || Number(data.params.limit) < totalCount}
<Paginator
{onAmountChange}
{onGo}
{onPageChange}
settings={{
page: Number(data.params.page),
limit: Number(data.params.limit),
size: totalCount,
amounts: [10, 50, 100, 250]
}}
{filters}
{sorters}
{totalCount}
/>
{/if}
{/await}
{#await data.sources}
<div class="grid h-full place-content-center p-8">
<Progress width="w-[100px]" />
</div>
{:then sources}
<div class="table-container">
<table class="table">
<thead class="table-head">
<th>ID</th>
<th>Name</th>
<th>Description</th>
<th>Type</th>
<th>Authoritative</th>
<th>Healthy</th>
<th>Delete Threshold</th>
<th>Owner</th>
<th />
</thead>
<tbody class="table-body">
{#each sources as source}
<tr>
<td>
<p class="text-center">{source.id}</p>
</td>
<td>
<p class="text-center">{source.name}</p>
</td>
<td>
<p class="text-center">{source.description}</p>
</td>
<td>
<p class="text-center">{source.type}</p>
</td>
<td>
<p class="text-center">{source.authoritative ? 'True' : 'False'}</p>
</td>
<td>
<p
class="text-center font-bold {source.healthy ? 'text-green-500' : 'text-red-500'}"
>
{source.healthy ? 'True' : 'False'}
</p>
</td>
<td>
<p class="text-center">{source.deleteThreshold}</p>
</td>
<td>
<p class="text-center">{source.owner.name}</p>
</td>
<td class="flex flex-col justify-center gap-1">
<a
href={`/home/sources/${source.id}`}
class="btn btn-sm variant-filled-primary text-sm !text-white"
data-sveltekit-preload-data="hover"
>
Open
</a>
<button
on:click={() => TriggerCodeModal(source, modalStore)}
class="btn btn-sm variant-filled-primary text-sm !text-white"
>
View
</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/await}
{#await data.totalCount then totalCount}
{#if totalCount > 250 || Number(data.params.limit) < totalCount}
<Paginator
{onAmountChange}
{onGo}
{onPageChange}
settings={{
page: Number(data.params.page),
limit: Number(data.params.limit),
size: totalCount,
amounts: [10, 50, 100, 250]
}}
{filters}
{sorters}
{totalCount}
/>
{/if}
{/await}
</div>

View File

@@ -0,0 +1,104 @@
import { createConfiguration } from '$lib/sailpoint/sdk.js';
import { getToken } from '$lib/utils/oauth.js';
import { SearchApi, SourcesApi, type EventDocument, type Search } from 'sailpoint-api-client';
export const load = async ({ cookies, params }) => {
const session = JSON.parse(cookies.get('session')!);
const idnSession = await getToken(cookies);
const config = createConfiguration(session.baseUrl, idnSession.access_token);
const sourceApi = new SourcesApi(config);
const searchApi = new SearchApi(config);
const sourceResp = await sourceApi.getSource({ id: params.sourceID });
const source = sourceResp.data;
type SourceEvents = {
accounts: { started: EventDocument | undefined; passed: EventDocument | undefined };
entitlements: { started: EventDocument | undefined; passed: EventDocument | undefined };
};
const sourceEvents = new Promise<SourceEvents>((resolve) => {
const eventNames: string[] = [
'Aggregate Source Account Passed',
'Aggregate Source Account Started',
'Aggregate Source Entitlement Passed',
'Aggregate Source Entitlement Started'
];
const allEvents: EventDocument[] = [];
const promises: Promise<EventDocument[]>[] = [];
for (const event of eventNames) {
const search: Search = {
indices: ['events'],
query: {
query: `target.name: "${source.name}" AND name:"${event}"`
},
sort: ['created']
};
promises.push(
searchApi
.searchPost({
search
})
.then((response) => {
return response.data;
})
.catch((err) => {
throw err;
})
);
}
Promise.allSettled(promises).then((results) => {
for (const event of results) {
if (event.status == 'fulfilled' && event.value.length > 0) {
allEvents.push(event.value[0]);
}
}
const sourceEvents: SourceEvents = {
accounts: { started: undefined, passed: undefined },
entitlements: { started: undefined, passed: undefined }
};
for (const event of allEvents) {
if (event.attributes!.sourceName === source.name) {
switch (event.technicalName) {
case 'SOURCE_ACCOUNT_AGGREGATE_STARTED':
if (!sourceEvents.accounts.started) {
sourceEvents.accounts.started = event || undefined;
}
break;
case 'SOURCE_ACCOUNT_AGGREGATE_PASSED':
if (!sourceEvents.accounts.passed) {
sourceEvents.accounts.passed = event || undefined;
}
break;
case 'SOURCE_ENTITLEMENT_AGGREGATE_STARTED':
if (!sourceEvents.entitlements.started) {
sourceEvents.entitlements.started = event || undefined;
}
break;
case 'SOURCE_ENTITLEMENT_AGGREGATE_PASSED':
if (!sourceEvents.entitlements.passed) {
sourceEvents.entitlements.passed = event || undefined;
}
break;
default:
break;
}
}
}
resolve(sourceEvents);
});
});
return { source, sourceEvents };
};

View File

@@ -0,0 +1,121 @@
<script lang="ts">
import VaCluster from '$lib/Components/VACluster.svelte';
import { formatDate } from '$lib/Utils.js';
import { Accordion, AccordionItem, CodeBlock, Tab, TabGroup } from '@skeletonlabs/skeleton';
import Progress from '$lib/Components/Progress.svelte';
export let data;
console.log(data);
let tabSet: number = 1;
</script>
<div class="flex flex-col gap-2">
<div class="card p-4">
<h1 class="text-2xl font-bold">{data.source.name}</h1>
<p class="">{data.source.description}</p>
<p class="">ID: {data.source.id}</p>
<p class="">Type: {data.source.type}</p>
<p class="">
Authoritative: {data.source.authoritative ? 'True' : 'False'}
</p>
<p>
Healthy:
<span class={data.source.healthy ? 'text-green-500' : 'text-red-500'}>
{data.source.healthy ? 'True' : 'False'}
</span>
</p>
</div>
<div class="card p-4">
<VaCluster cluster={data.source.cluster} />
</div>
{#await data.sourceEvents}
<div class="card grid h-full place-content-center p-8">
<Progress width="w-[100px]" />
</div>
{:then sourceEvents}
<div class="card p-4">
<h2>Most Recent Aggregations</h2>
<div>
<strong>Accounts:</strong>
<Accordion>
<AccordionItem>
<svelte:fragment slot="summary">
Started: {formatDate(sourceEvents.accounts.started?.created)}
</svelte:fragment>
<svelte:fragment slot="content">
<CodeBlock
lineNumbers
language="json"
code={JSON.stringify(sourceEvents.accounts.started, null, 4)}
/>
</svelte:fragment>
</AccordionItem>
<AccordionItem>
<svelte:fragment slot="summary">
Passed: {formatDate(sourceEvents.accounts.passed?.created)}
</svelte:fragment>
<svelte:fragment slot="content">
<CodeBlock
lineNumbers
language="json"
code={JSON.stringify(sourceEvents.accounts.passed, null, 4)}
/>
</svelte:fragment>
</AccordionItem>
</Accordion>
<strong>Entitlements</strong>
<Accordion>
<AccordionItem>
<svelte:fragment slot="summary">
Started: {formatDate(sourceEvents.entitlements.started?.created)}
</svelte:fragment>
<svelte:fragment slot="content">
<div>
<CodeBlock
lineNumbers
language="json"
code={JSON.stringify(sourceEvents.entitlements.started, null, 4)}
/>
</div>
</svelte:fragment>
</AccordionItem>
<AccordionItem>
<svelte:fragment slot="summary">
Passed: {formatDate(sourceEvents.entitlements.passed?.created)}
</svelte:fragment>
<svelte:fragment slot="content">
<CodeBlock
lineNumbers
language="json"
code={JSON.stringify(sourceEvents.entitlements.passed, null, 4)}
/>
</svelte:fragment>
</AccordionItem>
</Accordion>
</div>
</div>
{/await}
<div class="card p-4">
<TabGroup>
<!-- <Tab bind:group={tabSet} name="raw-source-values" value={0}>Source Events</Tab> -->
<Tab bind:group={tabSet} name="tab2" value={1}>Connector Attributes JSON</Tab>
<Tab bind:group={tabSet} name="raw-source-values" value={2}>Full Source JSON</Tab>
<!-- Tab Panels --->
<svelte:fragment slot="panel">
{#if tabSet === 0}
<!-- SOURCE EVENTS -->
{:else if tabSet === 1}
<CodeBlock
lineNumbers
language="json"
code={JSON.stringify(data.source.connectorAttributes, null, 4)}
/>
{:else if tabSet === 2}
<CodeBlock lineNumbers language="json" code={JSON.stringify(data.source, null, 4)} />
{/if}
</svelte:fragment>
</TabGroup>
</div>
</div>

View File

@@ -0,0 +1,15 @@
export const load = async ({ cookies }) => {
cookies.delete('session', {
path: '/',
httpOnly: false,
secure: false
});
cookies.delete('idnSession', {
path: '/',
httpOnly: false,
secure: false
});
return { sessionLoggedOut: true };
};

View File

@@ -0,0 +1,32 @@
<script lang="ts">
import { goto, invalidateAll } from '$app/navigation';
import { onMount } from 'svelte';
export let data;
onMount(() => {
setTimeout(async () => {
console.log('Redirecting to login...');
goto('/');
}, 2000);
});
</script>
<div class="p-4">
<div class="card p-4">
{#if data.sessionLoggedOut}
<p class="text-center p-2">Successfully Logged out</p>
{:else}
<p class="text-center p-2">
WHOOPS! an error occurred. <br /> If you believe this is a bug please submit an issue on
<a
class="underline text-blue-500 hover:text-blue-700"
href="https://github.com/sailpoint-oss/idn-admin-console/issues/new/choose"
rel="noreferrer"
target="_blank"
>
GitHub
</a>
</p>
{/if}
</div>
</div>