first commit
This commit is contained in:
74
src/app/api/auth/logout/route.ts
Normal file
74
src/app/api/auth/logout/route.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user