Symptom
After deleting a row from \"user\" (e.g. via SQL or deleteUser() at src/lib/users.ts:87), the affected user remains "logged in" in any browser that holds their JWT cookie. They continue to navigate the dashboard and hit APIs as if nothing changed.
Root cause
NextAuth is configured with session: { strategy: "jwt" } (src/lib/auth.ts:154-156). Sessions live in a stateless, signed cookie — not in the DB. Deleting the user row does not invalidate the cookie because there's no DB session record to delete.
The jwt callback at src/lib/auth.ts:201-228 does refresh the role from DB on every request:
const [dbUser] = await db
.select({ role: users.role, email: users.email })
.from(users)
.where(eq(users.id, token.id as string));
if (dbUser) {
// ... role refresh
}
But when dbUser is undefined (user was deleted), the if (dbUser) block is silently skipped. token.role stays at whatever it was, and downstream code happily treats the holder as authenticated. There's no path that says "user no longer exists → invalidate the session."
Why this matters
- Operational confusion. Was caught testing a different bug — admin deleted from DB, browser still showed admin dashboard. Hard to reason about access state.
- Security adjacent. If an admin offboards a user by deleting their row, the user retains access until their JWT expires (default ~30 days for NextAuth). For most threat models this is fine, but it's a surprising behavior.
- Stale-token paths. Various downstream API calls might fail in confusing ways or partially work via the global-admin role bypass at
/api/workspaces/route.ts:16-19.
Proposed fix
In the jwt callback, when dbUser is undefined for a non-empty token.id, invalidate the session by returning an empty/expired token:
if (token.id) {
const [dbUser] = await db.select(...)...
if (!dbUser) {
// User was deleted — invalidate the session
return {};
}
// ... existing refresh logic
}
NextAuth treats an empty token as unauthenticated, redirects to login on the next protected request. (Confirm exact shape via Auth.js v5 docs — may need return null or specific token shape.)
Acceptance criteria
Context
Discovered while testing PR #2. Documented for follow-up but not in scope of that PR.
Symptom
After deleting a row from
\"user\"(e.g. via SQL ordeleteUser()atsrc/lib/users.ts:87), the affected user remains "logged in" in any browser that holds their JWT cookie. They continue to navigate the dashboard and hit APIs as if nothing changed.Root cause
NextAuth is configured with
session: { strategy: "jwt" }(src/lib/auth.ts:154-156). Sessions live in a stateless, signed cookie — not in the DB. Deleting the user row does not invalidate the cookie because there's no DB session record to delete.The
jwtcallback atsrc/lib/auth.ts:201-228does refresh the role from DB on every request:But when
dbUseris undefined (user was deleted), theif (dbUser)block is silently skipped.token.rolestays at whatever it was, and downstream code happily treats the holder as authenticated. There's no path that says "user no longer exists → invalidate the session."Why this matters
/api/workspaces/route.ts:16-19.Proposed fix
In the
jwtcallback, whendbUseris undefined for a non-emptytoken.id, invalidate the session by returning an empty/expired token:NextAuth treats an empty token as unauthenticated, redirects to login on the next protected request. (Confirm exact shape via Auth.js v5 docs — may need
return nullor specific token shape.)Acceptance criteria
/login(not the broken dashboard with a ghost token)./login.Context
Discovered while testing PR #2. Documented for follow-up but not in scope of that PR.