diff --git a/package-lock.json b/package-lock.json index 27d9a63..da2c036 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,8 @@ "crypto-js": "^4.2.0", "jose": "^5.2.2", "semver": "^7.6.0", - "svelte-portal": "^2.2.1" + "svelte-portal": "^2.2.1", + "uuid": "^9.0.1" }, "devDependencies": { "@playwright/test": "^1.28.1", @@ -5900,6 +5901,18 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vite": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.4.tgz", diff --git a/package.json b/package.json index 91e32e5..896c5e0 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "crypto-js": "^4.2.0", "jose": "^5.2.2", "semver": "^7.6.0", - "svelte-portal": "^2.2.1" + "svelte-portal": "^2.2.1", + "uuid": "^9.0.1" } } diff --git a/src/lib/client.ts b/src/lib/client.ts index f65add7..66e97c0 100644 --- a/src/lib/client.ts +++ b/src/lib/client.ts @@ -1,21 +1,58 @@ +import { v4 as uuidv4 } from 'uuid'; import * as Api from '$lib/openapi/server'; import { removeCredentials } from '$lib/credentials.js'; +import { ToastSettings } from '@skeletonlabs/skeleton'; -export function client(token: string): BaseAPI { +// authenticationMiddleware performs logout if the request is unauthorized. +function authenticationMiddleware(): Api.Middleware { + return { + post: (ctx: Api.ErrorContext): Promise => { + if (ctx.response.status != 401) return; + removeCredentials(); + } + }; +} + +// baggageMiddleware injects w3c baggage to support request tracing and simple +// handling of support requests. +// TODO: this probably wants to be a full OpenTelemetry trace context +// but baby steps, because otel is impregnable! +function baggageMiddleware(toastStore: any): Api.Middleware { + const requestID = uuidv4(); + + return { + pre: (ctx: Api.RequestContext): Promise => { + ctx.init.headers['baggage'] = `request-id=${requestID}`; + }, + post: (ctx: Api.ErrorContext): Promise => { + if (ctx.response.ok) return; + + const toast: ToastSettings = { + autohide: false, + message: `Server request failed, please quote request ID ${requestID} when requesting help`, + background: 'variant-filled-error' + }; + + toastStore.trigger(toast); + } + }; +} + +// client gets a new initialized client with auth and any additional middlewares. +// NOTE: the toast store must be initialized in a svelte compenent or the context +// lookup for the store will fail, hence we have to pass it in. +export function client(toastStore: any, token: string): BaseAPI { const config = new Api.Configuration({ basePath: '', - accessToken: 'Bearer ' + token + accessToken: 'Bearer ' + token, + middleware: [authenticationMiddleware(), baggageMiddleware(toastStore)] }); return new Api.DefaultApi(config); } +// error is a generic fallback when an exception occurs, everything else should +// be handled in a middleware, and not on a per API basis. export function error(error: Error): void { - // Perhaps raise a toast here or something?? console.log(error); - - if (error.response.status == 401) { - removeCredentials(); - return; - } } diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 69c254b..5b60d8d 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -10,8 +10,12 @@ /* Required for drawers and modals */ import { initializeStores, Modal } from '@skeletonlabs/skeleton'; + console.log('init stores'); initializeStores(); + /* Required for toasts */ + import { Toast } from '@skeletonlabs/skeleton'; + /* Required for popups */ import { storePopup } from '@skeletonlabs/skeleton'; import { computePosition, autoUpdate, offset, shift, flip, arrow } from '@floating-ui/dom'; @@ -75,6 +79,7 @@ + diff --git a/src/routes/applications/catalog/+page.svelte b/src/routes/applications/catalog/+page.svelte index d1726cf..ca212e1 100644 --- a/src/routes/applications/catalog/+page.svelte +++ b/src/routes/applications/catalog/+page.svelte @@ -9,6 +9,9 @@ description: 'View platform applications that can be deployed on your infrastructure.' }; + import { getToastStore } from '@skeletonlabs/skeleton'; + const toastStore = getToastStore(); + /* Client setup */ import { token } from '$lib/credentials.js'; import { client, error } from '$lib/client.ts'; @@ -19,7 +22,7 @@ token.subscribe((at: string): void => { if (!at) return; - client(at) + client(toastStore, at) .apiV1ApplicationsGet() .then((v) => (applications = v)) .catch((e: Error) => error(e)); diff --git a/src/routes/identity/projects/+page.svelte b/src/routes/identity/projects/+page.svelte index e8ad7b0..ea27523 100644 --- a/src/routes/identity/projects/+page.svelte +++ b/src/routes/identity/projects/+page.svelte @@ -10,6 +10,9 @@ import { onDestroy } from 'svelte'; + import { getToastStore } from '@skeletonlabs/skeleton'; + const toastStore = getToastStore(); + import { getModalStore, ModalSettings } from '@skeletonlabs/skeleton'; const modalStore = getModalStore(); @@ -24,7 +27,7 @@ let resources: Models.Projects; function update(): void { - client(at) + client(toastStore, at) .apiV1ProjectsGet() .then((v) => (resources = v)) .catch((e: Error) => error(e)); @@ -53,7 +56,7 @@ projectName: resource.name }; - client(at) + client(toastStore, at) .apiV1ProjectsProjectNameDelete(parameters) .catch((e: Error) => error(e)); } diff --git a/src/routes/identity/projects/create/+page.svelte b/src/routes/identity/projects/create/+page.svelte index ef689c2..81710ed 100644 --- a/src/routes/identity/projects/create/+page.svelte +++ b/src/routes/identity/projects/create/+page.svelte @@ -9,6 +9,9 @@ description: 'Create a new project.' }; + import { getToastStore } from '@skeletonlabs/skeleton'; + const toastStore = getToastStore(); + import { Stepper, Step } from '@skeletonlabs/skeleton'; /* Client setup */ @@ -31,7 +34,7 @@ at = token; /* Get top-level resources required for the first step */ - client(at) + client(toastStore, at) .apiV1ProjectsGet() .then((v) => (projects = v)) .catch((e: Error) => error(e)); @@ -49,7 +52,7 @@ } }; - client(at) + client(toastStore, at) .apiV1ProjectsPost(parameters) .then(() => (window.location = '/identity/projects')) .catch((e: Error) => error(e)); diff --git a/src/routes/infrastructure/clusters/+page.svelte b/src/routes/infrastructure/clusters/+page.svelte index a1bf88b..60ba8b6 100644 --- a/src/routes/infrastructure/clusters/+page.svelte +++ b/src/routes/infrastructure/clusters/+page.svelte @@ -20,12 +20,15 @@ import * as Models from '$lib/openapi/server/models'; import * as Api from '$lib/openapi/server/apis'; + import { getToastStore } from '@skeletonlabs/skeleton'; + const toastStore = getToastStore(); + let at: string; let resources: Models.KubernetesClusters; function update(): void { - client(at) + client(toastStore, at) .apiV1ClustersGet() .then((v) => (resources = v)) .catch((e: Error) => error(e)); @@ -57,7 +60,7 @@ clusterName: resource.name }; - client(at) + client(toastStore, at) .apiV1ProjectsProjectNameControlplanesControlPlaneNameClustersClusterNameDelete( parameters ) diff --git a/src/routes/infrastructure/clusters/create/+page.svelte b/src/routes/infrastructure/clusters/create/+page.svelte index 303b889..a43e7f1 100644 --- a/src/routes/infrastructure/clusters/create/+page.svelte +++ b/src/routes/infrastructure/clusters/create/+page.svelte @@ -9,6 +9,9 @@ description: 'Create and deploy a new Kubernetes cluster.' }; + import { getToastStore } from '@skeletonlabs/skeleton'; + const toastStore = getToastStore(); + import { Stepper, Step, SlideToggle } from '@skeletonlabs/skeleton'; /* Client setup */ @@ -50,7 +53,7 @@ /* Get top-level resources required for the first step */ /* TODO: parallelize with Promise.all */ - client(at) + client(toastStore, at) .apiV1RegionsGet() .then((v) => { if (v.length == 0) return; @@ -60,7 +63,7 @@ }) .catch((e: Error) => error(e)); - client(at) + client(toastStore, at) .apiV1ProjectsGet() .then((v) => { if (v.length == 0) return; @@ -74,7 +77,7 @@ function updateControlPlanes(at: string, project: string) { if (!at || !project) return; - client(at) + client(toastStore, at) .apiV1ControlplanesGet() .then((v) => { if (v.length == 0) return; @@ -92,7 +95,7 @@ function updateClusters(at: string, controlplane: string): void { if (!at || !controlplane) return; - client(at) + client(toastStore, at) .apiV1ClustersGet() .then((v) => { if (v.length == 0) return; @@ -119,7 +122,7 @@ regionName: region }; - client(at) + client(toastStore, at) .apiV1RegionsRegionNameImagesGet(parameters) .then((v) => (images = v)) .catch((e: Error) => error(e)); @@ -132,7 +135,7 @@ regionName: region }; - client(at) + client(toastStore, at) .apiV1RegionsRegionNameFlavorsGet(parameters) .then((v) => (flavors = v)) .catch((e: Error) => error(e)); @@ -206,7 +209,7 @@ } }; - client(at) + client(toastStore, at) .apiV1ProjectsProjectNameControlplanesControlPlaneNameClustersPost(parameters) .then(() => (window.location = '/infrastructure/clusters')) .catch((e: Error) => error(e)); diff --git a/src/routes/infrastructure/controlplanes/+page.svelte b/src/routes/infrastructure/controlplanes/+page.svelte index e519cb4..df5a9ec 100644 --- a/src/routes/infrastructure/controlplanes/+page.svelte +++ b/src/routes/infrastructure/controlplanes/+page.svelte @@ -10,6 +10,9 @@ import { onDestroy } from 'svelte'; + import { getToastStore } from '@skeletonlabs/skeleton'; + const toastStore = getToastStore(); + import { getModalStore, ModalSettings } from '@skeletonlabs/skeleton'; const modalStore = getModalStore(); @@ -24,7 +27,7 @@ let resources: Models.ControlPlanes; function update(): void { - client(at) + client(toastStore, at) .apiV1ControlplanesGet() .then((v) => (resources = v)) .catch((e: Error) => error(e)); @@ -54,7 +57,7 @@ controlPlaneName: resource.name }; - client(at) + client(toastStore, at) .apiV1ProjectsProjectNameControlplanesControlPlaneNameDelete(parameters) .catch((e: Error) => error(e)); }