Merge pull request #923 from Tria-plc/freight_feature/usermanagement

Freight feature/usermanagement
This commit is contained in:
marshal
2026-07-23 00:02:55 +03:00
committed by GitHub
39 changed files with 3644 additions and 362 deletions

View File

@@ -0,0 +1,58 @@
import { describe, expect, it } from "vitest";
import type { AuthUser } from "@/auth/types";
import { canApproveContractStep } from "./permissions";
const withPositionType = (typeKey: string): AuthUser => ({
employee: [{ positions: [{ positionType: { key: typeKey } }] }],
});
const withRole = (roleKey: string): AuthUser => ({ roles: [{ key: roleKey }] });
const withPermission = (permKey: string): AuthUser => ({
permissionKeys: [permKey],
});
describe("canApproveContractStep", () => {
it("shows to the matching position type only", () => {
const chief = withPositionType("-marketing-chief");
expect(canApproveContractStep(chief, "-marketing-chief")).toBe(true);
// a marketing officer must NOT see the chief step's buttons
expect(canApproveContractStep(chief, "-marketing-director-")).toBe(false);
});
it("lets super/org admins action any step", () => {
expect(canApproveContractStep(withRole("super_admin"), "anything")).toBe(
true,
);
expect(
canApproveContractStep(withRole("organization_admin"), "-marketing-chief"),
).toBe(true);
});
it("resolves legacy chain roles via their position-type aliases", () => {
const director = withPositionType("operation-director");
expect(canApproveContractStep(director, "DIRECTOR")).toBe(true);
expect(canApproveContractStep(director, "CEO")).toBe(false);
});
it("honours the role's own legacy approve permission", () => {
const staff = withPermission(
"edr_freight_app:contracts:approve_director",
);
expect(canApproveContractStep(staff, "DIRECTOR")).toBe(true);
});
it("does NOT show to holders of an unrelated approve permission", () => {
// the dropped blanket fallback: a line-staff approver is not a chief
const lineStaff = withPermission(
"edr_freight_app:contracts:approve_line_staff",
);
expect(canApproveContractStep(lineStaff, "-marketing-chief")).toBe(false);
});
it("returns false without a user or role", () => {
expect(canApproveContractStep(null, "-marketing-chief")).toBe(false);
expect(canApproveContractStep(withPositionType("x"), null)).toBe(false);
});
});

View File

@@ -240,12 +240,6 @@ export const FREIGHT_PERMS = {
cancel: "edr_freight_app:warehouse_fee_invoices:cancel",
pay: "edr_freight_app:warehouse_fee_invoices:pay",
},
config: {
contractValidity: {
view: "edr_freight_app:config:contract_validity:view",
manage: "edr_freight_app:config:contract_validity:manage",
},
},
settings: {
fileUpload: {
view: "edr_freight_app:settings:file_upload:view",
@@ -420,6 +414,53 @@ export function hasPermission(
return getPermissionKeys(user).includes(key);
}
// Legacy chain roles predate position types; map each to the position types
// that stand in for it. Mirror of the API's LEGACY_ROLE_POSITION_TYPES so the
// button visibility matches what the approve/reject endpoint will accept.
const LEGACY_ROLE_POSITION_TYPES: Record<string, string[]> = {
LINE_STAFF: ["employee", "teamLeader", "officeHead", "recordOfficer"],
DIRECTOR: ["director", "operation-director"],
CEO: ["chief", "deputy"],
};
const CONTRACT_APPROVE_ROLE_PERMISSION: Record<string, string> = {
LINE_STAFF: FREIGHT_PERMS.contracts.approveLineStaff,
DIRECTOR: FREIGHT_PERMS.contracts.approveDirector,
CEO: FREIGHT_PERMS.contracts.approveCeo,
};
/**
* Can this user action a contract approval step requiring `requiredRole`?
*
* `requiredRole` is an `iam.position_types.key` (the role vocabulary approval
* chains are configured in), or a legacy LINE_STAFF/DIRECTOR/CEO string. Used
* to show Approve/Reject only to the step's actual approver — a chief step
* shows only to a chief, a marketing-officer step only to that officer.
*
* Deliberately STRICTER than the API's `assertCanApproveContractStep`, which
* also lets through anyone holding any contract-approve permission (a fallback
* for delegates whose token omits the position type). That blanket is what made
* every approver see the button, so it is dropped here: the visibility rule is
* admin OR the matching position type (direct / legacy alias) OR the role's own
* legacy approve permission. The server still guards the mutation.
*/
export function canApproveContractStep(
user: AuthUser | null | undefined,
requiredRole: string | null | undefined,
): boolean {
if (!user || !requiredRole) return false;
if (isFreightApprovalAdmin(user)) return true;
const positionTypes = getPositionTypeKeys(user);
if (positionTypes.includes(requiredRole)) return true;
const aliases = LEGACY_ROLE_POSITION_TYPES[requiredRole] ?? [];
if (aliases.some((alias) => positionTypes.includes(alias))) return true;
const legacyPermission = CONTRACT_APPROVE_ROLE_PERMISSION[requiredRole];
return Boolean(legacyPermission && hasPermission(user, legacyPermission));
}
export function canAccessBookings(user: AuthUser | null | undefined): boolean {
return hasPermission(user, FREIGHT_PERMS.bookings.view);
}