mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 18:20:57 +00:00
Merge branch 'dev' into freight/nati-2
This commit is contained in:
@@ -191,6 +191,24 @@ const POSITION_TYPE_PERMISSIONS = [
|
||||
},
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Audit trail access.
|
||||
*
|
||||
* Read-only by design: `audit_logs` rows are written solely by
|
||||
* `AuditInterceptor` and the module exposes no create/update/delete route, so
|
||||
* there is deliberately no `:create` / `:update` / `:delete` twin to grant.
|
||||
* The matching `:read` key is derived automatically by
|
||||
* `deriveReadPermissions` below.
|
||||
*/
|
||||
const AUDIT_LOG_PERMISSIONS = [
|
||||
{
|
||||
id: "52b2cdef-5313-4954-8c69-0f8c58ea2c1e",
|
||||
key: "edr_freight_app:audit_log:view",
|
||||
name: { am: "የኦዲት መዝገብ ይመልከቱ", en: "View audit logs" },
|
||||
applicationKey: EDR_FREIGHT_APPLICATION.key,
|
||||
},
|
||||
] as const;
|
||||
|
||||
const EDR_FREIGHT_VIEWABLE_PERMISSIONS = [
|
||||
...EMPLOYEE_REGISTRATION_PERMISSIONS,
|
||||
...ROLE_ASSIGNMENT_PERMISSIONS,
|
||||
@@ -198,6 +216,7 @@ const EDR_FREIGHT_VIEWABLE_PERMISSIONS = [
|
||||
...HIERARCHY_POSITION_PERMISSIONS,
|
||||
...HIERARCHY_EMPLOYEE_ASSIGNMENT_PERMISSIONS,
|
||||
...POSITION_TYPE_PERMISSIONS,
|
||||
...AUDIT_LOG_PERMISSIONS,
|
||||
...BOOKING_RULE_ENGINE_PERMISSIONS,
|
||||
];
|
||||
|
||||
|
||||
@@ -707,6 +707,29 @@ export const FLEET_RAIL_PERMISSIONS: FreightPermissionSeed[] = [
|
||||
"edr_freight_app:trains:assign_wagons",
|
||||
"Assign wagons to train",
|
||||
),
|
||||
// Granular splits of trains:update / trains:delete for the train-builder
|
||||
// detail page's Actions menu — each item gets its own grant instead of
|
||||
// sharing the coarse update/delete keys.
|
||||
perm(
|
||||
"e1c00001-0001-4000-8000-000000000006",
|
||||
"edr_freight_app:trains:change_locomotives",
|
||||
"Change train locomotives",
|
||||
),
|
||||
perm(
|
||||
"e1c00001-0001-4000-8000-000000000007",
|
||||
"edr_freight_app:trains:change_yard",
|
||||
"Change train yard",
|
||||
),
|
||||
perm(
|
||||
"e1c00001-0001-4000-8000-000000000008",
|
||||
"edr_freight_app:trains:toggle_active",
|
||||
"Activate or deactivate train",
|
||||
),
|
||||
perm(
|
||||
"e1c00001-0001-4000-8000-000000000009",
|
||||
"edr_freight_app:trains:disband",
|
||||
"Disband train",
|
||||
),
|
||||
perm(
|
||||
"e1d00001-0001-4000-8000-000000000001",
|
||||
"edr_freight_app:routes:view",
|
||||
@@ -1622,11 +1645,24 @@ export const FREIGHT_PERMS = {
|
||||
dispatch: "edr_freight_app:train_scheduling:dispatch",
|
||||
markPaid: "edr_freight_app:train_scheduling:mark_paid",
|
||||
expireBooking: "edr_freight_app:train_scheduling:expire_booking",
|
||||
/**
|
||||
* Edit a schedule's operational run numbers (train + voyage) before
|
||||
* dispatch. Separate from `update`: these numbers are what yards and
|
||||
* customs quote, so changing them is narrower than general scheduling edits.
|
||||
*/
|
||||
editTrainNumber: "edr_freight_app:train_scheduling:edit_train_number",
|
||||
},
|
||||
fleet: {
|
||||
view: "edr_freight_app:fleet:view",
|
||||
manage: "edr_freight_app:fleet:manage",
|
||||
},
|
||||
/**
|
||||
* Audit trail. View-only: the module has no write routes, so this is the
|
||||
* only key it needs — see AUDIT_LOG_PERMISSIONS in edr-freight.seed.ts.
|
||||
*/
|
||||
auditLog: {
|
||||
view: "edr_freight_app:audit_log:view",
|
||||
},
|
||||
admin: "edr_freight_app:admin",
|
||||
ruleEngine: {
|
||||
view: (slug: RuleEngineResourceSlug) =>
|
||||
@@ -1732,6 +1768,10 @@ export const FREIGHT_PERMS = {
|
||||
update: "edr_freight_app:trains:update",
|
||||
delete: "edr_freight_app:trains:delete",
|
||||
assignWagons: "edr_freight_app:trains:assign_wagons",
|
||||
changeLocomotives: "edr_freight_app:trains:change_locomotives",
|
||||
changeYard: "edr_freight_app:trains:change_yard",
|
||||
toggleActive: "edr_freight_app:trains:toggle_active",
|
||||
disband: "edr_freight_app:trains:disband",
|
||||
},
|
||||
routes: {
|
||||
view: "edr_freight_app:routes:view",
|
||||
@@ -1895,9 +1935,6 @@ export const FREIGHT_PERMS = {
|
||||
manage: "edr_freight_app:settings:support_content:manage",
|
||||
},
|
||||
},
|
||||
audit: {
|
||||
view: "edr_freight_app:audit:view",
|
||||
},
|
||||
support: {
|
||||
agentView: "edr_freight_app:support:agent_view",
|
||||
agentSend: "edr_freight_app:support:agent_send",
|
||||
@@ -2049,6 +2086,10 @@ const FLEET_GRANULAR_KEYS: string[] = [
|
||||
FREIGHT_PERMS.trains.update,
|
||||
FREIGHT_PERMS.trains.delete,
|
||||
FREIGHT_PERMS.trains.assignWagons,
|
||||
FREIGHT_PERMS.trains.changeLocomotives,
|
||||
FREIGHT_PERMS.trains.changeYard,
|
||||
FREIGHT_PERMS.trains.toggleActive,
|
||||
FREIGHT_PERMS.trains.disband,
|
||||
FREIGHT_PERMS.routes.view,
|
||||
FREIGHT_PERMS.routes.create,
|
||||
FREIGHT_PERMS.routes.update,
|
||||
@@ -2129,6 +2170,7 @@ export const ROLE_PERMISSION_PRESETS = {
|
||||
FREIGHT_PERMS.trainScheduling.dispatch,
|
||||
FREIGHT_PERMS.trainScheduling.markPaid,
|
||||
FREIGHT_PERMS.trainScheduling.expireBooking,
|
||||
FREIGHT_PERMS.trainScheduling.editTrainNumber,
|
||||
FREIGHT_PERMS.fleet.view,
|
||||
FREIGHT_PERMS.fleet.manage,
|
||||
...FLEET_GRANULAR_KEYS,
|
||||
@@ -2284,7 +2326,8 @@ export const POSITION_PERMISSION_PRESETS = {
|
||||
FREIGHT_PERMS.payments.view,
|
||||
]),
|
||||
// Director additionally manages train scheduling + rail fleet (same block the
|
||||
// operation officer/chief hold), on top of the approval-chain role preset.
|
||||
// operation officer/chief hold), on top of the approval-chain role preset,
|
||||
// and carries the same full warehouse authority the chief tier holds.
|
||||
director: dedupe([
|
||||
...ROLE_PERMISSION_PRESETS.director,
|
||||
FREIGHT_PERMS.trainScheduling.view,
|
||||
@@ -2293,9 +2336,22 @@ export const POSITION_PERMISSION_PRESETS = {
|
||||
FREIGHT_PERMS.trainScheduling.cancel,
|
||||
FREIGHT_PERMS.trainScheduling.reschedule,
|
||||
FREIGHT_PERMS.trainScheduling.rulesManage,
|
||||
FREIGHT_PERMS.trainScheduling.editTrainNumber,
|
||||
FREIGHT_PERMS.fleet.view,
|
||||
FREIGHT_PERMS.fleet.manage,
|
||||
...FLEET_GRANULAR_KEYS,
|
||||
// Warehouse — full CRUD, matching the chief tier. Unlike the dispatcher,
|
||||
// the director also owns the allocation and fee rules themselves.
|
||||
FREIGHT_PERMS.warehouseDashboard.view,
|
||||
...Object.values(FREIGHT_PERMS.warehouses),
|
||||
...Object.values(FREIGHT_PERMS.warehouseYards),
|
||||
...Object.values(FREIGHT_PERMS.warehouseZones),
|
||||
...Object.values(FREIGHT_PERMS.warehouseAllocationRules),
|
||||
...Object.values(FREIGHT_PERMS.warehouseFeeRules),
|
||||
...Object.values(FREIGHT_PERMS.warehouseInventory),
|
||||
...Object.values(FREIGHT_PERMS.warehouseInspectionReports),
|
||||
...Object.values(FREIGHT_PERMS.interchangeDocuments),
|
||||
...Object.values(FREIGHT_PERMS.warehouseFeeInvoices),
|
||||
]),
|
||||
ceo: dedupe([...ROLE_PERMISSION_PRESETS.ceo]),
|
||||
ethiopianGl: dedupe([...ROLE_PERMISSION_PRESETS.glEthiopia]),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
Application,
|
||||
Organization,
|
||||
Permission,
|
||||
Position,
|
||||
@@ -8,7 +9,23 @@ import {
|
||||
} from '@tria-plc/iamapi-common';
|
||||
import { DataSource, EntityManager, In } from 'typeorm';
|
||||
|
||||
import { EDR_FREIGHT_POSITIONS } from './edr-freight.seed';
|
||||
import { EDR_FREIGHT_APPLICATION, EDR_FREIGHT_POSITIONS } from './edr-freight.seed';
|
||||
|
||||
/**
|
||||
* Turn a permission key into a readable fallback name for a row this seeder has
|
||||
* to mint itself: `edr_freight_app:train_scheduling:edit_train_number` becomes
|
||||
* "Train scheduling: edit train number". Only used for keys absent from the
|
||||
* catalog — a key that IS catalogued keeps its curated Amharic/English name.
|
||||
*/
|
||||
const nameForKey = (key: string): { en: string } => {
|
||||
const [, ...rest] = key.split(':');
|
||||
const [resource, ...action] = rest;
|
||||
const humanize = (s: string) => s.replace(/_/g, ' ');
|
||||
const label = action.length
|
||||
? `${humanize(resource)}: ${humanize(action.join(' '))}`
|
||||
: humanize(resource);
|
||||
return { en: label.charAt(0).toUpperCase() + label.slice(1) };
|
||||
};
|
||||
|
||||
const SEED_FLAG = 'SEED_EDR_ORG';
|
||||
const EDR_ORG_KEY = 'edr_freight';
|
||||
@@ -83,7 +100,19 @@ export class FreightPositionsSeeder {
|
||||
);
|
||||
}
|
||||
|
||||
/** Resolve every permission key referenced by any position to its id. */
|
||||
/**
|
||||
* Resolve every permission key referenced by any position to its id, minting
|
||||
* the rows that do not exist yet.
|
||||
*
|
||||
* The position presets draw from FREIGHT_PERMS (the registry), which is
|
||||
* broader than the EDR_FREIGHT_PERMISSIONS catalog EdrOrgSeeder inserts —
|
||||
* module keys like `train_scheduling:*` live only in the registry. So every
|
||||
* newly-added preset key would otherwise abort boot with
|
||||
* `missing_permissions:<key>` until someone hand-inserted it. Ensuring them
|
||||
* here keeps this seeder self-sufficient: it declares the keys it needs, so
|
||||
* it is the one that guarantees they exist. Same approach, and the same
|
||||
* id-less insert reasoning, as FreightNotificationPermissionsSeeder.
|
||||
*/
|
||||
private async loadPermissionIds(
|
||||
manager: EntityManager,
|
||||
): Promise<Map<string, string>> {
|
||||
@@ -91,16 +120,54 @@ export class FreightPositionsSeeder {
|
||||
...new Set(EDR_FREIGHT_POSITIONS.flatMap((p) => p.permissionKeys)),
|
||||
];
|
||||
|
||||
const permissions = await manager.getRepository(Permission).find({
|
||||
where: { key: In(keys) },
|
||||
select: { id: true, key: true },
|
||||
});
|
||||
|
||||
const map = new Map(permissions.map((p) => [p.key, p.id as string]));
|
||||
const read = async () => {
|
||||
const rows = await manager.getRepository(Permission).find({
|
||||
where: { key: In(keys) },
|
||||
select: { id: true, key: true },
|
||||
});
|
||||
return new Map(rows.map((p) => [p.key, p.id as string]));
|
||||
};
|
||||
|
||||
let map = await read();
|
||||
const missing = keys.filter((key) => !map.has(key));
|
||||
if (missing.length > 0) {
|
||||
throw new Error(`missing_permissions:${missing.join(',')}`);
|
||||
if (missing.length === 0) {
|
||||
return map;
|
||||
}
|
||||
|
||||
const application = await manager.getRepository(Application).findOne({
|
||||
where: { key: EDR_FREIGHT_APPLICATION.key },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!application?.id) {
|
||||
throw new Error(`missing_application:${EDR_FREIGHT_APPLICATION.key}`);
|
||||
}
|
||||
|
||||
// Ids are left to the column default and never sent: iam.permissions has
|
||||
// two unique columns (PK id, UQ key) and ON CONFLICT can only target one,
|
||||
// so a hand-minted id already owned by a retired key would slip past
|
||||
// ON CONFLICT (key) and die on the PK.
|
||||
await manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(Permission)
|
||||
.values(
|
||||
missing.map((key) => ({
|
||||
key,
|
||||
name: nameForKey(key),
|
||||
applicationId: application.id as string,
|
||||
})),
|
||||
)
|
||||
.orIgnore()
|
||||
.execute();
|
||||
|
||||
this.logger.log(
|
||||
`Seeded ${missing.length} permission(s) referenced by positions but absent from the catalog: ${missing.join(', ')}`,
|
||||
);
|
||||
|
||||
map = await read();
|
||||
const stillMissing = keys.filter((key) => !map.has(key));
|
||||
if (stillMissing.length > 0) {
|
||||
throw new Error(`missing_permissions:${stillMissing.join(',')}`);
|
||||
}
|
||||
|
||||
return map;
|
||||
|
||||
Reference in New Issue
Block a user