import { Injectable } from '@nestjs/common'; import { DatabaseService } from '../database/database.service'; import { Prisma } from '../generated/prisma/client'; import { readPrincipal } from './session.store'; import { AppError } from '../common/errors/app-error'; import type { Principal } from './identity.types'; import type { Permission } from './permissions'; @Injectable() export class AccessStore { constructor(private readonly db: DatabaseService) {} async mutate( actor: Principal, permission: Permission | null, work: (tx: Prisma.TransactionClient, current: Principal) => Promise, lockOrganization = true, ): Promise { try { return await this.db.$transaction(async (tx) => { if (lockOrganization) { await tx.$queryRaw`SELECT id FROM organizations WHERE id = ${actor.organizationId}::uuid FOR UPDATE`; } const current = await readPrincipal(tx, actor.sessionId); if ( current.organizationId !== actor.organizationId || current.userId !== actor.userId ) { throw new AppError('SCOPE_DENIED'); } if (permission && !current.permissions.includes(permission)) throw new AppError('ACCESS_DENIED'); return work(tx, current); }); } catch (error) { if ( error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002' ) { throw new AppError('RECORD_CONFLICT'); } throw error; } } }