45 lines
1.5 KiB
TypeScript
45 lines
1.5 KiB
TypeScript
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<T>(
|
|
actor: Principal,
|
|
permission: Permission | null,
|
|
work: (tx: Prisma.TransactionClient, current: Principal) => Promise<T>,
|
|
lockOrganization = true,
|
|
): Promise<T> {
|
|
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;
|
|
}
|
|
}
|
|
}
|