feature: phase1 - org-type navigation, Cases permissions, staff filtering, and route guards.
This commit is contained in:
25
backend/src/common/guards/clinic-org.guard.ts
Normal file
25
backend/src/common/guards/clinic-org.guard.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { PrismaService } from '../../../prisma/prisma.service';
|
||||
import { assertClinicOrganization } from '../../common/organization-type';
|
||||
|
||||
@Injectable()
|
||||
export class ClinicOrgGuard implements CanActivate {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<{ user?: { organizationId?: string } }>();
|
||||
const organizationId = request.user?.organizationId;
|
||||
|
||||
if (!organizationId) {
|
||||
throw new UnauthorizedException('Organization is not selected');
|
||||
}
|
||||
|
||||
await assertClinicOrganization(this.prisma, organizationId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
100
backend/src/common/organization-type.ts
Normal file
100
backend/src/common/organization-type.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { ForbiddenException, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
import { ALL_TAB_PERMISSIONS, normalizeTabPermissions } from './permissions';
|
||||
|
||||
export type OrganizationTypeName = 'CLINIC' | 'LAB';
|
||||
|
||||
const CLINIC_ONLY_PERMISSIONS = new Set<string>([
|
||||
'TAB_PATIENTS_READ',
|
||||
'TAB_PATIENTS_EDIT',
|
||||
'TAB_APPOINTMENTS_READ',
|
||||
'TAB_APPOINTMENTS_EDIT',
|
||||
'TAB_TREATMENT_READ',
|
||||
'TAB_TREATMENT_EDIT',
|
||||
]);
|
||||
|
||||
const LAB_ONLY_PERMISSIONS = new Set<string>(['TAB_CASES_READ', 'TAB_CASES_EDIT']);
|
||||
|
||||
const SHARED_PERMISSIONS = ALL_TAB_PERMISSIONS.filter(
|
||||
(p) => !CLINIC_ONLY_PERMISSIONS.has(p) && !LAB_ONLY_PERMISSIONS.has(p),
|
||||
);
|
||||
|
||||
export const CLINIC_TAB_PERMISSIONS = [
|
||||
...SHARED_PERMISSIONS,
|
||||
...CLINIC_ONLY_PERMISSIONS,
|
||||
] as const;
|
||||
|
||||
export const LAB_TAB_PERMISSIONS = [
|
||||
...SHARED_PERMISSIONS,
|
||||
...LAB_ONLY_PERMISSIONS,
|
||||
] as const;
|
||||
|
||||
const CLINIC_TAB_SET = new Set<string>(CLINIC_TAB_PERMISSIONS);
|
||||
const LAB_TAB_SET = new Set<string>(LAB_TAB_PERMISSIONS);
|
||||
|
||||
export function permissionsAllowedForOrgType(orgType: OrganizationTypeName): Set<string> {
|
||||
return orgType === 'LAB' ? LAB_TAB_SET : CLINIC_TAB_SET;
|
||||
}
|
||||
|
||||
export function filterPermissionsForOrgType(
|
||||
names: string[],
|
||||
orgType: OrganizationTypeName,
|
||||
): string[] {
|
||||
const allowed = permissionsAllowedForOrgType(orgType);
|
||||
return normalizeTabPermissions(names.filter((n) => allowed.has(n)));
|
||||
}
|
||||
|
||||
export function ownerPermissionsForOrgType(
|
||||
orgType: OrganizationTypeName,
|
||||
hasActivePlan: boolean,
|
||||
): string[] {
|
||||
if (hasActivePlan) {
|
||||
return orgType === 'LAB' ? [...LAB_TAB_PERMISSIONS] : [...CLINIC_TAB_PERMISSIONS];
|
||||
}
|
||||
|
||||
const readOnly = (perms: readonly string[]) =>
|
||||
normalizeTabPermissions(perms.filter((p) => p.endsWith('_READ')));
|
||||
|
||||
return orgType === 'LAB' ? readOnly(LAB_TAB_PERMISSIONS) : readOnly(CLINIC_TAB_PERMISSIONS);
|
||||
}
|
||||
|
||||
export async function getOrganizationTypeName(
|
||||
prisma: PrismaService,
|
||||
organizationId: string,
|
||||
): Promise<OrganizationTypeName> {
|
||||
const org = await prisma.organization.findUnique({
|
||||
where: { id: organizationId },
|
||||
select: { type: { select: { name: true } } },
|
||||
});
|
||||
|
||||
if (!org) {
|
||||
throw new NotFoundException('Organization not found');
|
||||
}
|
||||
|
||||
const name = org.type.name;
|
||||
if (name !== 'CLINIC' && name !== 'LAB') {
|
||||
throw new ForbiddenException('Unknown organization type');
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
export async function assertClinicOrganization(
|
||||
prisma: PrismaService,
|
||||
organizationId: string,
|
||||
): Promise<void> {
|
||||
const type = await getOrganizationTypeName(prisma, organizationId);
|
||||
if (type !== 'CLINIC') {
|
||||
throw new ForbiddenException('This action is only available for clinic organizations');
|
||||
}
|
||||
}
|
||||
|
||||
export async function assertLabOrganization(
|
||||
prisma: PrismaService,
|
||||
organizationId: string,
|
||||
): Promise<void> {
|
||||
const type = await getOrganizationTypeName(prisma, organizationId);
|
||||
if (type !== 'LAB') {
|
||||
throw new ForbiddenException('This action is only available for lab organizations');
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ export const ALL_TAB_PERMISSIONS = [
|
||||
'TAB_APPOINTMENTS_EDIT',
|
||||
'TAB_TREATMENT_READ',
|
||||
'TAB_TREATMENT_EDIT',
|
||||
'TAB_CASES_READ',
|
||||
'TAB_CASES_EDIT',
|
||||
'TAB_BILLING_READ',
|
||||
'TAB_BILLING_EDIT',
|
||||
'TAB_REPORTS_READ',
|
||||
@@ -40,6 +42,7 @@ const EDIT_TO_READ: Record<string, string> = {
|
||||
TAB_STAFF_EDIT: 'TAB_STAFF_READ',
|
||||
TAB_ORGANIZATIONS_EDIT: 'TAB_ORGANIZATIONS_READ',
|
||||
TAB_TREATMENT_EDIT: 'TAB_TREATMENT_READ',
|
||||
TAB_CASES_EDIT: 'TAB_CASES_READ',
|
||||
TAB_BILLING_EDIT: 'TAB_BILLING_READ',
|
||||
TAB_REPORTS_EDIT: 'TAB_REPORTS_READ',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user