Files
andon/app/layouts/default.vue
Julien Calixte 82ac540909 feat(auth): Google OAuth, sealed session and default-deny gating (T9)
Gate the app behind Google sign-in restricted to verified @theodo.com
identities, and record the real reporter on filed defects (F9).

- nuxt-auth-utils for sealed cookie sessions + the Google OAuth handler,
  mounted at /auth/google/callback to match the registered redirect URI.
- isAllowedGoogleUser re-derives the domain from Google's verified email;
  the spoofable `hd` claim is deliberately ignored (DESIGN T9).
- Default-deny: server middleware 401s unauthenticated /api calls (health
  and the session endpoint excepted); a global route middleware redirects
  unauthenticated page navigations to /login.
- getReporter() now reads the session email instead of the dev stub.
- Env contract moves to nuxt-auth-utils names (NUXT_SESSION_PASSWORD,
  NUXT_OAUTH_GOOGLE_*); .env.example, compose and README updated.

Unit-tested: domain check (incl. hd-spoof + look-alike) and public-path
matching. Live OAuth round-trip pending manual verification.
2026-05-28 01:09:48 +02:00

46 lines
1.4 KiB
Vue

<script setup lang="ts">
// Single domain (ADR 0004): report at `/`, view all defects at `/defects`,
// with a persistent header to move between them.
const { loggedIn, user, clear } = useUserSession()
async function signOut() {
await clear()
await navigateTo('/login')
}
</script>
<template>
<div class="min-h-screen">
<header
class="navbar bg-base-100 border-base-300 sticky top-0 z-10 border-b px-4 shadow-sm"
>
<div class="navbar-start">
<NuxtLink to="/" class="btn btn-ghost gap-2 text-xl font-semibold">
<span class="size-2.5 rounded-full bg-[#e11d48]" aria-hidden="true" />
Project Board Andon
</NuxtLink>
</div>
<nav class="navbar-end">
<ul class="menu menu-horizontal gap-1 px-0">
<li>
<NuxtLink to="/" exact-active-class="menu-active">Report</NuxtLink>
</li>
<li>
<NuxtLink to="/defects" exact-active-class="menu-active">
View defects
</NuxtLink>
</li>
</ul>
<div v-if="loggedIn" class="flex items-center gap-3 pl-2">
<span class="hidden text-sm opacity-70 sm:inline">{{ user?.email }}</span>
<button type="button" class="btn btn-ghost btn-sm" @click="signOut">
Sign out
</button>
</div>
</nav>
</header>
<slot />
</div>
</template>