From 5bc7cace26cd12940a1c620c3fd9587030d0ec89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Wed, 1 Apr 2026 18:38:05 +0200 Subject: [PATCH] fix(auth): make active-session check fail-open; add missing DB migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The active_sessions table was never migrated to production — the model was added to the Prisma schema via db push only. prisma migrate deploy was a no-op because no migration directories existed. Without the table, prisma.activeSession.findUnique() throws P2021, crashing the tRPC handler with 500 on every authenticated request. This silently emptied all admin pages (users, system-roles, etc.). Changes: - Wrap the jti ActiveSession lookup in try-catch so the tRPC handler degrades gracefully (fail-open) if the table is temporarily missing - Add packages/db/prisma/migrations/20260401000000_active_sessions/ so prisma migrate deploy creates the table on next production deploy (idempotent via IF NOT EXISTS — safe if table already exists) Co-Authored-By: claude-flow --- apps/web/src/app/api/trpc/[trpc]/route.ts | 17 +++++---- .../migration.sql | 36 +++++++++++++++++++ 2 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 packages/db/prisma/migrations/20260401000000_active_sessions/migration.sql diff --git a/apps/web/src/app/api/trpc/[trpc]/route.ts b/apps/web/src/app/api/trpc/[trpc]/route.ts index a12a623..b0b1779 100644 --- a/apps/web/src/app/api/trpc/[trpc]/route.ts +++ b/apps/web/src/app/api/trpc/[trpc]/route.ts @@ -25,15 +25,20 @@ const handler = async (req: NextRequest) => { // Validate active session registry on every authenticated request. // Sessions kicked by concurrent-session limits or manual logout are rejected immediately. + // Fail-open: if the table doesn't exist yet (pending migration) the check is skipped. if (session?.user) { const jti = (session.user as typeof session.user & { jti?: string }).jti; if (jti) { - const activeSession = await prisma.activeSession.findUnique({ where: { jti } }); - if (!activeSession) { - return new Response(JSON.stringify({ error: "Session revoked" }), { - status: 401, - headers: { "Content-Type": "application/json" }, - }); + try { + const activeSession = await prisma.activeSession.findUnique({ where: { jti } }); + if (!activeSession) { + return new Response(JSON.stringify({ error: "Session revoked" }), { + status: 401, + headers: { "Content-Type": "application/json" }, + }); + } + } catch { + // Table may not exist yet (migration pending) — skip validation rather than crashing. } } } diff --git a/packages/db/prisma/migrations/20260401000000_active_sessions/migration.sql b/packages/db/prisma/migrations/20260401000000_active_sessions/migration.sql new file mode 100644 index 0000000..58d005e --- /dev/null +++ b/packages/db/prisma/migrations/20260401000000_active_sessions/migration.sql @@ -0,0 +1,36 @@ +-- CreateTable: active_sessions (JWT session registry) +-- Supports concurrent-session limits and manual session revocation. +-- Using IF NOT EXISTS guards for idempotency when table was pre-created via db push. + +CREATE TABLE IF NOT EXISTS "active_sessions" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "jti" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastSeenAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userAgent" TEXT, + "ipAddress" TEXT, + + CONSTRAINT "active_sessions_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX IF NOT EXISTS "active_sessions_jti_key" ON "active_sessions"("jti"); + +-- CreateIndex +CREATE INDEX IF NOT EXISTS "active_sessions_userId_createdAt_idx" ON "active_sessions"("userId", "createdAt"); + +-- AddForeignKey (skip if already exists) +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE constraint_name = 'active_sessions_userId_fkey' + AND table_name = 'active_sessions' + ) THEN + ALTER TABLE "active_sessions" + ADD CONSTRAINT "active_sessions_userId_fkey" + FOREIGN KEY ("userId") REFERENCES "users"("id") + ON DELETE CASCADE ON UPDATE CASCADE; + END IF; +END $$;