fix: bootstrap-gate auth before mount (krypto-kuns pattern); never call API unauthenticated
Root cause: the router-guard adapter let index.vue mount and call the API before auth resolved, so auth.fetch returned a synthetic 401 (the banner) and the package's redirect-loop guard could strand the user. Now use the core ZitadelAuth and gate in an async plugin (Nuxt awaits it before mount), mirroring the working krypto-kuns app.
This commit is contained in:
@@ -1,17 +1,10 @@
|
|||||||
// Access the provided Zitadel auth instance + a small JSON helper for /api calls.
|
import type { ZitadelAuth } from "@kuns/zitadel-auth";
|
||||||
|
|
||||||
|
// Access the bootstrap-provided Zitadel auth instance + a small JSON helper for /api calls.
|
||||||
|
// By the time any component mounts, the plugin has gated auth, so `auth` is authenticated.
|
||||||
// `auth.fetch` auto-attaches the Bearer access token.
|
// `auth.fetch` auto-attaches the Bearer access token.
|
||||||
export function useAuth() {
|
export function useAuth() {
|
||||||
const { $auth } = useNuxtApp() as unknown as {
|
const { $auth } = useNuxtApp() as unknown as { $auth: ZitadelAuth };
|
||||||
$auth: {
|
|
||||||
isAuthenticated: Ref<boolean>;
|
|
||||||
isLoading: Ref<boolean>;
|
|
||||||
user: Ref<{ sub: string; name: string; email: string } | null>;
|
|
||||||
error: Ref<string | null>;
|
|
||||||
login: () => void;
|
|
||||||
logout: () => Promise<void>;
|
|
||||||
fetch: (url: string, init?: RequestInit) => Promise<Response>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
async function api<T = unknown>(path: string, init?: RequestInit): Promise<T> {
|
async function api<T = unknown>(path: string, init?: RequestInit): Promise<T> {
|
||||||
const res = await $auth.fetch(`/api${path}`, init);
|
const res = await $auth.fetch(`/api${path}`, init);
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// The Zitadel client processes the OIDC redirect during init(); once authenticated,
|
// The auth plugin completes the OIDC code exchange during init() before mount.
|
||||||
// move back to the app root.
|
// This page just returns to the app root.
|
||||||
const { auth } = useAuth();
|
onMounted(() => {
|
||||||
watchEffect(() => {
|
|
||||||
if (!auth.isLoading.value && auth.isAuthenticated.value) {
|
|
||||||
navigateTo("/", { replace: true });
|
navigateTo("/", { replace: true });
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -86,15 +86,20 @@ async function addTask() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(refreshLists);
|
onMounted(() => {
|
||||||
|
// The auth plugin gates before mount, so this is normally authenticated.
|
||||||
|
// Safety net: if not, drive login instead of calling the API (no 401 banner).
|
||||||
|
if (auth.isAuthenticated) refreshLists();
|
||||||
|
else auth.login();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="app">
|
<div class="app">
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
<h1>Inbox</h1>
|
<h1>Inbox</h1>
|
||||||
<div class="who" v-if="auth.user.value">
|
<div class="who" v-if="auth.user">
|
||||||
<span class="email">{{ auth.user.value.email || auth.user.value.name }}</span>
|
<span class="email">{{ auth.user.email || auth.user.name }}</span>
|
||||||
<button class="link" @click="auth.logout()">Sign out</button>
|
<button class="link" @click="auth.logout()">Sign out</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -1,19 +1,34 @@
|
|||||||
import { useZitadelAuth } from "@kuns/zitadel-auth/vue";
|
import { ZitadelAuth } from "@kuns/zitadel-auth";
|
||||||
|
|
||||||
// Wire the framework-agnostic Zitadel OIDC client to Nuxt's router (client-only SPA).
|
// Bootstrap-gate auth BEFORE the app mounts (mirrors the working krypto-kuns pattern):
|
||||||
// Provides `$auth` (reactive state + login/logout/fetch). The adapter installs a
|
// Nuxt awaits async plugins before mounting, so we await init() and, if unauthenticated,
|
||||||
// router guard that redirects unauthenticated users to the Zitadel hosted login.
|
// redirect to Zitadel and hold the mount — the app never renders (and never calls the API)
|
||||||
export default defineNuxtPlugin(() => {
|
// while unauthenticated. This avoids the router-guard mount race that produced a 401 flash.
|
||||||
|
export default defineNuxtPlugin(async () => {
|
||||||
const cfg = useRuntimeConfig().public;
|
const cfg = useRuntimeConfig().public;
|
||||||
const scopes = ["openid", "profile", "email"];
|
const scopes = ["openid", "profile", "email"];
|
||||||
if (cfg.zitadelProjectId) {
|
if (cfg.zitadelProjectId) {
|
||||||
// Force the project id into the access token's `aud` for backend validation.
|
// Put the project id into the access token's `aud` for backend validation.
|
||||||
scopes.push(`urn:zitadel:iam:org:project:id:${cfg.zitadelProjectId}:aud`);
|
scopes.push(`urn:zitadel:iam:org:project:id:${cfg.zitadelProjectId}:aud`);
|
||||||
}
|
}
|
||||||
const auth = useZitadelAuth(useRouter() as never, {
|
|
||||||
|
const auth = new ZitadelAuth({
|
||||||
clientId: cfg.zitadelClientId as string,
|
clientId: cfg.zitadelClientId as string,
|
||||||
issuer: cfg.zitadelIssuer as string,
|
issuer: cfg.zitadelIssuer as string,
|
||||||
scopes,
|
scopes,
|
||||||
|
// Bootstrap gate issues at most one redirect per load, so a real loop never happens;
|
||||||
|
// raise the loop-guard ceiling so repeated manual reloads can't strand the user.
|
||||||
|
maxRedirects: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const onCallback = window.location.pathname.endsWith("/auth/callback");
|
||||||
|
await auth.init();
|
||||||
|
|
||||||
|
if (!auth.isAuthenticated && !onCallback) {
|
||||||
|
auth.login();
|
||||||
|
// Hold the mount while the browser navigates to the Zitadel login.
|
||||||
|
await new Promise(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
return { provide: { auth } };
|
return { provide: { auth } };
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user