Files
trackgo-be/src/modules/field/shared/field-privilege.guard.ts
T
shancheas 51db4f4a4d Refactor privilege management to support hierarchical privilege keys
- Updated privilege key structure to use a 3- or 4-part dotted hierarchy (e.g., `GROUP.PARENT.MODULE`).
- Modified the `RequirePrivilege` decorator to accept multiple keys, allowing for OR logic in privilege checks.
- Enhanced `PrivilegesGuard` to validate against multiple privilege keys, improving access control logic.
- Created migration scripts to update existing privilege keys in the database to the new format.
- Updated related services, controllers, and tests to accommodate the new privilege key structure and validation logic.
2026-09-01 13:14:58 +07:00

93 lines
2.5 KiB
TypeScript

import {
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import type { AuthUser } from '../../../common/auth/auth-user';
import type { PrivilegeAction } from '../../privileges/privilege-action';
import { PrivilegesService } from '../../privileges/privileges.service';
import {
fieldPrivilegeKeys,
isFieldPurpose,
type FieldPurpose,
type FieldResource,
} from './field-purpose';
import {
REQUIRE_FIELD_PRIVILEGE_KEY,
type RequireFieldPrivilegeMeta,
} from './field-privilege.decorator';
@Injectable()
export class FieldPrivilegeGuard implements CanActivate {
constructor(
private readonly reflector: Reflector,
private readonly privilegesService: PrivilegesService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const required = this.reflector.getAllAndOverride<
RequireFieldPrivilegeMeta | undefined
>(REQUIRE_FIELD_PRIVILEGE_KEY, [context.getHandler(), context.getClass()]);
if (!required) {
return true;
}
const request = context.switchToHttp().getRequest<{
user?: AuthUser;
body?: { purpose?: string };
query?: { purpose?: string };
}>();
const user = request.user;
if (!user) {
throw new UnauthorizedException();
}
if (user.isSuperadmin) {
return true;
}
const purposeRaw = request.body?.purpose ?? request.query?.purpose;
const allowed = await this.allowedPurposes(
user.id,
required.resource,
required.action,
);
if (purposeRaw !== undefined && purposeRaw !== '') {
if (!isFieldPurpose(purposeRaw)) {
throw new ForbiddenException('Insufficient privilege');
}
if (!allowed.includes(purposeRaw)) {
throw new ForbiddenException('Insufficient privilege');
}
return true;
}
if (allowed.length === 0) {
throw new ForbiddenException('Insufficient privilege');
}
return true;
}
async allowedPurposes(
userId: string,
resource: FieldResource,
action: PrivilegeAction,
): Promise<FieldPurpose[]> {
const purposes: FieldPurpose[] = ['sales', 'logistics'];
const matches: FieldPurpose[] = [];
for (const purpose of purposes) {
const ok = await this.privilegesService.checkAnyPermission(
userId,
fieldPrivilegeKeys(resource, purpose),
action,
);
if (ok) {
matches.push(purpose);
}
}
return matches;
}
}