Files
CRM_Montzar/apps/api/src/auth/roles.guard.ts
T
MahdiandCursor f7b07f6b7a feat: owner read-only view-as from team users table
Allow org owners to inspect any member's data scope via dual JWT; server blocks mutations and team page stays reachable for return.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-17 19:57:26 +03:30

85 lines
2.5 KiB
TypeScript

import {
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import type { UserRole } from '@mento/shared';
import type { Actor } from '../common/record-scope';
import { ROLES_KEY } from './roles.decorator';
const VIEW_AS_MESSAGE =
'در حالت مشاهده به‌جای کاربر دیگر امکان تغییر وجود ندارد.';
/** خواندن‌های صفحهٔ تیم وقتی مالک در حالت مشاهده است. */
function isTeamSettingsRead(method: string, url: string): boolean {
if (method.toUpperCase() !== 'GET') return false;
const path = (url.split('?')[0] || '').replace(/\/+$/, '');
return (
/\/users(\/|$)/.test(path) ||
/\/org-roles(\/|$)/.test(path) ||
/\/teams(\/|$)/.test(path) ||
/\/organizations\/current/.test(path)
);
}
function isViewAsAllowedMutation(method: string, url: string): boolean {
const path = (url.split('?')[0] || '').replace(/\/+$/, '') || '/';
const m = method.toUpperCase();
if (m === 'POST' && (path.endsWith('/auth/view-as') || path.endsWith('/auth/view-as/stop'))) {
return true;
}
if (m === 'PATCH' && path.endsWith('/auth/me/prior-archive')) {
return true;
}
return false;
}
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<{
user?: Actor;
method?: string;
originalUrl?: string;
url?: string;
}>();
const method = (request.method || 'GET').toUpperCase();
const url = request.originalUrl || request.url || '';
const actor = request.user;
if (
actor?.viewAsReadOnly &&
method !== 'GET' &&
method !== 'HEAD' &&
method !== 'OPTIONS' &&
!isViewAsAllowedMutation(method, url)
) {
throw new ForbiddenException(VIEW_AS_MESSAGE);
}
const roles = this.reflector.getAllAndOverride<UserRole[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!roles?.length) return true;
const role = actor?.role as UserRole | undefined;
if (role && roles.includes(role)) return true;
if (
actor?.viewAsReadOnly &&
actor.impersonatorRole === 'owner' &&
roles.includes('owner') &&
isTeamSettingsRead(method, url)
) {
return true;
}
throw new ForbiddenException('دسترسی به این بخش برای نقش شما مجاز نیست');
}
}