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>
85 lines
2.5 KiB
TypeScript
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('دسترسی به این بخش برای نقش شما مجاز نیست');
|
|
}
|
|
}
|