feat(ST-703): auth and Prisma client singleton
- 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) <noreply@anthropic.com>
This commit is contained in:
parent
992a4ad5e1
commit
2b52b1cedd
2 changed files with 66 additions and 0 deletions
51
src/auth.ts
Normal file
51
src/auth.ts
Normal file
|
|
@ -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<AuthContext> {
|
||||
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<AuthContext> {
|
||||
const auth = await getAuth()
|
||||
if (auth.isDemo) {
|
||||
throw new PermissionDeniedError()
|
||||
}
|
||||
return auth
|
||||
}
|
||||
15
src/prisma.ts
Normal file
15
src/prisma.ts
Normal file
|
|
@ -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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue