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>
This commit is contained in:
@@ -6,26 +6,79 @@ import {
|
||||
} 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 request = context.switchToHttp().getRequest<{
|
||||
user?: { role?: string };
|
||||
}>();
|
||||
const role = request.user?.role as UserRole | undefined;
|
||||
if (!role || !roles.includes(role)) {
|
||||
throw new ForbiddenException('دسترسی به این بخش برای نقش شما مجاز نیست');
|
||||
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;
|
||||
}
|
||||
return true;
|
||||
|
||||
throw new ForbiddenException('دسترسی به این بخش برای نقش شما مجاز نیست');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user