Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion app/components/header/AvatarButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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",
Expand All @@ -32,7 +38,7 @@ const menuItems = computed(() => [
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
isAuthenticated.value
? signOut({ callbackUrl: "/" })
: signIn(`${idpProvider}`);
: attemptSignIn();
},
},
{
Expand Down
8 changes: 7 additions & 1 deletion app/components/landing/IdpAuthBtns.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
<script lang="ts" setup>
import { useRuntimeConfig } from "nuxt/app";
import { useToast } from "primevue/usetoast";

const { signIn, signOut } = useAuth();
const { status } = useAuthState();
const config = useRuntimeConfig();
const toast = useToast();
const idpProvider: string = config.public.idpProvider as string;
const idpNameCapitalized: string =
idpProvider.charAt(0).toUpperCase() + idpProvider.slice(1);

const attemptSignIn = async () => {
if (await checkIdpReachable(toast)) await signIn(idpProvider);
};
</script>

<template>
Expand All @@ -25,7 +31,7 @@ const idpNameCapitalized: string =
class="idp-auth-success"
outlined
severity="success"
@click="signIn(idpProvider)"
@click="attemptSignIn"
>Login with {{ idpNameCapitalized }}
</Button>
</div>
Expand Down
18 changes: 18 additions & 0 deletions app/composables/connectionErrorToast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
};
33 changes: 33 additions & 0 deletions app/composables/useIdpHealth.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
let health: IdpHealth;

try {
health = await $fetch<IdpHealth>("/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;
}
47 changes: 47 additions & 0 deletions server/routes/flame/api/health.get.ts
Original file line number Diff line number Diff line change
@@ -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 };
Comment thread
brucetony marked this conversation as resolved.
} 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",
};
}
});