54 lines
2.5 KiB
TypeScript
54 lines
2.5 KiB
TypeScript
export const dynamic = 'force-dynamic'
|
|
import { NextResponse } from 'next/server'
|
|
import jwt from 'jsonwebtoken'
|
|
import { auth } from '@/auth'
|
|
import { apiBase } from '@/lib/api'
|
|
|
|
// Review CR-02: "sign out everywhere" is an explicit, observable action.
|
|
// The NextAuth signOut event is a fire-and-forget backstop; this route lets
|
|
// the UI establish whether Go's global revocation (tokenVersion bump, which
|
|
// kills Go cookie sessions and previously minted assertions) actually
|
|
// succeeded BEFORE the local session is discarded — so a backend outage
|
|
// degrades to an accurate "signed out here, other sessions may remain"
|
|
// outcome instead of a silent partial logout. The request is deadline-bound
|
|
// and logs without secrets.
|
|
export async function POST(req: Request) {
|
|
const expectedOrigin = new URL(process.env.NEXTAUTH_URL || req.url).origin
|
|
if (req.headers.get('origin') !== expectedOrigin) {
|
|
return NextResponse.json({ error: 'Invalid request origin' }, { status: 403 })
|
|
}
|
|
const session = await auth()
|
|
const secret = process.env.NEXTAUTH_SECRET || process.env.AUTH_SECRET
|
|
const id = typeof session?.user?.id === 'string' ? session.user.id : ''
|
|
const ver = typeof session?.user?.ver === 'number' ? session.user.ver : 0
|
|
if (!secret || !id || ver < 1) {
|
|
// No backend identity attached to this session — nothing to revoke.
|
|
return NextResponse.json({ revoked: false, reason: 'no-backend-session' })
|
|
}
|
|
const controller = new AbortController()
|
|
const timer = setTimeout(() => controller.abort(), 5000)
|
|
try {
|
|
const res = await fetch(`${apiBase()}/api/auth/signout`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
// Same short-lived assertion the data proxy signs (lib/api.ts).
|
|
Authorization: `Bearer ${jwt.sign({ uid: id, ver, typ: 'assertion' }, secret, { algorithm: 'HS256', issuer: 'computer', expiresIn: '60s' })}`,
|
|
},
|
|
cache: 'no-store',
|
|
signal: controller.signal,
|
|
})
|
|
if (!res.ok) {
|
|
console.error(`session revoke: Go signout answered ${res.status} for user ${id}`)
|
|
return NextResponse.json({ revoked: false, reason: 'backend-error' })
|
|
}
|
|
return NextResponse.json({ revoked: true })
|
|
} catch (error) {
|
|
const kind = error instanceof Error ? error.name : 'Error'
|
|
console.error(`session revoke: Go signout unreachable for user ${id} (${kind})`)
|
|
return NextResponse.json({ revoked: false, reason: 'unreachable' })
|
|
} finally {
|
|
clearTimeout(timer)
|
|
}
|
|
}
|