mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 21:08:12 +00:00
Merge pull request #837 from Tria-plc/freight_feature/usermanagement
contrat nad booking modification
This commit is contained in:
@@ -7,16 +7,23 @@ const SUPER_ADMIN_ROLE = 'super_admin';
|
||||
const ORGANIZATION_ADMIN_ROLE = 'organization_admin';
|
||||
|
||||
type PermissionLike = { key?: string };
|
||||
type PositionTypeLike = { key?: string };
|
||||
type MeLikeUser = {
|
||||
roles?: { key?: string }[];
|
||||
permissions?: PermissionLike[];
|
||||
employee?:
|
||||
| {
|
||||
position?: { permissions?: PermissionLike[] };
|
||||
position?: {
|
||||
permissions?: PermissionLike[];
|
||||
positionType?: PositionTypeLike | null;
|
||||
};
|
||||
delegatedPositions?: { permissions?: PermissionLike[] }[];
|
||||
}
|
||||
| {
|
||||
positions?: { permissions?: PermissionLike[] }[];
|
||||
positions?: {
|
||||
permissions?: PermissionLike[];
|
||||
positionType?: PositionTypeLike | null;
|
||||
}[];
|
||||
}[]
|
||||
| null;
|
||||
};
|
||||
@@ -90,12 +97,110 @@ export function assertFreightPermission(
|
||||
throw new ForbiddenException(`Missing permission: ${permissionKey}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* The caller's IAM position-type keys (`iam.position_types.key`). A position
|
||||
* type is the platform's notion of a role — it is what carries permissions via
|
||||
* `iam.position_type_permissions` — and it is the vocabulary contract approval
|
||||
* chains are configured in.
|
||||
*
|
||||
* Mirrors `collectPermissionKeys`' handling of both JWT shapes: `employee` is
|
||||
* an object on some tokens and an array on others.
|
||||
*
|
||||
* Note delegated positions carry no `positionType` in the token, so a delegate
|
||||
* is not reachable here — they authorize through the permission arm of
|
||||
* `assertCanApproveContractStep` instead.
|
||||
*/
|
||||
export function collectPositionTypeKeys(
|
||||
user: MeLikeUser | null | undefined,
|
||||
): string[] {
|
||||
const employee = user?.employee;
|
||||
if (!employee) return [];
|
||||
|
||||
const keys = new Set<string>();
|
||||
|
||||
if (Array.isArray(employee)) {
|
||||
for (const emp of employee) {
|
||||
for (const pos of emp.positions ?? []) {
|
||||
if (pos.positionType?.key) keys.add(pos.positionType.key);
|
||||
}
|
||||
}
|
||||
return [...keys];
|
||||
}
|
||||
|
||||
if (employee.position?.positionType?.key) {
|
||||
keys.add(employee.position.positionType.key);
|
||||
}
|
||||
return [...keys];
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy chain roles predate position types. Historical `approval_rules` and
|
||||
* in-flight `contract_approval_steps` rows still carry them, so map each to the
|
||||
* position types that stand in for it. Without this, an approver holding a
|
||||
* modern position type could not action an older step.
|
||||
*/
|
||||
const LEGACY_ROLE_POSITION_TYPES: Record<string, string[]> = {
|
||||
LINE_STAFF: ['employee', 'teamLeader', 'officeHead', 'recordOfficer'],
|
||||
DIRECTOR: ['director', 'operation-director'],
|
||||
CEO: ['chief', 'deputy'],
|
||||
};
|
||||
|
||||
const APPROVE_ROLE_PERMISSION: Record<string, string> = {
|
||||
LINE_STAFF: FREIGHT_PERMS.bookings.approveLineStaff,
|
||||
DIRECTOR: FREIGHT_PERMS.bookings.approveDirector,
|
||||
CEO: FREIGHT_PERMS.bookings.approveCeo,
|
||||
};
|
||||
|
||||
const CONTRACT_APPROVE_ROLE_PERMISSION: Record<string, string> = {
|
||||
LINE_STAFF: FREIGHT_PERMS.contracts.approveLineStaff,
|
||||
DIRECTOR: FREIGHT_PERMS.contracts.approveDirector,
|
||||
CEO: FREIGHT_PERMS.contracts.approveCeo,
|
||||
};
|
||||
|
||||
const ANY_CONTRACT_APPROVE_PERMISSION = [
|
||||
FREIGHT_PERMS.contracts.approveLineStaff,
|
||||
FREIGHT_PERMS.contracts.approveDirector,
|
||||
FREIGHT_PERMS.contracts.approveCeo,
|
||||
];
|
||||
|
||||
/**
|
||||
* May this caller action a contract approval step requiring `requiredRole`?
|
||||
*
|
||||
* `requiredRole` is an `iam.position_types.key` for chains configured by an
|
||||
* admin, or one of the legacy LINE_STAFF/DIRECTOR/CEO strings for older rows.
|
||||
* A caller passes when any of these hold:
|
||||
*
|
||||
* - they are a super/organization admin (blanket bypass);
|
||||
* - their position type matches the step, directly or via a legacy alias;
|
||||
* - they hold the approve permission the legacy role maps to;
|
||||
* - they hold any contract approve permission — this covers delegates (whose
|
||||
* position type is absent from the token) and staff whose IAM position has
|
||||
* no position type assigned yet.
|
||||
*/
|
||||
export function assertCanApproveContractStep(
|
||||
user: TCurrentUser | MeLikeUser | null | undefined,
|
||||
requiredRole: string,
|
||||
): void {
|
||||
if (isFreightApprovalAdmin(user)) return;
|
||||
|
||||
const positionTypes = collectPositionTypeKeys(user);
|
||||
if (positionTypes.includes(requiredRole)) return;
|
||||
|
||||
const aliases = LEGACY_ROLE_POSITION_TYPES[requiredRole] ?? [];
|
||||
if (aliases.some((alias) => positionTypes.includes(alias))) return;
|
||||
|
||||
const legacyPermission = CONTRACT_APPROVE_ROLE_PERMISSION[requiredRole];
|
||||
if (legacyPermission && hasFreightPermission(user, legacyPermission)) return;
|
||||
|
||||
if (ANY_CONTRACT_APPROVE_PERMISSION.some((p) => hasFreightPermission(user, p))) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ForbiddenException(
|
||||
`You are not the required approver (${requiredRole}) for this step.`,
|
||||
);
|
||||
}
|
||||
|
||||
export function assertCanApproveBookingStep(
|
||||
user: TCurrentUser | MeLikeUser | null | undefined,
|
||||
requiredRole: string,
|
||||
|
||||
Reference in New Issue
Block a user