Partial rewrite save, testing action

This commit is contained in:
Luke Hagar
2023-05-17 08:47:20 -05:00
parent 5ec4f2c12c
commit bc4b9f6993
30 changed files with 660593 additions and 152 deletions

75
.github/workflows/build.yaml vendored Normal file
View File

@@ -0,0 +1,75 @@
name: Build and Upload Chrome Extension
run-name: ${{ github.actor }} is Building and Uploading a new Anchor Version 🚀
on: [push, workflow_dispatch]
jobs:
Build-And-Push:
runs-on: ubuntu-latest
steps:
# Checkout the main branch of this repo
- name: Checkout PR branch
uses: actions/checkout@v3
with:
repository: lukehagar/anchor
path: anchor
ref: main
# Checkout the main branch of api-specs
- name: Checkout API Specs Repo
uses: actions/checkout@v3
with:
repository: sailpoint-oss/api-specs
path: api-specs
ref: main
- name: Set up Node
uses: actions/setup-node@v3
with:
node-version: '16'
- name: Install swagger-cli
run: |
npm install -g swagger-cli
- name: Dereference and Bundle Beta API Specification
id: buildBeta
run: |
swagger-cli bundle --dereference api-specs/idn/sailpoint-api.beta.yaml -t json -o anchor/src/routes/api-client/BetaSpec.json
- name: Dereference and Bundle V3 API Specification
id: buildV3
if: steps.buildBeta.outcome == 'success'
run: |
swagger-cli bundle --dereference api-specs/idn/sailpoint-api.v3.yaml -t json -o anchor/src/routes/api-client/V3Spec.json
- name: Dereference and Bundle V2 API Specification
id: buildV2
if: steps.buildV3.outcome == 'success'
run: |
swagger-cli bundle --dereference api-specs/idn/sailpoint-api.v2.yaml -t json -o anchor/src/routes/api-client/V2Spec.json
- name: Dereference and Bundle CC API Specification
id: buildCC
if: steps.buildV2.outcome == 'success'
run: |
swagger-cli bundle --dereference api-specs/idn/sailpoint-api.cc.yaml -t json -o anchor/src/routes/api-client/CCSpec.json
- name: Install Dependencies
id: installDeps
if: steps.buildCC.outcome == 'success'
run: |
cd anchor
pnpm install
- name: Build Extension
id: buildExtension
if: steps.installDeps.outcome == 'success'
run: |
cd anchor
pnpm build
- name: Archive chrome-extension artifact
uses: actions/upload-artifact@v2
with:
name: anchor-${{ github.sha }}
path: anchor-${{ github.event.pull_request.head.sha }}

1
.npmrc
View File

@@ -1,2 +1,3 @@
engine-strict=true engine-strict=true
resolution-mode=highest resolution-mode=highest
enable-pre-post-scripts=true

View File

@@ -1,6 +1,7 @@
{ {
"prettier.documentSelectors": ["**/*.svelte"], "prettier.documentSelectors": ["**/*.svelte"],
"tailwindCSS.classAttributes": [ "tailwindCSS.classAttributes": [
"classes",
"class", "class",
"accent", "accent",
"active", "active",

View File

@@ -4,6 +4,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"prebuild": "curl https://raw.githubusercontent.com/sailpoint-oss/api-specs/main/dereferenced/deref-sailpoint-api.v3.yaml -o src/lib/V3Spec.yaml && curl https://raw.githubusercontent.com/sailpoint-oss/api-specs/main/dereferenced/deref-sailpoint-api.beta.yaml -o src/lib/BetaSpec.yaml && yq -o=json eval src/lib/V3Spec.yaml > src/routes/api-client/V3Spec.json && yq -o=json eval src/lib/BetaSpec.yaml > src/routes/api-client/BetaSpec.json",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
@@ -12,7 +13,9 @@
"format": "prettier --plugin-search-dir . --write ." "format": "prettier --plugin-search-dir . --write ."
}, },
"devDependencies": { "devDependencies": {
"@floating-ui/dom": "^1.2.7", "@apidevtools/swagger-parser": "^10.1.0",
"@floating-ui/dom": "^1.2.8",
"@modyfi/vite-plugin-yaml": "^1.0.4",
"@skeletonlabs/skeleton": "^1.5.1", "@skeletonlabs/skeleton": "^1.5.1",
"@sveltejs/adapter-auto": "^2.0.0", "@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/adapter-static": "^2.0.2", "@sveltejs/adapter-static": "^2.0.2",
@@ -22,24 +25,28 @@
"@types/chrome": "^0.0.235", "@types/chrome": "^0.0.235",
"@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0", "@typescript-eslint/parser": "^5.45.0",
"algoliasearch": "^4.17.0",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"axios": "^1.4.0",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"eslint": "^8.28.0", "eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte": "^2.26.0", "eslint-plugin-svelte": "^2.26.0",
"highlight.js": "^11.8.0", "highlight.js": "^11.8.0",
"openapi-types": "^12.1.0",
"postcss": "^8.4.23", "postcss": "^8.4.23",
"prettier": "^2.8.0", "prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.8.1", "prettier-plugin-svelte": "^2.8.1",
"sailpoint-api-client": "^1.0.4", "sailpoint-api-client": "^1.0.4",
"svelte": "^3.54.0", "svelte": "^3.54.0",
"svelte-algolia-instantsearch": "^0.7.0",
"svelte-check": "^3.0.1", "svelte-check": "^3.0.1",
"svelte-jsoneditor": "^0.17.3",
"sveltekit-adapter-chrome-extension": "^2.0.0", "sveltekit-adapter-chrome-extension": "^2.0.0",
"tailwindcss": "^3.3.2", "tailwindcss": "^3.3.2",
"tslib": "^2.4.1", "tslib": "^2.4.1",
"typescript": "^5.0.0", "typescript": "^5.0.0",
"vite": "^4.3.0" "vite": "^4.3.0",
"yaml": "^2.2.2"
}, },
"type": "module" "type": "module"
} }

713
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

169
src/app.d.ts vendored
View File

@@ -8,20 +8,157 @@ declare namespace App {
// interface Platform {} // interface Platform {}
} }
declare type IdnSession = { declare type Status = {
tenant?: string; page: Page;
authType?: string; status: Status2;
baseUrl?: string; };
logoutUrl?: string;
accessToken?: string; declare type Page = {
refreshIn?: number | string; id: string;
pollUrl?: string; name: string;
strongAuth?: boolean | string; url: string;
strongAuthUrl?: string; time_zone: string;
csrfToken?: string; updated_at: string;
expiration?: date; };
org?: string;
region?: string; declare type Status2 = {
pod?: string; indicator: string;
layer?: string; description: string;
};
declare type IdnSession = {
tenant: string;
authType: string;
baseUrl: string;
logoutUrl: string;
accessToken: string;
refreshIn: number;
pollUrl: string;
strongAuth: boolean;
strongAuthUrl: string;
csrfToken: string;
expiration: date;
};
declare type HostingData = {
org: string;
pod: string;
publicPod: string;
layer: string;
region: string;
};
declare type TenantData = {
id: string;
alias: string;
uid: string;
name: string;
displayName: string;
uuid: string;
encryptionKey: null;
encryptionCheck: null;
status: string;
pending: boolean;
passwordResetSinceLastLogin: boolean;
usageCertAttested: null;
userFlags: Meta;
enabled: boolean;
altAuthVia: string;
altAuthViaIntegrationData: null;
kbaAnswers: number;
disablePasswordReset: boolean;
ptaSourceId: null;
supportsPasswordPush: boolean;
attributes: Attributes;
externalId: string;
role: string[];
phone: null;
email: string;
personalEmail: null;
employeeNumber: null;
riskScore: number;
featureFlags: { [key: string]: boolean };
feature: string[];
orgEncryptionKey: string;
orgEncryptionKeyId: string;
meta: any;
org: Org;
stepUpAuth: boolean;
bxInstallPrompted: boolean;
federatedLogin: boolean;
auth: Auth;
onNetwork: boolean;
onTrustedGeo: boolean;
loginUrl: string;
};
declare type Attributes = {
lastLoginTimestamp: number;
uid: string;
firstname: string;
cloudAuthoritativeSource: string;
cloudStatus: string;
displayName: string;
internalCloudStatus: string;
lastSyncDate: string;
workPhone: string;
email: string;
lastname: string;
};
declare type Auth = {
service: string;
encryption: string;
};
declare type Org = {
name: string;
scriptName: string;
mode: string;
numQuestions: number;
status: string;
maxRegisteredUsers: number;
pod: string;
pwdResetPersonalPhone: boolean;
pwdResetPersonalEmail: boolean;
pwdResetKba: boolean;
pwdResetEmail: boolean;
pwdResetDuo: boolean;
pwdResetPhoneMask: boolean;
authErrorText: null;
strongAuthKba: boolean;
strongAuthPersonalPhone: boolean;
strongAuthPersonalEmail: boolean;
integrations: any[];
productName: string;
kbaReqForAuthn: number;
kbaReqAnswers: number;
lockoutAttemptThreshold: number;
lockoutTimeMinutes: number;
usageCertRequired: boolean;
adminStrongAuthRequired: boolean;
enableExternalPasswordChange: boolean;
enablePasswordReplay: boolean;
enableAutomaticPasswordReplay: boolean;
notifyAuthenticationSettingChange: boolean;
netmasks: null;
countryCodes: null;
whiteList: boolean;
usernameEmptyText: null;
usernameLabel: null;
enableAutomationGeneration: boolean;
emailTestMode: boolean;
emailTestAddress: string;
orgType: string;
passwordReplayState: string;
systemNotificationConfig: string;
maxClusterDebugHours: string;
brandName: string;
logo: null;
emailFromAddress: string;
standardLogoUrl: null;
narrowLogoUrl: null;
actionButtonColor: string;
activeLinkColor: string;
navigationColor: string;
}; };

View File

@@ -3,3 +3,7 @@ html,
body { body {
@apply h-[600px] w-[800px]; @apply h-[600px] w-[800px];
} }
.ais-Hits-list {
@apply flex flex-col gap-1;
}

161443
src/lib/BetaSpec.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
fill="#000000"
height="800px"
width="800px"
version="1.1"
id="Capa_1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 489.425 489.425"
xml:space="preserve"
>
<g>
<g>
<path
d="M122.825,394.663c17.8,19.4,43.2,30.6,69.5,30.6h216.9c44.2,0,80.2-36,80.2-80.2v-200.7c0-44.2-36-80.2-80.2-80.2h-216.9
c-26.4,0-51.7,11.1-69.5,30.6l-111.8,121.7c-14.7,16.1-14.7,40.3,0,56.4L122.825,394.663z M29.125,233.063l111.8-121.8
c13.2-14.4,32-22.6,51.5-22.6h216.9c30.7,0,55.7,25,55.7,55.7v200.6c0,30.7-25,55.7-55.7,55.7h-217c-19.5,0-38.3-8.2-51.5-22.6
l-111.7-121.8C23.025,249.663,23.025,239.663,29.125,233.063z"
/>
<path
d="M225.425,309.763c2.4,2.4,5.5,3.6,8.7,3.6s6.3-1.2,8.7-3.6l47.8-47.8l47.8,47.8c2.4,2.4,5.5,3.6,8.7,3.6s6.3-1.2,8.7-3.6
c4.8-4.8,4.8-12.5,0-17.3l-47.9-47.8l47.8-47.8c4.8-4.8,4.8-12.5,0-17.3s-12.5-4.8-17.3,0l-47.8,47.8l-47.8-47.8
c-4.8-4.8-12.5-4.8-17.3,0s-4.8,12.5,0,17.3l47.8,47.8l-47.8,47.8C220.725,297.263,220.725,304.962,225.425,309.763z"
/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import type { Writable } from 'svelte/store';
export let tenantData: Writable<TenantData>;
</script>
<div class="p-2 card variant-soft-surface min-w-[150px]">
<p class="underline text-center pb-2">Current User</p>
<div class="flex flex-row justify-between">
<p class="text-sm">Account:</p>
<p class="text-sm">{$tenantData.uid}</p>
</div>
<div class="flex flex-col justify-between">
<p class="text-sm pb-2">Roles:</p>
{#each $tenantData.role as role}
<p class="text-xs">{role}</p>
{/each}
</div>
</div>

View File

@@ -0,0 +1,25 @@
<script lang="ts">
import type { Writable } from 'svelte/store';
export let hostingData: Writable<HostingData>;
</script>
<div class="p-2 card variant-soft-surface min-w-[150px]">
<p class="underline text-center pb-2">Hosting Data</p>
<div class="flex flex-row justify-between">
<p class="text-sm">Org:</p>
<p class="text-sm">{$hostingData.org}</p>
</div>
<div class="flex flex-row justify-between">
<p class="text-sm">Pod:</p>
<p class="text-sm">{$hostingData.pod}</p>
</div>
<div class="flex flex-row justify-between">
<p class="text-sm">Layer:</p>
<p class="text-sm">{$hostingData.layer}</p>
</div>
<div class="flex flex-row justify-between">
<p class="text-sm">Region:</p>
<p class="text-sm">{$hostingData.region}</p>
</div>
</div>

View File

@@ -0,0 +1,54 @@
<script lang="ts">
import { popup, type PopupSettings } from '@skeletonlabs/skeleton';
const links: { 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: '🧭 Compass',
href: 'https://community.sailpoint.com'
},
{
label: '🔏 User Level Access Matrix',
href: 'https://documentation.sailpoint.com/saas/help/common/users/user_level_matrix.html'
}
];
const popupResources: PopupSettings = {
event: 'click',
target: 'popupResources',
placement: 'bottom',
closeQuery: '.listbox-item'
};
</script>
<button class="btn btn-sm variant-ghost-primary" use:popup={popupResources}>Resources</button>
<div class="p-2 card" data-popup="popupResources">
<ul class="flex flex-col justify-center gap-2">
{#each links as link}
<li class="listbox-item">
<a
class="hover:underline text-center hover:text-tertiary-600"
target="_blank"
rel="noreferrer"
href={link.href}
>
{link.label}
</a>
</li>
{/each}
</ul>
</div>

View File

@@ -0,0 +1,111 @@
<script lang="ts">
import {
InstantSearch,
SearchBox,
Hits,
Pagination,
HitsPerPage
} from 'svelte-algolia-instantsearch';
import algoliasearch from 'algoliasearch/lite';
import { RadioGroup, RadioItem, popup, type PopupSettings } from '@skeletonlabs/skeleton';
import SearchIcon from './searchIcon.svelte';
const searchClient = algoliasearch('TB01H1DFAM', '726952a7a9389c484b6c96808a3e0010');
// Available Indexes
// prod_DEVELOPER_SAILPOINT_COM
// discourse-posts
let index: string = 'prod_DEVELOPER_SAILPOINT_COM';
const popupFocusBlur: PopupSettings = {
event: 'focus-click',
target: 'popupFocusBlur',
placement: 'bottom',
closeQuery: ''
};
</script>
<div class="px-2 py-4">
{#key index}
<InstantSearch indexName={index} {searchClient}>
<div class="flex flex-col gap-1 h-full">
<div class="flex flex-row">
<div use:popup={popupFocusBlur} class="grow relative">
<SearchIcon />
<SearchBox
placeholder="Search the {index === 'prod_DEVELOPER_SAILPOINT_COM' ? 'Docs' : 'Forum'}"
classes={{
input: 'input rounded-r-none pl-12',
form: '',
resetIcon: 'white',
submit: 'hidden',
reset: 'hidden',
root: 'grow w-full'
}}
/>
</div>
<select bind:value={index} class="rounded-l-none rounded-full select w-fit">
<option value="prod_DEVELOPER_SAILPOINT_COM">Docs</option>
<option value="discourse-posts">Forum</option>
</select>
</div>
<div class="card max-w-[785px] p-2" data-popup="popupFocusBlur">
<div class="flex overflow-y-scroll overflow-hidden max-h-[300px] grow">
<Hits let:hit classes={{ root: 'grow' }}>
{#if hit.hierarchy}
<a href={hit.url}>
<div
class="flex flex-col card variant-soft-surface overflow-hidden p-2 w-[765px]"
>
<div class="flex flex-row justify-start gap-2">
<p class="truncate">
{hit.hierarchy.lvl1} - {hit.hierarchy.lvl2}
</p>
</div>
<p class="text-xs opacity-70">Tags: {hit.tags.join(', ')}</p>
<!-- <p class="text-xs text-primary-300 opacity-70">{hit.url_without_anchor}</p> -->
</div>
</a>
{:else}
<a href={hit.topic.url}>
<div class="flex flex-col card variant-soft-surface p-2 max-w-[765px] w-[765px]">
<div class="flex flex-row justify-between">
<div class="flex flex-col">
<p class="truncate max-w-[650px]">
{hit.topic.title}
</p>
<p class="text-xs opacity-70 pb-2">
{hit.user.name}
</p>
</div>
<p class=" top-1 right-1">{hit.topic.views} 👁️‍🗨️</p>
</div>
<div class="flex flex-row justify-between">
{#if hit.topic.tags.length > 0}
<p class="text-xs opacity-70">Tags: {hit.topic.tags.join(', ')}</p>
{:else}
<p class="text-xs opacity-70">No Tags</p>
{/if}
<!-- <p class="text-xs text-primary-300 opacity-70 truncate max-w-[550px]">
{hit.topic.url}
</p> -->
</div>
</div>
</a>
{/if}
</Hits>
</div>
<div class="flex flex-row justify-center grow">
<div class="flex flex-col justify-center grow">
<Pagination classes={{ list: 'flex flex-row justify-center gap-2 text-xl' }} />
</div>
</div>
</div>
</div>
</InstantSearch>
{/key}
</div>

View File

@@ -0,0 +1,37 @@
<script lang="ts">
import { onMount } from 'svelte';
let resp: Promise<Status>;
const getStatus = async () => {
console.debug('Getting Status Page Details');
resp = await (await fetch('https://status.sailpoint.com/api/v2/status.json')).json();
console.debug(resp);
};
onMount(async () => {
getStatus();
setInterval(() => getStatus(), 10000);
});
</script>
<div class="p-2 card variant-soft-surface min-w-[183.53px]">
<p class="underline text-center pb-2">Status Page</p>
<div class="flex flex-row gap-2 justify-center">
{#await resp}
<div class="placeholder-circle w-16" />
<p>Checking</p>
{:then status}
{#if status?.status?.description == 'All Systems Operational'}
<p class="text-green-500 text-center">All Systems Operational</p>
{:else}
<div>
<p class="text-red-500 text-center">Ongoing Issues</p>
<a href="https://status.sailpoint.com" rel="noreferrer" target="_blank">
Click for details
</a>
</div>
{/if}
{/await}
</div>
</div>

View File

@@ -0,0 +1,26 @@
<script lang="ts">
import { labelSort } from '$lib/utilities';
const links: { 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/' }
];
</script>
<div class="p-2 card variant-soft-surface">
<p class="underline text-center pb-2">Support</p>
<div class="flex flex-col gap-2">
{#each links as link}
<a class="btn btn-sm variant-filled" target="_blank" rel="noreferrer" href={link.href}>
{link.label}
</a>
{/each}
</div>
</div>

View File

@@ -0,0 +1,27 @@
<script lang="ts">
import type { Writable } from 'svelte/store';
export let tenantData: Writable<TenantData>;
</script>
<div class="p-2 card variant-soft-surface min-w-[150px]">
<p class="underline text-center pb-2">Tenant Data</p>
<div class="flex flex-row flex-wrap justify-between">
<p class="text-sm">Name:</p>
<p class="text-sm px-1">{$tenantData.org.name}</p>
</div>
<div class="flex flex-row justify-between">
<p class="text-sm">Tenant:</p>
<p class="text-sm">{$tenantData.org.scriptName}</p>
</div>
<div class="flex flex-row justify-between">
<p class="text-sm">Tenant Type:</p>
<p class="text-sm">{$tenantData.org.orgType}</p>
</div>
<div class="flex flex-col justify-between">
<p class="text-sm pb-2">Enabled Features:</p>
{#each $tenantData.feature as feature}
<p class="text-xs">{feature}</p>
{/each}
</div>
</div>

View File

@@ -0,0 +1,50 @@
<script lang="ts">
import { popup, type PopupSettings } from '@skeletonlabs/skeleton';
import type { Writable } from 'svelte/store';
export let idnSession: Writable<IdnSession>;
const links: { 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'
},
{ label: '🔍 Search Tenant', slug: '/ui/search/search' }
];
const popupTenantLinks: PopupSettings = {
event: 'click',
target: 'popupTenantLinks',
placement: 'bottom',
closeQuery: '.listbox-item'
};
</script>
<button class="btn btn-sm variant-ghost-primary" use:popup={popupTenantLinks}>Tenant Links</button>
<div class="p-2 card" data-popup="popupTenantLinks">
<ul class="flex flex-col gap-2">
{#each links as link}
<li class="listbox-item">
<a
class="hover:underline text-center hover:text-tertiary-600"
target="_blank"
rel="noreferrer"
href={$idnSession.tenant || 'https://placeholder.com' + link.slug}
>
{link.label}
</a>
</li>
{/each}
</ul>
</div>

View File

@@ -0,0 +1,17 @@
<script lang="ts">
export let color = 'white';
</script>
<svg
class="absolute h-8 left-0 top-0 bottom-0 z-50 mt-1 pt-1 has:focus:hidden"
xmlns="http://www.w3.org/2000/svg"
{...$$restProps}
viewBox="0 0 50 50"
width="50px"
height="50px"
fill={color}
>
<path
d="M 21 3 C 11.621094 3 4 10.621094 4 20 C 4 29.378906 11.621094 37 21 37 C 24.710938 37 28.140625 35.804688 30.9375 33.78125 L 44.09375 46.90625 L 46.90625 44.09375 L 33.90625 31.0625 C 36.460938 28.085938 38 24.222656 38 20 C 38 10.621094 30.378906 3 21 3 Z M 21 5 C 29.296875 5 36 11.703125 36 20 C 36 28.296875 29.296875 35 21 35 C 12.703125 35 6 28.296875 6 20 C 6 11.703125 12.703125 5 21 5 Z"
/>
</svg>

121060
src/lib/V3Spec.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,77 +1,136 @@
import axios from 'axios'; import {
import { idnSession } from './settings'; hostingData,
idnSession,
noHostingData,
noSession,
noTenantData,
tenantData
} from './settings';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
// Gets currently active tab from Chrome via Extension API
export async function getActiveTabURL() { export async function getActiveTabURL() {
const tabs = await chrome.tabs.query({ const tabs = await chrome.tabs.query({
active: true, active: true,
currentWindow: true currentWindow: true
}); });
if (tabs.length === 0) { if (tabs.length < 1) {
console.debug('No Tabs returned, Returning'); throw new Error('No tabs returned');
return null;
} }
const activeTab = tabs[0]; const activeTab = tabs[0];
if (!activeTab || !activeTab.url) { if (!activeTab || !activeTab.url) {
console.debug('No ActiveTab, Returning'); throw new Error('No active tab');
return null;
} }
return new URL(activeTab.url); return new URL(activeTab.url);
} }
export async function checkAuth() { // retrieve the hosting data for the tenant from the API
console.debug('Getting Session - ' + new Date().toLocaleTimeString()); export async function getHostingData(session: IdnSession) {
console.debug('Retrieving Hosting Data');
const resp = await fetch(`${session.baseUrl}/beta/tenant-data/hosting-data`, {
method: 'GET',
headers: {
Authorization: `Bearer ${session.accessToken}`
}
});
if (resp.status === 401) return noHostingData;
const hostingData = (await resp.json()) satisfies HostingData;
console.debug(hostingData);
return hostingData;
}
// retrieve the tenant data for the tenant from the API
export async function getTenantData(session: IdnSession) {
console.debug('Retrieving Tenant Data');
const resp = await fetch(`${session.baseUrl}/cc/api/user/get`, {
method: 'GET',
headers: {
Authorization: `Bearer ${session.accessToken}`
}
});
if (resp.status === 401) return noTenantData;
const tenantData = (await resp.json()) satisfies TenantData;
console.debug(tenantData);
return tenantData;
}
// Check for a current session
export async function checkSession() {
console.debug('Checking Session - ' + new Date().toLocaleTimeString());
let session; let session;
let tabUrl;
try { if (window.chrome && chrome.runtime && chrome.runtime.id) {
tabUrl = await getActiveTabURL(); let tabUrl;
if (!tabUrl) {
throw new Error('No Active Tab');
}
session = await axios.get(tabUrl.origin + '/ui/session');
console.debug('Current page is a valid IDN Tenant');
} catch (error) {
const tenant = get(idnSession).tenant;
if (tenant) {
tabUrl = new URL(tenant);
session = await axios.get(tenant + '/ui/session');
console.debug('Using cached session');
} else {
console.debug('No Session, and Current Tab is not an IDN Tenant');
return;
}
}
console.debug('Setting timeout for ' + session.data.refreshIn + ' milliseconds'); try {
setTimeout(() => checkAuth(), session.data.refreshIn); tabUrl = await getActiveTabURL();
session = await (await fetch(tabUrl.origin + '/ui/session')).json();
const sessionData = { console.debug('Current page is a valid IDN Tenant');
...session.data, } catch (error) {
expiration: new Date(Date.now() + session.data.refreshIn) const tenant = get(idnSession).tenant;
}; if (tenant) {
tabUrl = new URL(tenant);
try { const sessionResp = await fetch(tenant + '/ui/session').catch((err: Error) =>
const hostingData = await axios.get(`${session.data.baseUrl}/beta/tenant-data/hosting-data`, { console.debug(err)
method: 'GET', );
headers: { if (!sessionResp) return;
Authorization: `Bearer ${session.data.accessToken}` session = await sessionResp.json().catch((err: Error) => console.debug(err));
console.debug('Using cached session');
} else {
console.debug('No Session, and Current Tab is not an IDN Tenant');
session = noSession;
} }
}); }
console.debug('Checking Session again in ' + session.refreshIn + ' milliseconds');
setTimeout(() => checkSession(), session.refreshIn);
} else {
console.debug('Using Dev Session');
const tenant = import.meta.env.VITE_TENANT;
sessionData.tenant = tabUrl.origin; const accessTokenResp = await fetch(
sessionData.org = hostingData.data.org; `https://${tenant}.api.identitynow.com/oauth/token?grant_type=client_credentials&client_id=${
sessionData.pod = hostingData.data.pod; import.meta.env.VITE_CLIENT_ID
sessionData.layer = hostingData.data.layer; }&client_secret=${import.meta.env.VITE_CLIENT_SECRET}`,
sessionData.region = hostingData.data.region; { method: 'POST' }
);
idnSession.set(sessionData); const accessTokenData = await accessTokenResp.json();
} catch (error) { console.debug(accessTokenData);
console.error('Error fetching hosting data:', error);
session = {
authType: 'OAuth2.0',
baseUrl: `https://${tenant}.api.identitynow.com`,
logoutUrl: `https://${tenant}.identitynow.com/logout`,
accessToken: accessTokenData.access_token,
refreshIn: accessTokenData.expires_in,
pollUrl: `https://${tenant}.identitynow.com/ui/session`,
strongAuth: accessTokenData.strong_auth,
strongAuthUrl: `https://${tenant}.identitynow.com/api/user/strongAuthn`,
csrfToken: ''
};
console.debug('Checking Session again in ' + session.refreshIn + ' milliseconds');
setTimeout(() => checkSession(), session.refreshIn);
} }
console.debug('Session Data');
console.debug(session);
hostingData.set(await getHostingData(session));
tenantData.set(await getTenantData(session));
idnSession.set({
...session,
expiration: new Date(Date.now() + session.refreshIn),
tenant: new URL(session.pollUrl).origin
});
} }

View File

@@ -1,6 +1,137 @@
import { localStorageStore } from '@skeletonlabs/skeleton'; import { localStorageStore } from '@skeletonlabs/skeleton';
import type { Writable } from 'svelte/store'; import { writable, type Writable } from 'svelte/store';
export const idnSession: Writable<IdnSession> = localStorageStore('tenantData', { export const noSession: IdnSession = {
tenant: 'https://whatever.com' tenant: '',
}); authType: '',
baseUrl: '',
logoutUrl: '',
accessToken: '',
refreshIn: 30000,
pollUrl: '',
strongAuth: false,
strongAuthUrl: '',
csrfToken: '',
expiration: new Date()
};
export const noHostingData: HostingData = {
org: 'No Data',
pod: 'No Data',
publicPod: 'No Data',
layer: 'No Data',
region: 'No Data'
};
export const noTenantData: TenantData = {
id: '',
alias: '',
uid: '',
name: '',
displayName: '',
uuid: '',
encryptionKey: null,
encryptionCheck: null,
status: '',
pending: false,
passwordResetSinceLastLogin: false,
usageCertAttested: null,
userFlags: {},
enabled: false,
altAuthVia: '',
altAuthViaIntegrationData: null,
kbaAnswers: 0,
disablePasswordReset: false,
ptaSourceId: null,
supportsPasswordPush: false,
attributes: {
lastLoginTimestamp: 0,
uid: '',
firstname: '',
cloudAuthoritativeSource: '',
cloudStatus: '',
displayName: '',
internalCloudStatus: '',
lastSyncDate: '',
workPhone: '',
email: '',
lastname: ''
},
externalId: '',
role: [],
phone: null,
email: '',
personalEmail: null,
employeeNumber: null,
riskScore: 0,
featureFlags: {},
feature: [],
orgEncryptionKey: '',
orgEncryptionKeyId: '',
meta: {},
org: {
name: '',
scriptName: '',
mode: '',
numQuestions: 9,
status: '',
maxRegisteredUsers: 0,
pod: '',
pwdResetPersonalPhone: false,
pwdResetPersonalEmail: false,
pwdResetKba: false,
pwdResetEmail: false,
pwdResetDuo: false,
pwdResetPhoneMask: false,
authErrorText: null,
strongAuthKba: false,
strongAuthPersonalPhone: false,
strongAuthPersonalEmail: false,
integrations: [],
productName: '',
kbaReqForAuthn: 0,
kbaReqAnswers: 0,
lockoutAttemptThreshold: 0,
lockoutTimeMinutes: 0,
usageCertRequired: false,
adminStrongAuthRequired: false,
enableExternalPasswordChange: false,
enablePasswordReplay: false,
enableAutomaticPasswordReplay: false,
notifyAuthenticationSettingChange: false,
netmasks: null,
countryCodes: null,
whiteList: false,
usernameEmptyText: null,
usernameLabel: null,
enableAutomationGeneration: false,
emailTestMode: false,
emailTestAddress: '',
orgType: '',
passwordReplayState: '',
systemNotificationConfig: '',
maxClusterDebugHours: '',
brandName: '',
logo: null,
emailFromAddress: '',
standardLogoUrl: null,
narrowLogoUrl: null,
actionButtonColor: '',
activeLinkColor: '',
navigationColor: ''
},
stepUpAuth: false,
bxInstallPrompted: false,
federatedLogin: false,
auth: {
service: '',
encryption: ''
},
onNetwork: false,
onTrustedGeo: false,
loginUrl: ''
};
export const idnSession: Writable<IdnSession> = localStorageStore('tenantData', noSession);
export const hostingData: Writable<HostingData> = writable(noHostingData);
export const tenantData: Writable<TenantData> = writable(noTenantData);

1
src/lib/utilities.ts Normal file
View File

@@ -0,0 +1 @@
export const labelSort = (a: any, b: any) => a.label.localeCompare(b.label);

View File

@@ -7,17 +7,22 @@
import '../app.postcss'; import '../app.postcss';
import { AppBar, AppShell, storeHighlightJs } from '@skeletonlabs/skeleton'; import { AppBar, AppShell, storeHighlightJs } from '@skeletonlabs/skeleton';
import { checkSession } from '$lib/authentication';
import { checkAuth } from '$lib/authentication';
import { idnSession } from '$lib/settings'; import { idnSession } from '$lib/settings';
import { arrow, autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom';
import { storePopup } from '@skeletonlabs/skeleton';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import hljs from 'highlight.js'; import hljs from 'highlight.js';
import 'highlight.js/styles/github-dark.css'; import 'highlight.js/styles/github-dark.css';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import TenantLinks from '$lib/Components/TenantLinks.svelte';
import { page } from '$app/stores';
import Resources from '$lib/Components/Resources.svelte';
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
storeHighlightJs.set(hljs); storeHighlightJs.set(hljs);
onMount(async () => checkAuth()); onMount(async () => checkSession());
let now = dayjs(); let now = dayjs();
let minutesUntil = dayjs($idnSession?.expiration).diff(now, 'minutes'); let minutesUntil = dayjs($idnSession?.expiration).diff(now, 'minutes');
@@ -35,10 +40,17 @@
<AppBar> <AppBar>
<svelte:fragment slot="lead"> <svelte:fragment slot="lead">
<div class="flex flex-row gap-4"> <div class="flex flex-row gap-4">
<a class="btn btn-sm variant-filled" href="/">Tenant</a> <a class:text-primary-400={$page.url.pathname === '/'} href="/">Tenant</a>
<a class="btn btn-sm variant-filled" href="/session">Session</a> <a class:text-primary-400={$page.url.pathname === '/api-client'} href="/api-client">
API Client
</a>
<a class:text-primary-400={$page.url.pathname === '/session'} href="/session">Session</a>
</div> </div>
</svelte:fragment> </svelte:fragment>
<div class="flex flex-row justify-center gap-2">
<TenantLinks {idnSession} />
<Resources />
</div>
<svelte:fragment slot="trail"> <svelte:fragment slot="trail">
<div class="p-1 top-0 right-0 flex flex-row gap-2"> <div class="p-1 top-0 right-0 flex flex-row gap-2">
{#if minutesUntil < 0 || secondsUntil < 0} {#if minutesUntil < 0 || secondsUntil < 0}
@@ -53,7 +65,7 @@
Strong Auth: {#if $idnSession.strongAuth === true} Strong Auth: {#if $idnSession.strongAuth === true}
<span class="text-green-500">True</span> <span class="text-green-500">True</span>
{:else} {:else}
<span class="text-red-500">True</span> <span class="text-red-500">False</span>
{/if} {/if}
</p> </p>
<p class="text-xs text-white my-auto"> <p class="text-xs text-white my-auto">

View File

@@ -1,22 +1,29 @@
<script lang="ts"> <script lang="ts">
import { idnSession } from '$lib/settings'; import CurrentUser from '$lib/Components/CurrentUser.svelte';
import HostingData from '$lib/Components/HostingData.svelte';
import Resources from '$lib/Components/Resources.svelte';
import Support from '$lib/Components/Support.svelte';
import StatusPage from '$lib/Components/StatusPage.svelte';
import TenantData from '$lib/Components/TenantData.svelte';
import TenantLinks from '$lib/Components/TenantLinks.svelte';
import { idnSession, hostingData, tenantData } from '$lib/settings';
import Search from '$lib/Components/Search.svelte';
</script> </script>
<div class="p-2 flex flex-row gap-2"> <div class="flex flex-col h-full">
<div class="p-1"> <Search />
<p class="underline text-lg">Tenant Info</p> <div class="flex flex-row h-full gap-4 p-2 grow">
<p class="text-sm">Tenant: {$idnSession.org}</p> <div class=" flex flex-col gap-4 grow">
<p class="text-sm">Region: {$idnSession.region}</p> <TenantData {tenantData} />
<p class="text-sm">Pod: {$idnSession.pod}</p> </div>
</div>
<div class="p-1"> <div class=" flex flex-col gap-4 grow">
<p class="underline text-lg">Quick Links</p> <CurrentUser {tenantData} />
<a <HostingData {hostingData} />
target="_blank" </div>
rel="noreferrer" <div class=" flex flex-col gap-4 grow">
href={$idnSession.tenant + '/ui/admin#admin:dashboard:overview'} <StatusPage />
> <Support />
Tenant Dashboard </div>
</a>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,69 @@
<script lang="ts">
import { Tab, TabGroup } from '@skeletonlabs/skeleton';
import { JSONEditor } from 'svelte-jsoneditor';
import 'svelte-jsoneditor/themes/jse-theme-dark.css';
import BetaSpec from './BetaSpec.json';
import CustomSpec from './CustomSpec.json';
import V3Spec from './V3Spec.json';
let APIVersions;
let specification = BetaSpec;
let path = Object.entries(specification.paths)[0][0];
let requestContent = {
text: undefined, // can be used to pass a stringified JSON document instead
json: {
request: 'All your base belong to us'
}
};
let responseContent = {
text: undefined, // can be used to pass a stringified JSON document instead
json: {
response: 200
}
};
let tabSet: number = 0;
$: methods = Object.entries(specification.paths[path] || { get: 'content' });
</script>
<div class="p-4 flex flex-col gap-4 h-full">
<div class="flex flex-row">
<select bind:value={specification} class="select w-fit rounded-r-none">
<option value={BetaSpec}>Beta API</option>
<option value={V3Spec}>V3 API</option>
<option value={CustomSpec}>Custom</option>
</select>
<select bind:value={path} class="select rounded-l-none">
{#each Object.entries(specification.paths) as [path, methods]}
<option value={path}>{path}</option>
{/each}
</select>
</div>
<div class="flex flex-row">
<select class="select rounded-r-none w-fit">
{#each methods as [method, content]}
<option>{method.toUpperCase()}</option>
{/each}
</select>
<input bind:value={path} class="input rounded-l-none rounded-r-none pl-4" />
<button class="btn variant-filled-surface rounded-l-none rounded-r-lg">Call</button>
</div>
<TabGroup justify="justify-center" class="grow">
<Tab bind:group={tabSet} name="tab1" value={0}>Request</Tab>
<Tab bind:group={tabSet} name="tab2" value={1}>Response</Tab>
</TabGroup>
{#if tabSet === 0}
<div class="jse-theme-dark h-full">
<JSONEditor bind:content={requestContent} />
</div>
{:else if tabSet === 1}
<div class="jse-theme-dark h-full">
<JSONEditor bind:content={responseContent} />
</div>
{/if}
</div>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,12 @@
{
"paths": {
"/custom/api/endpoint": {
"get": "content",
"post": "content",
"put": "content",
"patch": "content",
"delete": "content",
"head": "content"
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -3,6 +3,6 @@
import { CodeBlock } from '@skeletonlabs/skeleton'; import { CodeBlock } from '@skeletonlabs/skeleton';
</script> </script>
<div class="p-1"> <div class="p-4 h-full flex flex-col justify-center">
<CodeBlock lineNumbers language="json" code={JSON.stringify($idnSession, null, ' ')} /> <CodeBlock lineNumbers language="json" code={JSON.stringify($idnSession, null, ' ')} />
</div> </div>

BIN
yq.exe Normal file

Binary file not shown.