diff --git a/app/components/header/AvatarButton.vue b/app/components/header/AvatarButton.vue
index ed4b388..545e3dd 100644
--- a/app/components/header/AvatarButton.vue
+++ b/app/components/header/AvatarButton.vue
@@ -2,10 +2,12 @@
import Menu from "primevue/menu";
import Button from "primevue/button";
import { useRuntimeConfig } from "nuxt/app";
+import { useToast } from "primevue/usetoast";
import CleanupDialog from "~/components/header/CleanupDialog.vue";
const { signIn, signOut } = useAuth();
const { status: authStatus, data: authData } = useAuthState();
+const toast = useToast();
const menu = ref();
const showCleanupDialog = ref(false);
@@ -21,6 +23,10 @@ const toggle = (event) => {
menu.value.toggle(event);
};
+const attemptSignIn = async () => {
+ if (await checkIdpReachable(toast)) await signIn(`${idpProvider}`);
+};
+
const menuItems = computed(() => [
{
label: "Options",
@@ -32,7 +38,7 @@ const menuItems = computed(() => [
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
isAuthenticated.value
? signOut({ callbackUrl: "/" })
- : signIn(`${idpProvider}`);
+ : attemptSignIn();
},
},
{
diff --git a/app/components/landing/IdpAuthBtns.vue b/app/components/landing/IdpAuthBtns.vue
index 7c3a0b5..89dd645 100644
--- a/app/components/landing/IdpAuthBtns.vue
+++ b/app/components/landing/IdpAuthBtns.vue
@@ -1,12 +1,18 @@
@@ -25,7 +31,7 @@ const idpNameCapitalized: string =
class="idp-auth-success"
outlined
severity="success"
- @click="signIn(idpProvider)"
+ @click="attemptSignIn"
>Login with {{ idpNameCapitalized }}
diff --git a/app/composables/connectionErrorToast.ts b/app/composables/connectionErrorToast.ts
index 0bf9456..7ac8bcb 100644
--- a/app/composables/connectionErrorToast.ts
+++ b/app/composables/connectionErrorToast.ts
@@ -224,3 +224,21 @@ export const showHubSpecificErrorMessage = (
});
console.warn(msg);
};
+
+// IDP error toasts
+
+export const showIdpUnreachableToast = (
+ toast: ToastServiceMethods,
+ msg?: string,
+) => {
+ const detail =
+ "Unable to contact the identity provider, this is likely a proxy configuration issue";
+
+ showConnectionErrorToast(toast, {
+ severity: "error",
+ summary: "Sign in failed",
+ detail: msg ? `${detail} (${msg})` : detail,
+ life: 8000,
+ });
+ console.warn("IDP is currently unreachable, sign in was not attempted");
+};
diff --git a/app/composables/useIdpHealth.ts b/app/composables/useIdpHealth.ts
new file mode 100644
index 0000000..5e5b905
--- /dev/null
+++ b/app/composables/useIdpHealth.ts
@@ -0,0 +1,33 @@
+import type { ToastServiceMethods } from "primevue/toastservice";
+import { showIdpUnreachableToast } from "~/composables/connectionErrorToast";
+
+interface IdpHealth {
+ reachable: boolean;
+ error?: string;
+}
+
+/**
+ * Checks that the IDP is reachable before signIn is called
+ *
+ * Returns true when sign in should go ahead
+ */
+export async function checkIdpReachable(
+ toast: ToastServiceMethods,
+): Promise {
+ let health: IdpHealth;
+
+ try {
+ health = await $fetch("/flame/api/health");
+ } catch (error) {
+ console.error("IDP health check failed:", error);
+ showIdpUnreachableToast(toast);
+ return false;
+ }
+
+ if (!health.reachable) {
+ showIdpUnreachableToast(toast, health.error);
+ return false;
+ }
+
+ return true;
+}
diff --git a/server/routes/flame/api/health.get.ts b/server/routes/flame/api/health.get.ts
new file mode 100644
index 0000000..ccf599e
--- /dev/null
+++ b/server/routes/flame/api/health.get.ts
@@ -0,0 +1,47 @@
+import { createProxy } from "node-fetch-native/proxy";
+
+// Keep under 3500ms (next-auth default)
+const TIMEOUT_MS = Number(process.env.NUXT_IDP_HEALTH_TIMEOUT_MS ?? 3000);
+
+function describeFetchError(error: unknown): string {
+ if (!(error instanceof Error)) return String(error); // Catch when not proper Error
+ if (error.name === "TimeoutError")
+ return `no response within ${TIMEOUT_MS}ms`;
+ return error.cause instanceof Error ? error.cause.message : error.message;
+}
+
+export default defineEventHandler(async () => {
+ const clientIssuer =
+ process.env.NUXT_PUBLIC_IDP_ISSUER ?? "http://localhost:8080/realms/flame";
+ const wellKnown = `${clientIssuer.replace(/\/$/, "")}/.well-known/openid-configuration`;
+
+ try {
+ const response = await fetch(wellKnown, {
+ ...(createProxy() as RequestInit),
+ signal: AbortSignal.timeout(TIMEOUT_MS),
+ });
+
+ const { ok, status } = response;
+
+ // Free the connection
+ await response.body?.cancel().catch(() => {});
+
+ if (!ok) {
+ console.warn(`IDP discovery returned ${status}: ${wellKnown}`);
+ return {
+ reachable: false,
+ error: `The identity provider responded with ${status}`,
+ };
+ }
+
+ return { reachable: true };
+ } catch (error) {
+ // try and parse error message for toast
+ const message = describeFetchError(error);
+ console.error(`IDP discovery unreachable at ${wellKnown}:`, message, error);
+ return {
+ reachable: false,
+ error: "The identity provider could not be reached",
+ };
+ }
+});