import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import type { Request } from 'express'; import { PrismaService } from '../prisma/prisma.service'; import type { AuthenticatedUser } from '../auth/auth.types'; import { REQUIRED_PERMISSION_KEY, type PermissionAction } from './permission.decorator'; type PermissionRequirement = { module: string; action: PermissionAction; }; type PermissionRow = { id?: unknown; values?: unknown; }; const fallbackRolePermissions: Record>>> = { admin: { campaigns: { view: true, edit: true, delete: true, manage: true }, templates: { view: true, edit: true, delete: true, manage: true }, users: { view: true, edit: true, delete: true, manage: true }, roles: { view: true, edit: true, delete: true, manage: true }, contacts: { view: true, edit: true, delete: true, manage: true }, conversations: { view: true, edit: true, delete: true, manage: true }, analytics: { view: true, edit: true, delete: true, manage: true }, settings: { view: true, edit: true, delete: true, manage: true }, }, editor: { campaigns: { view: true, edit: true, delete: false, manage: false }, templates: { view: true, edit: true, delete: false, manage: false }, contacts: { view: true, edit: false, delete: false, manage: false }, conversations: { view: true, edit: true, delete: false, manage: false }, analytics: { view: true, edit: false, delete: false, manage: false }, }, agent: { campaigns: { view: true, edit: false, delete: false, manage: false }, templates: { view: true, edit: false, delete: false, manage: false }, contacts: { view: true, edit: true, delete: false, manage: false }, conversations: { view: true, edit: true, delete: false, manage: false }, analytics: { view: true, edit: false, delete: false, manage: false }, }, }; @Injectable() export class PermissionGuard implements CanActivate { constructor( private readonly reflector: Reflector, private readonly prisma: PrismaService, ) {} async canActivate(context: ExecutionContext): Promise { const requirement = this.reflector.getAllAndOverride( REQUIRED_PERMISSION_KEY, [context.getHandler(), context.getClass()], ); if (!requirement) { return true; } const request = context.switchToHttp().getRequest(); const authUser = request.user; if (!authUser?.sub) { throw new ForbiddenException('Missing authenticated user context'); } const user = await this.prisma.user.findUnique({ where: { id: authUser.sub }, select: { role: { select: { key: true, permissionsJson: true, }, }, }, }); if (!user?.role) { throw new ForbiddenException('No role assigned to this account'); } if (user.role.key === 'admin') { return true; } const allowed = this.hasPermissionFromRoleJson(user.role.permissionsJson, requirement.module, requirement.action) || this.hasFallbackPermission(user.role.key, requirement.module, requirement.action); if (!allowed) { throw new ForbiddenException(`Missing permission: ${requirement.module}.${requirement.action}`); } return true; } private hasPermissionFromRoleJson( permissionsJson: unknown, module: string, action: PermissionAction, ) { if (!Array.isArray(permissionsJson)) { return false; } const row = permissionsJson.find((item): item is PermissionRow => { if (!item || typeof item !== 'object') return false; return typeof (item as PermissionRow).id === 'string' && (item as PermissionRow).id === module; }); if (!row || !row.values || typeof row.values !== 'object') { return false; } const value = (row.values as Record)[action]; return value === true; } private hasFallbackPermission(roleKey: string, module: string, action: PermissionAction) { return fallbackRolePermissions[roleKey]?.[module]?.[action] === true; } }