fix(auth): use token.sid to avoid Auth.js jti claim conflict

Auth.js v5 manages token.jti internally and overwrites it after the jwt
callback. Storing our session UUID in token.sid ensures the value we
persist in active_sessions matches what the signed cookie carries.

- jwt callback: token.sid = jti (was token.jti)
- session callback: read from token.sid
- signOut event: falls back to token.jti for backward compat with any
  sessions created before this change

Also adds Playwright dev-system test suite (playwright.dev.config.ts +
e2e/dev-system/) that validates login, session registry health, and
RBAC enforcement against the running localhost:3100 dev server.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-04-01 19:00:44 +02:00
parent a867672afa
commit 3d8a256d52
5 changed files with 307 additions and 5 deletions
+45
View File
@@ -0,0 +1,45 @@
import { expect, type Page } from "@playwright/test";
/** Dev-system credentials — these exist in the planarchy.dev seed data */
export const DEV_USERS = {
admin: { email: "admin@planarchy.dev", password: "admin123" },
manager: { email: "manager@planarchy.dev", password: "manager123" },
viewer: { email: "viewer@planarchy.dev", password: "viewer123" },
} as const;
export async function signIn(page: Page, email: string, password: string) {
await page.goto("/auth/signin");
await page.fill('input[type="email"]', email);
await page.fill('input[type="password"]', password);
await page.click('button[type="submit"]');
await expect(page).toHaveURL(/\/(dashboard|resources)/, { timeout: 15000 });
}
export async function signOut(page: Page) {
await page.goto("/auth/signout");
// Wait for redirect back to signin
await page.waitForURL(/\/auth\/signin/, { timeout: 10000 });
}
/**
* Intercept all tRPC batch responses and assert none return HTTP 401.
* Returns a list of intercepted tRPC paths that were called.
*/
export async function assertNoTrpc401s(page: Page, action: () => Promise<void>) {
const failures: string[] = [];
page.on("response", (response) => {
if (response.url().includes("/api/trpc/") && response.status() === 401) {
const url = new URL(response.url());
failures.push(url.pathname + url.search.slice(0, 80));
}
});
await action();
if (failures.length > 0) {
throw new Error(
`tRPC 401 responses detected (session registry / auth broken):\n${failures.join("\n")}`,
);
}
}