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
@@ -0,0 +1,96 @@
/**
* Auth & Session Registry tests — dev system
*
* Validates that:
* - Login works with the dev-DB seed users
* - After login, all tRPC calls succeed (no 401 "Session revoked")
* - After logout, protected routes redirect to sign-in
* - Invalid credentials are rejected
*
* These tests guard against regressions in the active-session registry
* (token.sid / active_sessions table) introduced in the auth hardening work.
*/
import { expect, test } from "@playwright/test";
import { assertNoTrpc401s, DEV_USERS, signIn, signOut } from "./helpers.js";
test.describe("Auth — login / logout", () => {
test("admin login succeeds and lands on a protected page", async ({ page }) => {
await signIn(page, DEV_USERS.admin.email, DEV_USERS.admin.password);
await expect(page).not.toHaveURL(/\/auth\/signin/);
});
test("manager login succeeds", async ({ page }) => {
await signIn(page, DEV_USERS.manager.email, DEV_USERS.manager.password);
await expect(page).not.toHaveURL(/\/auth\/signin/);
});
test("viewer login succeeds", async ({ page }) => {
await signIn(page, DEV_USERS.viewer.email, DEV_USERS.viewer.password);
await expect(page).not.toHaveURL(/\/auth\/signin/);
});
test("invalid credentials show an error and stay on sign-in", async ({ page }) => {
await page.goto("/auth/signin");
await page.fill('input[type="email"]', "nobody@example.com");
await page.fill('input[type="password"]', "wrong");
await page.click('button[type="submit"]');
await expect(page).toHaveURL(/\/auth\/signin/, { timeout: 5000 });
// Error message visible
await expect(
page.locator("text=/invalid|incorrect|wrong|credentials/i"),
).toBeVisible({ timeout: 5000 });
});
test("after logout, protected routes redirect to sign-in", async ({ page }) => {
await signIn(page, DEV_USERS.admin.email, DEV_USERS.admin.password);
await signOut(page);
await page.goto("/dashboard");
await expect(page).toHaveURL(/\/auth\/signin/, { timeout: 10000 });
});
});
test.describe("Session registry — no tRPC 401s after login", () => {
test("admin login produces a valid session: dashboard loads without 401s", async ({ page }) => {
await assertNoTrpc401s(page, async () => {
await signIn(page, DEV_USERS.admin.email, DEV_USERS.admin.password);
await page.goto("/dashboard");
await page.waitForLoadState("networkidle");
});
});
test("admin navigating to /admin/users fires no 401s and loads user rows", async ({ page }) => {
await signIn(page, DEV_USERS.admin.email, DEV_USERS.admin.password);
await assertNoTrpc401s(page, async () => {
await page.goto("/admin/users");
await page.waitForLoadState("networkidle");
});
// At least one user row should be visible
await expect(page.locator("table")).toBeVisible({ timeout: 10000 });
await expect(page.locator("text=/planarchy\.dev|capakraken\.dev/")).toBeVisible({
timeout: 10000,
});
await expect(page.locator("text=No users found")).toHaveCount(0);
});
test("admin navigating to /admin/system-roles fires no 401s", async ({ page }) => {
await signIn(page, DEV_USERS.admin.email, DEV_USERS.admin.password);
await assertNoTrpc401s(page, async () => {
await page.goto("/admin/system-roles");
await page.waitForLoadState("networkidle");
});
// Page should render something (not blank)
await expect(page.locator("h1,h2").first()).toBeVisible({ timeout: 10000 });
});
test("viewer login produces a valid session: no 401s on dashboard", async ({ page }) => {
await assertNoTrpc401s(page, async () => {
await signIn(page, DEV_USERS.viewer.email, DEV_USERS.viewer.password);
await page.goto("/dashboard");
await page.waitForLoadState("networkidle");
});
});
});