import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { OrgRole } from '@prisma/client'; import { PrismaService } from '../../prisma/prisma.service'; import { ROLES_KEY } from '../decorators/roles.decorator'; import { Principal, ROLE_RANK } from '../principal'; // Runs after UserOrApiKeyGuard on org-scoped routes (/orgs/:orgId/...). // Users: must be a member of the org, with at least the @Roles() role if present. // API keys: must belong to the org (scopes are checked by ScopesGuard). @Injectable() export class OrgRolesGuard implements CanActivate { constructor( private readonly reflector: Reflector, private readonly prisma: PrismaService, ) {} async canActivate(context: ExecutionContext): Promise { const req = context.switchToHttp().getRequest(); const principal: Principal | undefined = req.user; const orgId: string | undefined = req.params?.orgId; if (!principal || !orgId) { throw new ForbiddenException('Organization scope required'); } if (principal.type === 'apiKey') { if (principal.orgId !== orgId) { throw new ForbiddenException('API key does not belong to this organization'); } return true; } const membership = await this.prisma.orgMembership.findUnique({ where: { orgId_userId: { orgId, userId: principal.userId } }, }); if (!membership) { throw new ForbiddenException('Not a member of this organization'); } const required = this.reflector.getAllAndOverride(ROLES_KEY, [ context.getHandler(), context.getClass(), ]); if (required && ROLE_RANK[membership.role] < ROLE_RANK[required]) { throw new ForbiddenException(`Requires ${required} role`); } req.membership = membership; return true; } }