From 2b52b1ceddf0fcb02b79f5a02ee352bfbd8c6486 Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Sun, 26 Apr 2026 23:01:22 +0200 Subject: [PATCH] feat(ST-703): auth and Prisma client singleton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/prisma.ts: PrismaClient via PrismaPg adapter and pg.Pool, same pattern as Scrum4Me's lib/prisma.ts - src/auth.ts: getAuth() resolves SCRUM4ME_TOKEN once, caches { userId, username, isDemo }. requireWriteAccess() throws PermissionDeniedError for demo tokens — write tools call this before any DB mutation Co-Authored-By: Claude Opus 4.7 (1M context) --- src/auth.ts | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/prisma.ts | 15 +++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 src/auth.ts create mode 100644 src/prisma.ts diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 0000000..3756045 --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,51 @@ +import { createHash } from 'crypto' +import { prisma } from './prisma.js' + +export type AuthContext = { + userId: string + username: string + isDemo: boolean +} + +let cached: AuthContext | null = null + +export async function getAuth(): Promise { + if (cached) return cached + + const token = process.env.SCRUM4ME_TOKEN + if (!token) { + throw new Error('SCRUM4ME_TOKEN is not set — see .env.example') + } + + const tokenHash = createHash('sha256').update(token).digest('hex') + const apiToken = await prisma.apiToken.findUnique({ + where: { token_hash: tokenHash }, + include: { user: true }, + }) + + if (!apiToken || apiToken.revoked_at) { + throw new Error('SCRUM4ME_TOKEN is invalid or revoked') + } + + cached = { + userId: apiToken.user_id, + username: apiToken.user.username, + isDemo: apiToken.user.is_demo, + } + return cached +} + +export class PermissionDeniedError extends Error { + constructor(message = 'Demo accounts cannot perform write operations') { + super(message) + this.name = 'PermissionDeniedError' + } +} + +export async function requireWriteAccess(): Promise { + const auth = await getAuth() + if (auth.isDemo) { + throw new PermissionDeniedError() + } + return auth +} diff --git a/src/prisma.ts b/src/prisma.ts new file mode 100644 index 0000000..ca9eab7 --- /dev/null +++ b/src/prisma.ts @@ -0,0 +1,15 @@ +import { PrismaClient } from '@prisma/client' +import { Pool } from 'pg' +import { PrismaPg } from '@prisma/adapter-pg' + +function createClient(): PrismaClient { + const url = process.env.DATABASE_URL + if (!url) { + throw new Error('DATABASE_URL is not set — see .env.example') + } + const pool = new Pool({ connectionString: url }) + const adapter = new PrismaPg(pool) + return new PrismaClient({ adapter, log: ['error'] }) +} + +export const prisma = createClient()