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()