75 lines
3.0 KiB
TypeScript
75 lines
3.0 KiB
TypeScript
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;
|
|
}
|