import {NextResponse} from 'next/server'; import {authApi} from '@/services/api/authApi'; import {sessionCookieOptions} from '@/features/auth/services/sessionToken'; import { sessionCookieFor, tokenCookieFor, } from '@/features/auth/services/tabScope'; import {resolveTabId} from '@/features/auth/services/tabScopeRequest'; import {peekAccessToken} from '@/features/auth/services/upstreamSession'; export const dynamic = 'force-dynamic'; /** * POST /api/auth/logout — sign THIS TAB out. * * Revokes the session upstream first, then clears that tab's two cookies. The * order is deliberate, and so is the fact that an upstream failure does NOT * abort the local clear: if the platform is unreachable, the least bad outcome * is that this tab is signed out immediately and the server-side session lapses * on its own expiry. Leaving somebody apparently signed in because a revoke * call failed is the one outcome nobody expects from pressing Sign out. * * No refresh attempt: the token is about to be thrown away, so spending a * refresh token to revoke it is pure waste. * * ── Only this tab, and the upstream revoke is still real ───────────────── * `peekAccessToken` resolves through the tab scope, so the token revoked * upstream is THIS tab's session and no other. Signing out of the manager tab * ends the manager's platform session — genuinely, server-side, as before — and * leaves the admin and staff tabs holding their own untouched sessions in their * own cookies. Nothing here weakens server-side invalidation; it narrows what * gets invalidated to what the person actually asked to sign out of. * * A request with no resolvable tab clears nothing and still answers 200. There * is no session to end, and guessing at one would sign out a tab that never * asked. */ export async function POST() { const accessToken = await peekAccessToken(); if (accessToken) { try { await authApi.logout(accessToken); } catch { // Already-expired, revoked, or unreachable — all fine. The cookies below // are what actually ends this tab's session. } } const res = NextResponse.json( {data: {ok: true}, meta: {generatedAt: new Date().toISOString()}}, {headers: {'cache-control': 'no-store'}}, ); const tabId = await resolveTabId(); if (tabId) { // Overwrite with an expired cookie rather than only deleting: a delete that // misses on `path` leaves a live session behind. res.cookies.set(sessionCookieFor(tabId), '', sessionCookieOptions(0)); res.cookies.set(tokenCookieFor(tabId), '', { httpOnly: true, sameSite: 'lax', secure: process.env.NODE_ENV === 'production', path: '/', maxAge: 0, }); } // The pointer is deliberately left alone. It names a tab, not a session, and // the signed-out tab rewrites it on its next load anyway — clearing it here // would only blank the server-rendered first paint of whichever OTHER tab the // person switches to next. return res; }