mirror of
https://github.com/LukeHagar/ui-development-kit.git
synced 2025-12-09 12:57:44 +00:00
Attempting to correct previous issues with monorepo structure
This commit is contained in:
28
Sveltekit-App/src/app.d.ts
vendored
Normal file
28
Sveltekit-App/src/app.d.ts
vendored
Normal 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 {}
|
||||
}
|
||||
}
|
||||
13
Sveltekit-App/src/app.html
Normal file
13
Sveltekit-App/src/app.html
Normal 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>
|
||||
18
Sveltekit-App/src/app.postcss
Normal file
18
Sveltekit-App/src/app.postcss
Normal 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;
|
||||
}
|
||||
20
Sveltekit-App/src/error.html
Normal file
20
Sveltekit-App/src/error.html
Normal 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>
|
||||
48
Sveltekit-App/src/hooks.server.ts
Normal file
48
Sveltekit-App/src/hooks.server.ts
Normal 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;
|
||||
};
|
||||
110
Sveltekit-App/src/lib/Components/AnimatedCounter.svelte
Normal file
110
Sveltekit-App/src/lib/Components/AnimatedCounter.svelte
Normal 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>
|
||||
13
Sveltekit-App/src/lib/Components/CodeBlockModal.svelte
Normal file
13
Sveltekit-App/src/lib/Components/CodeBlockModal.svelte
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
53
Sveltekit-App/src/lib/Components/HomepageCards/links.ts
Normal file
53
Sveltekit-App/src/lib/Components/HomepageCards/links.ts
Normal 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/' }
|
||||
];
|
||||
43
Sveltekit-App/src/lib/Components/Paginator.svelte
Normal file
43
Sveltekit-App/src/lib/Components/Paginator.svelte
Normal 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>
|
||||
14
Sveltekit-App/src/lib/Components/Progress.svelte
Normal file
14
Sveltekit-App/src/lib/Components/Progress.svelte
Normal 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>
|
||||
49
Sveltekit-App/src/lib/Components/SVGs/HamburgerSVG.svelte
Normal file
49
Sveltekit-App/src/lib/Components/SVGs/HamburgerSVG.svelte
Normal 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 |
14
Sveltekit-App/src/lib/Components/SVGs/HomeSVG.svelte
Normal file
14
Sveltekit-App/src/lib/Components/SVGs/HomeSVG.svelte
Normal 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 |
14
Sveltekit-App/src/lib/Components/SVGs/IdentitiesSVG.svelte
Normal file
14
Sveltekit-App/src/lib/Components/SVGs/IdentitiesSVG.svelte
Normal 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 |
14
Sveltekit-App/src/lib/Components/SVGs/MessagesSVG.svelte
Normal file
14
Sveltekit-App/src/lib/Components/SVGs/MessagesSVG.svelte
Normal 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 |
14
Sveltekit-App/src/lib/Components/SVGs/ReportsSVG.svelte
Normal file
14
Sveltekit-App/src/lib/Components/SVGs/ReportsSVG.svelte
Normal 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 |
14
Sveltekit-App/src/lib/Components/SVGs/SourcesSVG.svelte
Normal file
14
Sveltekit-App/src/lib/Components/SVGs/SourcesSVG.svelte
Normal 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 |
77
Sveltekit-App/src/lib/Components/VACluster.svelte
Normal file
77
Sveltekit-App/src/lib/Components/VACluster.svelte
Normal 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}
|
||||
106
Sveltekit-App/src/lib/Utils.ts
Normal file
106
Sveltekit-App/src/lib/Utils.ts
Normal 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('');
|
||||
}
|
||||
30
Sveltekit-App/src/lib/reports.ts
Normal file
30
Sveltekit-App/src/lib/reports.ts
Normal 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'
|
||||
}
|
||||
];
|
||||
6
Sveltekit-App/src/lib/sailpoint/sdk.ts
Normal file
6
Sveltekit-App/src/lib/sailpoint/sdk.ts
Normal 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;
|
||||
}
|
||||
29
Sveltekit-App/src/lib/sidebar/Sidebar.svelte
Normal file
29
Sveltekit-App/src/lib/sidebar/Sidebar.svelte
Normal 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>
|
||||
15
Sveltekit-App/src/lib/sidebar/SidebarDrawer.svelte
Normal file
15
Sveltekit-App/src/lib/sidebar/SidebarDrawer.svelte
Normal 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>
|
||||
43
Sveltekit-App/src/lib/sidebar/navigation.ts
Normal file
43
Sveltekit-App/src/lib/sidebar/navigation.ts
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
211
Sveltekit-App/src/lib/utils/oauth.ts
Normal file
211
Sveltekit-App/src/lib/utils/oauth.ts
Normal 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.
|
||||
}
|
||||
}
|
||||
55
Sveltekit-App/src/routes/+error.svelte
Normal file
55
Sveltekit-App/src/routes/+error.svelte
Normal 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>
|
||||
3
Sveltekit-App/src/routes/+layout.server.ts
Normal file
3
Sveltekit-App/src/routes/+layout.server.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const load = async ({ locals }) => {
|
||||
return { tokenDetails: locals.tokenDetails };
|
||||
};
|
||||
206
Sveltekit-App/src/routes/+layout.svelte
Normal file
206
Sveltekit-App/src/routes/+layout.svelte
Normal 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>›</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>
|
||||
51
Sveltekit-App/src/routes/+page.server.ts
Normal file
51
Sveltekit-App/src/routes/+page.server.ts
Normal 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 {};
|
||||
};
|
||||
77
Sveltekit-App/src/routes/+page.svelte
Normal file
77
Sveltekit-App/src/routes/+page.svelte
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
44
Sveltekit-App/src/routes/callback/+page.server.ts
Normal file
44
Sveltekit-App/src/routes/callback/+page.server.ts
Normal 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 };
|
||||
};
|
||||
37
Sveltekit-App/src/routes/callback/+page.svelte
Normal file
37
Sveltekit-App/src/routes/callback/+page.svelte
Normal 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>
|
||||
237
Sveltekit-App/src/routes/callback/loadinglist.ts
Normal file
237
Sveltekit-App/src/routes/callback/loadinglist.ts
Normal 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());
|
||||
5
Sveltekit-App/src/routes/home/+page.server.ts
Normal file
5
Sveltekit-App/src/routes/home/+page.server.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const load = async ({ locals }) => {
|
||||
if (!locals.hasSession) return { baseUrl: '', tenantUrl: '' };
|
||||
|
||||
return { session: locals.session };
|
||||
};
|
||||
18
Sveltekit-App/src/routes/home/+page.svelte
Normal file
18
Sveltekit-App/src/routes/home/+page.svelte
Normal 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>
|
||||
19
Sveltekit-App/src/routes/home/courier/+page.server.ts
Normal file
19
Sveltekit-App/src/routes/home/courier/+page.server.ts
Normal 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 };
|
||||
};
|
||||
160
Sveltekit-App/src/routes/home/courier/+page.svelte
Normal file
160
Sveltekit-App/src/routes/home/courier/+page.svelte
Normal 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>
|
||||
62
Sveltekit-App/src/routes/home/identities/+page.server.ts
Normal file
62
Sveltekit-App/src/routes/home/identities/+page.server.ts
Normal 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 }
|
||||
};
|
||||
};
|
||||
125
Sveltekit-App/src/routes/home/identities/+page.svelte
Normal file
125
Sveltekit-App/src/routes/home/identities/+page.svelte
Normal 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>
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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>
|
||||
29
Sveltekit-App/src/routes/home/reports/+page.svelte
Normal file
29
Sveltekit-App/src/routes/home/reports/+page.svelte
Normal 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>
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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 } };
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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 } };
|
||||
};
|
||||
@@ -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>
|
||||
49
Sveltekit-App/src/routes/home/sources/+page.server.ts
Normal file
49
Sveltekit-App/src/routes/home/sources/+page.server.ts
Normal 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 } };
|
||||
};
|
||||
135
Sveltekit-App/src/routes/home/sources/+page.svelte
Normal file
135
Sveltekit-App/src/routes/home/sources/+page.svelte
Normal 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>
|
||||
104
Sveltekit-App/src/routes/home/sources/[sourceID]/+page.server.ts
Normal file
104
Sveltekit-App/src/routes/home/sources/[sourceID]/+page.server.ts
Normal 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 };
|
||||
};
|
||||
121
Sveltekit-App/src/routes/home/sources/[sourceID]/+page.svelte
Normal file
121
Sveltekit-App/src/routes/home/sources/[sourceID]/+page.svelte
Normal 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>
|
||||
15
Sveltekit-App/src/routes/logout/+page.server.ts
Normal file
15
Sveltekit-App/src/routes/logout/+page.server.ts
Normal 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 };
|
||||
};
|
||||
32
Sveltekit-App/src/routes/logout/+page.svelte
Normal file
32
Sveltekit-App/src/routes/logout/+page.svelte
Normal 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>
|
||||
Reference in New Issue
Block a user