mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat(seed): implement local IAM baseline seeder with configurable settings
This commit is contained in:
@@ -42,8 +42,15 @@ JWT_REFRESH_TOKEN_EXPIRES=7d
|
||||
# IAM seed defaults (used by @tria-plc/iamapi-common on first boot)
|
||||
SUPER_ADMIN_EMAIL=superadmin@tria.com
|
||||
SUPER_ADMIN_PHONE=
|
||||
# Super-admin password. Falls back to DEFAULT_PASSWORD when empty.
|
||||
SUPER_ADMIN_PASSWORD=
|
||||
DEFAULT_PASSWORD=password@tria
|
||||
|
||||
# IAM baseline (roles, IAM app + permissions, position types, org/unit settings,
|
||||
# super-admin account). Local replacement for the package DataSeeder —
|
||||
# see src/seed/iam-baseline.seed.ts.
|
||||
SEED_IAM_BASELINE=false
|
||||
|
||||
# Freight org + staff (bookings / rule-engine IAM)
|
||||
SEED_EDR_ORG=true
|
||||
SEED_FREIGHT_STAFF=true
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
ensurePostgresSchemas,
|
||||
APPLICATION_SEARCH_PATH,
|
||||
} from "./config/ensure-postgres-schemas";
|
||||
import { IamModule, DataSeeder } from "@tria-plc/iamapi-common";
|
||||
import { IamModule } from "@tria-plc/iamapi-common";
|
||||
import { SharedAuthModule } from "@tria-plc/api-common/modules/auth/shared-auth.module";
|
||||
|
||||
import appConfig from "./config/app.config";
|
||||
@@ -55,6 +55,7 @@ import {
|
||||
EDR_FREIGHT_PERMISSIONS,
|
||||
} from "./seed/edr-freight.seed";
|
||||
import { EdrOrgSeeder } from "./seed/edr-org.seeder";
|
||||
import { IamBaselineSeeder } from "./seed/iam-baseline.seeder";
|
||||
import { FreightPositionsSeeder } from "./seed/freight-positions.seeder";
|
||||
// Disabled seeds — imports commented out with their provider/injection/run below.
|
||||
// import { DemoUsersSeeder } from "./seed/demo-users.seeder";
|
||||
@@ -206,6 +207,7 @@ import { LoggerMiddleware } from "./logger.middleware";
|
||||
AiModule,
|
||||
],
|
||||
providers: [
|
||||
IamBaselineSeeder,
|
||||
EdrOrgSeeder,
|
||||
FreightPositionsSeeder,
|
||||
FileUploadSettingsSeeder,
|
||||
@@ -231,7 +233,7 @@ import { LoggerMiddleware } from "./logger.middleware";
|
||||
})
|
||||
export class AppModule implements OnApplicationBootstrap {
|
||||
constructor(
|
||||
private readonly seeder: DataSeeder,
|
||||
private readonly iamBaselineSeeder: IamBaselineSeeder,
|
||||
private readonly edrOrgSeeder: EdrOrgSeeder,
|
||||
private readonly freightPositionsSeeder: FreightPositionsSeeder,
|
||||
private readonly fileUploadSettingsSeeder: FileUploadSettingsSeeder,
|
||||
@@ -261,13 +263,23 @@ export class AppModule implements OnApplicationBootstrap {
|
||||
|
||||
// Permissions foundation — keep enabled:
|
||||
// freightPermissionKeyMigration → renames legacy permission keys
|
||||
// seeder (IAM DataSeeder) → seeds the IAM app, roles, permissions
|
||||
// edrOrgSeeder → seeds org/unit + the Permission catalog
|
||||
// iamBaselineSeeder → seeds the IAM app, roles, permissions,
|
||||
// position types, org/unit settings and the
|
||||
// super-admin account. Local replacement for
|
||||
// the package's DataSeeder (still exported as
|
||||
// `DataSeeder` from @tria-plc/iamapi-common
|
||||
// and runnable via `pnpm iam:seed:run`) —
|
||||
// see src/seed/iam-baseline.seed.ts for
|
||||
// what it seeds and what it drops. Runs after
|
||||
// edrOrgSeeder because the super admin is
|
||||
// attached to the edr_freight org/unit.
|
||||
// Writes nothing unless SEED_IAM_BASELINE=true.
|
||||
// freightPositionsSeeder → seeds Position + PositionPermission rows
|
||||
// (depends on edrOrgSeeder, must run after)
|
||||
await this.freightPermissionKeyMigrationSeeder.run();
|
||||
await this.seeder.run();
|
||||
await this.edrOrgSeeder.run();
|
||||
await this.iamBaselineSeeder.run();
|
||||
await this.freightPositionsSeeder.run();
|
||||
|
||||
// File upload settings — keep enabled.
|
||||
|
||||
611
apps/edr-freight-api/src/seed/iam-baseline.seed.ts
Normal file
611
apps/edr-freight-api/src/seed/iam-baseline.seed.ts
Normal file
@@ -0,0 +1,611 @@
|
||||
/**
|
||||
* IAM baseline seed data — the rows `IamBaselineSeeder` writes.
|
||||
*
|
||||
* These started as a copy of the seed constants inside
|
||||
* `@tria-plc/iamapi-common` (`dist/db/seed/role.seed`,
|
||||
* `organization-setting.seed`, `unit-setting.seed`) as of 0.7.12, and are now
|
||||
* owned here: edit, extend or delete anything below and the seeder follows.
|
||||
* Nothing in this file is imported from the package, so a version bump cannot
|
||||
* change what gets seeded — it also will not hand you new IAM permissions, so
|
||||
* diff against those files when upgrading.
|
||||
*
|
||||
* Ids are the package's originals. Keep them: the rows already in every
|
||||
* environment carry these ids.
|
||||
*
|
||||
* Deliberately NOT seeded (the package's DataSeeder does all of it):
|
||||
* - TRIA super-admin organization + its `superadmin` user and password
|
||||
* - 11 Addis Ababa sub-city organizations
|
||||
* - woreda / subcity / office / branch organization types + 65 default units
|
||||
* - the 7 non-IAM Smart Office applications (booking, bot, chronicle, dms,
|
||||
* metabase, performance, record)
|
||||
*/
|
||||
|
||||
export type LocalizedName = { am: string; en: string };
|
||||
|
||||
export type SeedApplication = { id: string; key: string; name: LocalizedName };
|
||||
|
||||
export type SeedRole = { id: string; key: string; name: LocalizedName };
|
||||
|
||||
export type SeedPermission = {
|
||||
id: string;
|
||||
key: string;
|
||||
/** Omitted for the 21 org/unit/location permissions the package leaves unlinked. */
|
||||
applicationKey?: string;
|
||||
name: LocalizedName;
|
||||
};
|
||||
|
||||
export type SeedPositionType = {
|
||||
id: string;
|
||||
key: string;
|
||||
isSystem: boolean;
|
||||
name: LocalizedName;
|
||||
};
|
||||
|
||||
export type SeedSuperAdmin = {
|
||||
username: string;
|
||||
name: LocalizedName;
|
||||
/** Key in `roles` — the grant that carries the access. */
|
||||
roleKey: string;
|
||||
/** Existing organization/unit to attach the employee to. */
|
||||
organizationKey: string;
|
||||
unitKey: string;
|
||||
/** Used when SUPER_ADMIN_EMAIL / SUPER_ADMIN_PASSWORD / DEFAULT_PASSWORD are all unset. */
|
||||
fallbackEmail: string;
|
||||
fallbackPassword: string;
|
||||
};
|
||||
|
||||
export type SettingDefault = {
|
||||
key: string;
|
||||
displayName: string;
|
||||
/** Matches the package's ESettingType. */
|
||||
type: "value" | "file";
|
||||
value?: string | null;
|
||||
};
|
||||
|
||||
/** Applications permissions hang off. `edr_freight_app` is seeded by EdrOrgSeeder. */
|
||||
const APPLICATIONS: SeedApplication[] = [
|
||||
{
|
||||
id: "019bcb17-5470-7604-8708-7ed04d842b41",
|
||||
key: "iam",
|
||||
name: { am: "የስማርት ኦፊስ ማንነት እና መዳረሻ አስተዳደር", en: "Smart Office Identity and Access Management" },
|
||||
},
|
||||
];
|
||||
|
||||
const ROLES: SeedRole[] = [
|
||||
{
|
||||
id: "520836ee-dc13-4c08-b572-8eced5bfd309",
|
||||
key: "super_admin",
|
||||
name: { am: "ዋና ተቆጣጣሪ", en: "Super Admin" },
|
||||
},
|
||||
{
|
||||
id: "b2de1eae-ef93-4e90-8ec9-351f0dd8a6a9",
|
||||
key: "organization_admin",
|
||||
name: { am: "የመስሪያ ቤት ዋና ተቆጣጣሪ", en: "Organization Admin" },
|
||||
},
|
||||
{
|
||||
id: "b3a9a5b5-9825-4290-8498-c62fb5925acd",
|
||||
key: "unit_admin",
|
||||
name: { am: "የመስሪያ ቤት ጽሕፈት ቤት ዋና ተቆጣጣሪ", en: "Organization Unit Admin" },
|
||||
},
|
||||
{
|
||||
id: "ffe82427-ab16-4571-913c-553deb1b0f0f",
|
||||
key: "guest",
|
||||
name: { am: "ተጠቃሚ", en: "Guest" },
|
||||
},
|
||||
];
|
||||
|
||||
const PERMISSIONS: SeedPermission[] = [
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000001",
|
||||
key: "can:create:role",
|
||||
applicationKey: "iam",
|
||||
name: { am: "ሚና መፍጠር", en: "Create Role" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000002",
|
||||
key: "can:update:role",
|
||||
applicationKey: "iam",
|
||||
name: { am: "ሚና ማሻሻል", en: "Update Role" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000003",
|
||||
key: "can:delete:role",
|
||||
applicationKey: "iam",
|
||||
name: { am: "ሚና ማጥፋት", en: "Delete Role" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000006",
|
||||
key: "can:update:permission",
|
||||
applicationKey: "iam",
|
||||
name: { am: "ፈቃድ ማሻሻል", en: "Update Permission" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000007",
|
||||
key: "can:delete:permission",
|
||||
applicationKey: "iam",
|
||||
name: { am: "ፈቃድ ማጥፋት", en: "Delete Permission" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000009",
|
||||
key: "can:create:role_permission",
|
||||
applicationKey: "iam",
|
||||
name: { am: "የሚና-ፈቃድ መፍጠር", en: "Create Role-Permission" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000010",
|
||||
key: "can:delete:role_permission",
|
||||
applicationKey: "iam",
|
||||
name: { am: "የሚና-ፈቃድ ማጥፋት", en: "Delete Role-Permission" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000011",
|
||||
key: "can:view:role_permission",
|
||||
applicationKey: "iam",
|
||||
name: { am: "የሚና-ፈቃድ መመልከት", en: "View Role-Permission" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000012",
|
||||
key: "can:create:user_role",
|
||||
applicationKey: "iam",
|
||||
name: { am: "የተጠቃሚ-ሚና መፍጠር", en: "Create User-Role" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000013",
|
||||
key: "can:delete:user_role",
|
||||
applicationKey: "iam",
|
||||
name: { am: "የተጠቃሚ-ሚና ማጥፋት", en: "Delete User-Role" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000014",
|
||||
key: "can:view:user_role",
|
||||
applicationKey: "iam",
|
||||
name: { am: "የተጠቃሚ-ሚና መመልከት", en: "View User-Role" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000015",
|
||||
key: "can:create:position_permission",
|
||||
applicationKey: "iam",
|
||||
name: { am: "የመደብ-ሚና መፍጠር", en: "Create Position-Permission" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000016",
|
||||
key: "can:delete:position_permission",
|
||||
applicationKey: "iam",
|
||||
name: { am: "የመደብ-ሚና ማጥፋት", en: "Delete Position-Permission" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000017",
|
||||
key: "can:view:position_permission",
|
||||
applicationKey: "iam",
|
||||
name: { am: "የመደብ-ሚና መመልከት", en: "View Position-Permission" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000018",
|
||||
key: "can:find_all:organization",
|
||||
name: { am: "ሁሉንም ድርጅቶች መፈለግ", en: "Find All Organizations" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000019",
|
||||
key: "can:update:organization",
|
||||
name: { am: "ድርጅት ማሻሻል", en: "Update Organization" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000020",
|
||||
key: "can:delete:organization",
|
||||
name: { am: "ድርጅት ማጥፋት", en: "Delete Organization" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000021",
|
||||
key: "can:create:unit",
|
||||
name: { am: "ክፍል መፍጠር", en: "Create Unit" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000022",
|
||||
key: "can:update:unit",
|
||||
name: { am: "ክፍል ማሻሻል", en: "Update Unit" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000023",
|
||||
key: "can:delete:unit",
|
||||
name: { am: "ክፍል ማጥፋት", en: "Delete Unit" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000024",
|
||||
key: "can:create:default_unit",
|
||||
name: { am: "የዩኒት አይነት መፍጠር", en: "Create Default Unit" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000025",
|
||||
key: "can:update:default_unit",
|
||||
name: { am: "የዩኒት አይነት ማሻሻል", en: "Update Default Unit" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000026",
|
||||
key: "can:delete:default_unit",
|
||||
name: { am: "የዩኒት አይነት ማጥፋት", en: "Delete Default Unit" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000027",
|
||||
key: "can:create:default_position",
|
||||
name: { am: "የስራ መደብ አይነት መፍጠር", en: "Create Default Position" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000028",
|
||||
key: "can:update:default_position",
|
||||
name: { am: "የስራ መደብ አይነት ማሻሻል", en: "Update Default Position" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000029",
|
||||
key: "can:delete:default_position",
|
||||
name: { am: "የስራ መደብ አይነት ማጥፋት", en: "Delete Default Position" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000030",
|
||||
key: "can:create:organization_type",
|
||||
name: { am: "የድርጅት አይነት መፍጠር", en: "Create Organization Type" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000031",
|
||||
key: "can:update:organization_type",
|
||||
name: { am: "የድርጅት አይነት ማሻሻል", en: "Update Organization Type" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000032",
|
||||
key: "can:delete:organization_type",
|
||||
name: { am: "የድርጅት አይነት ማጥፋት", en: "Delete Organization Type" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000033",
|
||||
key: "can:create:location_type",
|
||||
name: { am: "የአካባቢ አይነት መፍጠር", en: "Create Location Type" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000034",
|
||||
key: "can:update:location_type",
|
||||
name: { am: "የአካባቢ አይነት ማሻሻል", en: "Update Location Type" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000035",
|
||||
key: "can:delete:location_type",
|
||||
name: { am: "የአካባቢ አይነት ማጥፋት", en: "Delete Location Type" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000036",
|
||||
key: "can:create:location",
|
||||
name: { am: "አካባቢ መፍጠር", en: "Create Location" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000037",
|
||||
key: "can:update:location",
|
||||
name: { am: "አካባቢ ማሻሻል", en: "Update Location" },
|
||||
},
|
||||
{
|
||||
id: "019b5993-0000-0000-0000-000000000038",
|
||||
key: "can:delete:location",
|
||||
name: { am: "አካባቢ ማጥፋት", en: "Delete Location" },
|
||||
},
|
||||
{
|
||||
id: "a9a7c0fa-e4fc-4c0e-b1c2-f74f9da40421",
|
||||
key: "create:organization",
|
||||
applicationKey: "iam",
|
||||
name: { am: "የመስሪያ ቤት መፍጠር", en: "Create Organization" },
|
||||
},
|
||||
{
|
||||
id: "c807691b-2693-4079-9dc5-1e080b67006c",
|
||||
key: "activate:organization",
|
||||
applicationKey: "iam",
|
||||
name: { am: "የመስሪያ ቤት አስተካክል", en: "Activate Organization" },
|
||||
},
|
||||
{
|
||||
id: "457a3659-ed96-456a-a6a1-2881226a86ed",
|
||||
key: "can:debarOrganization",
|
||||
applicationKey: "iam",
|
||||
name: { am: "መቼት መቆጣጠር ይችላል", en: "Debar Organization" },
|
||||
},
|
||||
{
|
||||
id: "2cd9e3c5-bd41-48f3-a849-f497b18b2906",
|
||||
key: "manage:organizationAdmin",
|
||||
applicationKey: "iam",
|
||||
name: { am: "መቼት መቆጣጠር ይችላል", en: "Manage Organization Admin" },
|
||||
},
|
||||
{
|
||||
id: "257d8c6e-ef30-4510-892f-d4a2ad4c814d",
|
||||
key: "manage:unitAdmin",
|
||||
applicationKey: "iam",
|
||||
name: { am: "የጽሕፈት ቤት መቼት መቆጣጠር ይችላል", en: "Manage Unit Admin" },
|
||||
},
|
||||
{
|
||||
id: "4052b7b8-e9f6-4bfe-9b93-eb59f9e4e576",
|
||||
key: "can:createEmployee",
|
||||
applicationKey: "iam",
|
||||
name: { am: "ሰራተኞችን መመደብ/መፍጠር ይችላሉ", en: "Can create employees" },
|
||||
},
|
||||
{
|
||||
id: "965ec76d-bbdb-47a1-a916-07c52f609fa7",
|
||||
key: "can:deactivateEmployee",
|
||||
applicationKey: "iam",
|
||||
name: { am: "ሰራተኞችን ማባረር ይችላሉ", en: "Can deactivate employees" },
|
||||
},
|
||||
{
|
||||
id: "019cbdc6-3d7a-73aa-ac57-51436dfa50e9",
|
||||
key: "can:activateEmployee",
|
||||
applicationKey: "iam",
|
||||
name: { am: "ሰራተኞችን መቀበል ይችላሉ", en: "Can activate employees" },
|
||||
},
|
||||
{
|
||||
id: "ebd8cf49-243d-4887-b2c9-cbe61e86a17a",
|
||||
key: "can:uploadUserCSV",
|
||||
applicationKey: "iam",
|
||||
name: { am: "የሰራተኞችን መረጃ መጫን ይችላል", en: "Can Upload User CSV" },
|
||||
},
|
||||
{
|
||||
id: "68bebc03-c1a8-832f-ae5a-b66553e6bcef",
|
||||
key: "can:exportUnitUsers",
|
||||
applicationKey: "iam",
|
||||
name: { am: "የተቀጣሪዎችን መረጃ ማውጣት ይችላሉ", en: "Can Export Unit Users" },
|
||||
},
|
||||
{
|
||||
id: "c2566286-248e-4cb4-923d-b113d6a5d4ba",
|
||||
key: "can:changeUsersProfile",
|
||||
applicationKey: "iam",
|
||||
name: { am: "ተጠቃሚዎች መግለጫ መቀየር ይችላል", en: "Can Change Users Profile" },
|
||||
},
|
||||
{
|
||||
id: "14f438c6-9295-44a0-a4f3-4ac9855efc37",
|
||||
key: "can:activateUser",
|
||||
applicationKey: "iam",
|
||||
name: { am: "ተጠቃሚዎችን መቆጣጠር ይችላል", en: "Can Activate/Deactivate User" },
|
||||
},
|
||||
{
|
||||
id: "31a39f88-47c0-4322-80ef-f397a6791ff2",
|
||||
key: "can:approveNewUser",
|
||||
applicationKey: "iam",
|
||||
name: { am: "አዲስ ተመዝጋቢ ማፅደቅ ይችላል", en: "Can Approve New User" },
|
||||
},
|
||||
{
|
||||
id: "69694936-8b54-8330-b176-16ffc98c33a7",
|
||||
key: "can:viewAllUsers",
|
||||
applicationKey: "iam",
|
||||
name: { am: "ሁሉንም ተጠቃሚዎች ማየት ይችላል", en: "Can View All Users" },
|
||||
},
|
||||
{
|
||||
id: "68f1017f-4c60-8322-984d-e4317986e641",
|
||||
key: "can:manageUsersAccountConfiguration",
|
||||
applicationKey: "iam",
|
||||
name: { am: "የተጠቃሚ መለያ አዋቂነት መቆጣጠር ይችላል", en: "Can Manage Users Account Configuration" },
|
||||
},
|
||||
{
|
||||
id: "fceaa4c5-621f-45ce-b5a7-0ffc9e366fe1",
|
||||
key: "can:setUserRequirementDocument",
|
||||
applicationKey: "iam",
|
||||
name: { am: "ተመዝጋቢዎች የሚያስገቡትን መረጃ መቆጣጠር ይችላል", en: "Can Set User Requirement Document" },
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Role → permission-key grants. Applied additively: the seeder only inserts
|
||||
* missing pairs, so grants made through the IAM UI survive a reseed.
|
||||
*/
|
||||
const ROLE_PERMISSIONS: { roleKey: string; permissionKeys: string[] }[] = [
|
||||
{
|
||||
roleKey: "super_admin",
|
||||
permissionKeys: [
|
||||
"can:create:role",
|
||||
"can:update:role",
|
||||
"can:delete:role",
|
||||
"can:update:permission",
|
||||
"can:delete:permission",
|
||||
"can:create:role_permission",
|
||||
"can:delete:role_permission",
|
||||
"can:view:role_permission",
|
||||
"can:create:user_role",
|
||||
"can:delete:user_role",
|
||||
"can:view:user_role",
|
||||
"can:create:position_permission",
|
||||
"can:delete:position_permission",
|
||||
"can:view:position_permission",
|
||||
"can:find_all:organization",
|
||||
"can:update:organization",
|
||||
"can:delete:organization",
|
||||
"can:create:unit",
|
||||
"can:update:unit",
|
||||
"can:delete:unit",
|
||||
"can:create:default_unit",
|
||||
"can:update:default_unit",
|
||||
"can:delete:default_unit",
|
||||
"can:create:default_position",
|
||||
"can:update:default_position",
|
||||
"can:delete:default_position",
|
||||
"can:create:organization_type",
|
||||
"can:update:organization_type",
|
||||
"can:delete:organization_type",
|
||||
"can:create:location_type",
|
||||
"can:update:location_type",
|
||||
"can:delete:location_type",
|
||||
"create:organization",
|
||||
"activate:organization",
|
||||
"can:debarOrganization",
|
||||
"manage:organizationAdmin",
|
||||
"manage:unitAdmin",
|
||||
"can:activateUser",
|
||||
"can:approveNewUser",
|
||||
"can:viewAllUsers",
|
||||
"can:setUserRequirementDocument",
|
||||
"can:create:location",
|
||||
"can:update:location",
|
||||
"can:delete:location",
|
||||
],
|
||||
},
|
||||
{
|
||||
roleKey: "organization_admin",
|
||||
permissionKeys: [
|
||||
"can:uploadUserCSV",
|
||||
"can:changeUsersProfile",
|
||||
"can:createEmployee",
|
||||
"can:deactivateEmployee",
|
||||
"can:exportUnitUsers",
|
||||
"can:create:position_permission",
|
||||
"can:delete:position_permission",
|
||||
"can:view:position_permission",
|
||||
"can:create:unit",
|
||||
"can:update:unit",
|
||||
"can:delete:unit",
|
||||
"manage:unitAdmin",
|
||||
],
|
||||
},
|
||||
{
|
||||
roleKey: "unit_admin",
|
||||
permissionKeys: [
|
||||
"can:uploadUserCSV",
|
||||
"can:createEmployee",
|
||||
"can:exportUnitUsers",
|
||||
"can:changeUsersProfile",
|
||||
"can:deactivateEmployee",
|
||||
"can:manageUsersAccountConfiguration",
|
||||
"can:create:position_permission",
|
||||
"can:delete:position_permission",
|
||||
"can:view:position_permission",
|
||||
"can:create:unit",
|
||||
"can:update:unit",
|
||||
"can:delete:unit",
|
||||
],
|
||||
},
|
||||
{
|
||||
roleKey: "guest",
|
||||
permissionKeys: [
|
||||
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const POSITION_TYPES: SeedPositionType[] = [
|
||||
{
|
||||
id: "457a3659-ed96-456a-a6a1-2881226a86ec",
|
||||
key: "employee",
|
||||
isSystem: true,
|
||||
name: { am: "ባለሙያ", en: "Employee" },
|
||||
},
|
||||
{
|
||||
id: "34a7f69c-3f30-47a0-81c3-fcfc3087e456",
|
||||
key: "teamLeader",
|
||||
isSystem: true,
|
||||
name: { am: "ቡድን መሪ", en: "Team Leader" },
|
||||
},
|
||||
{
|
||||
id: "2cd9e3c5-bd41-48f3-a849-f497b18b2905",
|
||||
key: "director",
|
||||
isSystem: true,
|
||||
name: { am: "ዳይሬክተር", en: "Director" },
|
||||
},
|
||||
{
|
||||
id: "db310acf-7a78-40a3-83c7-9a9e9e6d1fc7",
|
||||
key: "deputy",
|
||||
isSystem: true,
|
||||
name: { am: "ዘርፍ ኃላፊ", en: "Deputy" },
|
||||
},
|
||||
{
|
||||
id: "1a4b4d40-e4fc-4f38-99a6-f81dc5fcff23",
|
||||
key: "officeHead",
|
||||
isSystem: true,
|
||||
name: { am: "ቢሮ ኃላፊ", en: "Office Head" },
|
||||
},
|
||||
{
|
||||
id: "83bc6cd3-119e-4a41-917c-c763fb3fd013",
|
||||
key: "recordOfficer",
|
||||
isSystem: true,
|
||||
name: { am: "መዝገብ ቤት", en: "Record Officer" },
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* PositionType → permission-key grants. Empty because freight grants through
|
||||
* Position/PositionPermission (see FreightPositionsSeeder), not position types.
|
||||
*/
|
||||
const POSITION_TYPE_PERMISSIONS: {
|
||||
positionTypeKey: string;
|
||||
permissionKeys: string[];
|
||||
}[] = [];
|
||||
|
||||
/** Seeded once per organization. All start unset; the IAM UI fills them in. */
|
||||
const ORGANIZATION_SETTINGS: SettingDefault[] = [
|
||||
{ key: "logoFileUrl", displayName: "Logo", type: "file" },
|
||||
{ key: "faviconFileUrl", displayName: "Favicon", type: "file" },
|
||||
{ key: "loginBackgroundFileUrl", displayName: "Login Background", type: "file" },
|
||||
{ key: "stampImageFileUrl", displayName: "Stamp Image", type: "file" },
|
||||
{ key: "customCssFileUrl", displayName: "Custom CSS", type: "file" },
|
||||
{ key: "primaryColor", displayName: "Primary Color", type: "value" },
|
||||
{ key: "secondaryColor", displayName: "Secondary Color", type: "value" },
|
||||
{ key: "accentColor", displayName: "Accent Color", type: "value" },
|
||||
{ key: "loginTitle", displayName: "Login Title", type: "value" },
|
||||
{ key: "loginSubtitle", displayName: "Login Subtitle", type: "value" },
|
||||
{ key: "sidebarColor", displayName: "Sidebar Color", type: "value" },
|
||||
{ key: "headerColor", displayName: "Header Color", type: "value" },
|
||||
{ key: "stampText", displayName: "Stamp Text", type: "value" },
|
||||
{ key: "footerText", displayName: "Footer Text", type: "value" },
|
||||
{ key: "supportEmail", displayName: "Support Email", type: "value" },
|
||||
{ key: "supportPhone", displayName: "Support Phone", type: "value" },
|
||||
];
|
||||
|
||||
/** Seeded once per unit. */
|
||||
const UNIT_SETTINGS: SettingDefault[] = [
|
||||
{ key: "isMultipleDelegationAllowed", displayName: "Is Multiple Delegation Allowed", type: "value" },
|
||||
{ key: "internalSuffix", displayName: "Internal Suffix", type: "value" },
|
||||
{ key: "internalPrefix", displayName: "Internal Prefix", type: "value" },
|
||||
{ key: "internalSuffixCC", displayName: "Internal Suffix CC", type: "value" },
|
||||
{ key: "internalPrefixCC", displayName: "Internal Prefix CC", type: "value" },
|
||||
{ key: "referenceNumberPrefix", displayName: "Reference Number Prefix", type: "value" },
|
||||
{ key: "externalReferenceNumberPrefix", displayName: "External Reference Number Prefix", type: "value" },
|
||||
{ key: "internalMemoReferenceNumberPrefix", displayName: "Internal Memo Reference Number Prefix", type: "value" },
|
||||
{ key: "escalationHour", displayName: "Escalation Hour", type: "value" },
|
||||
{ key: "urgentLetterEscalationHour", displayName: "Urgent Letter Escalation Hour", type: "value" },
|
||||
{ key: "onReviewLetterEscalationHour", displayName: "On Review Letter Escalation Hour", type: "value" },
|
||||
{ key: "urgentOnReviewLetterEscalationHour", displayName: "Urgent On Review Letter Escalation Hour", type: "value" },
|
||||
{ key: "shouldCollaboratorAlwaysSign", displayName: "Should Collaborator Always Sign", type: "value" },
|
||||
{ key: "waitAllCollaboratorsBeforeAction", displayName: "Wait All Collaborators Before Action", type: "value" },
|
||||
{ key: "shouldIncludeForYourReferenceInCC", displayName: "Should Include For Your Reference In CC", type: "value" },
|
||||
{ key: "forwardWithTeeterSignature", displayName: "Forward With Teeter Signature", type: "value" },
|
||||
{ key: "attachSignatureOnAttachment", displayName: "Attach Signature On Attachment", type: "value" },
|
||||
{ key: "positionScopeToFetch", displayName: "Position Scope To Fetch", type: "value" },
|
||||
];
|
||||
|
||||
/**
|
||||
* The super-admin account. Its access comes entirely from the `super_admin`
|
||||
* UserRole: `isSuperAdmin()` in `common/freight-permission.util.ts` short-circuits
|
||||
* `hasFreightPermission`, so this account passes every FreightPermissionGuard
|
||||
* and every approval-step check without holding one freight permission row.
|
||||
*
|
||||
* Unlike the package seeder, the account lands in the freight organization
|
||||
* (`edr_freight` / unit `edr_freight_app`) rather than a separate `tria`
|
||||
* organization, so its employee context matches the app it administers. Both
|
||||
* must already exist — EdrOrgSeeder creates them, and must run first.
|
||||
*
|
||||
* Read from env at seed time: SUPER_ADMIN_EMAIL, SUPER_ADMIN_PHONE, and for the
|
||||
* password SUPER_ADMIN_PASSWORD first, then DEFAULT_PASSWORD. The values below
|
||||
* are the last resort. Set this to null to seed no account at all.
|
||||
*/
|
||||
const SUPER_ADMIN: SeedSuperAdmin | null = {
|
||||
username: "superadmin",
|
||||
name: { am: "ሱፐር አድሚን", en: "Super Admin" },
|
||||
roleKey: "super_admin",
|
||||
organizationKey: "edr_freight",
|
||||
unitKey: "edr_freight_app",
|
||||
fallbackEmail: "superadmin@tria.com",
|
||||
fallbackPassword: "password@tria",
|
||||
};
|
||||
|
||||
/**
|
||||
* What `IamBaselineSeeder` writes. Empty an array to skip that section
|
||||
* entirely; the seeder never deletes rows, so emptying one leaves whatever is
|
||||
* already in the database alone.
|
||||
*/
|
||||
export const IAM_BASELINE_SEED = {
|
||||
applications: APPLICATIONS,
|
||||
roles: ROLES,
|
||||
permissions: PERMISSIONS,
|
||||
rolePermissions: ROLE_PERMISSIONS,
|
||||
positionTypes: POSITION_TYPES,
|
||||
positionTypePermissions: POSITION_TYPE_PERMISSIONS,
|
||||
organizationSettings: ORGANIZATION_SETTINGS,
|
||||
unitSettings: UNIT_SETTINGS,
|
||||
superAdmin: SUPER_ADMIN,
|
||||
};
|
||||
468
apps/edr-freight-api/src/seed/iam-baseline.seeder.ts
Normal file
468
apps/edr-freight-api/src/seed/iam-baseline.seeder.ts
Normal file
@@ -0,0 +1,468 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { hashPassword } from "@tria-plc/api-common/utils/argon";
|
||||
import {
|
||||
Application,
|
||||
EEmployeeStatus,
|
||||
Employee,
|
||||
EUserStatus,
|
||||
EUserType,
|
||||
Organization,
|
||||
Permission,
|
||||
PositionType,
|
||||
PositionTypePermission,
|
||||
Role,
|
||||
RolePermission,
|
||||
Unit,
|
||||
UnitSetting,
|
||||
User,
|
||||
UserCredential,
|
||||
UserRole,
|
||||
} from "@tria-plc/iamapi-common";
|
||||
// Not re-exported from the package root, unlike UnitSetting.
|
||||
import { OrganizationSetting } from "@tria-plc/iamapi-common/entities/iam/organization-structure/organization-setting.entity";
|
||||
import type { ESettingType } from "@tria-plc/iamapi-common/enums/setting-type.enum";
|
||||
import {
|
||||
DataSource,
|
||||
EntityManager,
|
||||
EntityTarget,
|
||||
In,
|
||||
ObjectLiteral,
|
||||
} from "typeorm";
|
||||
|
||||
import { IAM_BASELINE_SEED } from "./iam-baseline.seed";
|
||||
import { missingSettings } from "./missing-settings.util";
|
||||
|
||||
const SEED_FLAG = "SEED_IAM_BASELINE";
|
||||
|
||||
/**
|
||||
* Local stand-in for `DataSeeder` from `@tria-plc/iamapi-common`, seeding only
|
||||
* what `IAM_BASELINE_SEED` lists (see that file for what was deliberately left
|
||||
* out). Differences from the upstream seeder, all deliberate:
|
||||
*
|
||||
* - Nothing is deleted. Upstream wipes every `position_type_permissions` row
|
||||
* for the 6 system position types on each run and nulls their `unit_id`.
|
||||
* - Settings are matched on (organization|unit, key). Upstream upserts them on
|
||||
* `id`, which it never supplies, so every run inserts a duplicate set.
|
||||
* - Ids are resolved from the database by key rather than from the seed
|
||||
* constants, so rows that already exist under a different id still link up.
|
||||
*
|
||||
* Gated behind SEED_IAM_BASELINE=true so a normal boot never writes.
|
||||
*/
|
||||
@Injectable()
|
||||
export class IamBaselineSeeder {
|
||||
private readonly logger = new Logger(IamBaselineSeeder.name);
|
||||
|
||||
constructor(private readonly dataSource: DataSource) {}
|
||||
|
||||
async run() {
|
||||
if (process.env[SEED_FLAG]?.trim().toLowerCase() !== "true") {
|
||||
this.logger.log(
|
||||
`Skipping IAM baseline seed because ${SEED_FLAG} is not enabled`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
await this.seedApplications(manager);
|
||||
await this.seedPermissions(manager);
|
||||
await this.seedRoles(manager);
|
||||
await this.seedRolePermissions(manager);
|
||||
await this.seedPositionTypes(manager);
|
||||
await this.seedPositionTypePermissions(manager);
|
||||
await this.seedOrganizationSettings(manager);
|
||||
await this.seedUnitSettings(manager);
|
||||
await this.seedSuperAdmin(manager);
|
||||
});
|
||||
|
||||
this.logger.log("IAM baseline seed complete");
|
||||
}
|
||||
|
||||
private async seedApplications(manager: EntityManager) {
|
||||
const { applications } = IAM_BASELINE_SEED;
|
||||
if (applications.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await manager.getRepository(Application).upsert(
|
||||
applications.map((application) => ({
|
||||
id: application.id,
|
||||
key: application.key,
|
||||
name: { ...application.name },
|
||||
})),
|
||||
{ conflictPaths: { key: true } },
|
||||
);
|
||||
|
||||
this.logger.log(`Ensured ${applications.length} applications`);
|
||||
}
|
||||
|
||||
private async seedPermissions(manager: EntityManager) {
|
||||
const { permissions } = IAM_BASELINE_SEED;
|
||||
if (permissions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Permissions without an applicationKey stay unlinked (application_id null),
|
||||
// which is how the package ships the org/unit/location ones.
|
||||
const applicationIdByKey = await this.loadIdsByKey(
|
||||
manager,
|
||||
Application,
|
||||
permissions.flatMap((permission) =>
|
||||
permission.applicationKey ? [permission.applicationKey] : [],
|
||||
),
|
||||
"missing_applications",
|
||||
);
|
||||
|
||||
await manager.getRepository(Permission).upsert(
|
||||
permissions.map((permission) => ({
|
||||
id: permission.id,
|
||||
key: permission.key,
|
||||
name: { ...permission.name },
|
||||
applicationId: permission.applicationKey
|
||||
? applicationIdByKey.get(permission.applicationKey)
|
||||
: undefined,
|
||||
})),
|
||||
{ conflictPaths: { key: true } },
|
||||
);
|
||||
|
||||
this.logger.log(`Ensured ${permissions.length} permissions`);
|
||||
}
|
||||
|
||||
private async seedRoles(manager: EntityManager) {
|
||||
const { roles } = IAM_BASELINE_SEED;
|
||||
if (roles.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await manager.getRepository(Role).upsert(
|
||||
roles.map((role) => ({
|
||||
id: role.id,
|
||||
key: role.key,
|
||||
name: { ...role.name },
|
||||
})),
|
||||
{ conflictPaths: { key: true } },
|
||||
);
|
||||
|
||||
this.logger.log(`Ensured ${roles.length} roles`);
|
||||
}
|
||||
|
||||
private async seedRolePermissions(manager: EntityManager) {
|
||||
const { rolePermissions } = IAM_BASELINE_SEED;
|
||||
if (rolePermissions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const roleIdByKey = await this.loadIdsByKey(
|
||||
manager,
|
||||
Role,
|
||||
rolePermissions.map((mapping) => mapping.roleKey),
|
||||
"missing_roles",
|
||||
);
|
||||
const permissionIdByKey = await this.loadIdsByKey(
|
||||
manager,
|
||||
Permission,
|
||||
rolePermissions.flatMap((mapping) => mapping.permissionKeys),
|
||||
"missing_permissions",
|
||||
);
|
||||
|
||||
const repository = manager.getRepository(RolePermission);
|
||||
const roleIds = [...roleIdByKey.values()];
|
||||
const existing = await repository.find({
|
||||
where: { roleId: In(roleIds) },
|
||||
select: { roleId: true, permissionId: true },
|
||||
});
|
||||
const existingPairs = new Set(
|
||||
existing.map((row) => `${row.roleId}:${row.permissionId}`),
|
||||
);
|
||||
|
||||
const rows = rolePermissions.flatMap((mapping) => {
|
||||
const roleId = roleIdByKey.get(mapping.roleKey) as string;
|
||||
return mapping.permissionKeys
|
||||
.map((key) => permissionIdByKey.get(key) as string)
|
||||
.filter((permissionId) => !existingPairs.has(`${roleId}:${permissionId}`))
|
||||
.map((permissionId) => ({ roleId, permissionId }));
|
||||
});
|
||||
|
||||
if (rows.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await repository.insert(rows);
|
||||
this.logger.log(`Granted ${rows.length} role permissions`);
|
||||
}
|
||||
|
||||
private async seedPositionTypes(manager: EntityManager) {
|
||||
const { positionTypes } = IAM_BASELINE_SEED;
|
||||
if (positionTypes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// unitId is intentionally left alone — upstream resets it to null here.
|
||||
await manager.getRepository(PositionType).upsert(
|
||||
positionTypes.map((positionType) => ({
|
||||
id: positionType.id,
|
||||
key: positionType.key,
|
||||
name: { ...positionType.name },
|
||||
isSystem: positionType.isSystem ?? true,
|
||||
})),
|
||||
{ conflictPaths: { key: true } },
|
||||
);
|
||||
|
||||
this.logger.log(`Ensured ${positionTypes.length} position types`);
|
||||
}
|
||||
|
||||
private async seedPositionTypePermissions(manager: EntityManager) {
|
||||
const { positionTypePermissions } = IAM_BASELINE_SEED;
|
||||
if (positionTypePermissions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const positionTypeIdByKey = await this.loadIdsByKey(
|
||||
manager,
|
||||
PositionType,
|
||||
positionTypePermissions.map((mapping) => mapping.positionTypeKey),
|
||||
"missing_position_types",
|
||||
);
|
||||
const permissionIdByKey = await this.loadIdsByKey(
|
||||
manager,
|
||||
Permission,
|
||||
positionTypePermissions.flatMap((mapping) => mapping.permissionKeys),
|
||||
"missing_permissions",
|
||||
);
|
||||
|
||||
const repository = manager.getRepository(PositionTypePermission);
|
||||
const existing = await repository.find({
|
||||
where: { positionTypeId: In([...positionTypeIdByKey.values()]) },
|
||||
select: { positionTypeId: true, permissionId: true },
|
||||
});
|
||||
const existingPairs = new Set(
|
||||
existing.map((row) => `${row.positionTypeId}:${row.permissionId}`),
|
||||
);
|
||||
|
||||
const rows = positionTypePermissions.flatMap((mapping) => {
|
||||
const positionTypeId = positionTypeIdByKey.get(
|
||||
mapping.positionTypeKey,
|
||||
) as string;
|
||||
return mapping.permissionKeys
|
||||
.map((key) => permissionIdByKey.get(key) as string)
|
||||
.filter(
|
||||
(permissionId) =>
|
||||
!existingPairs.has(`${positionTypeId}:${permissionId}`),
|
||||
)
|
||||
.map((permissionId) => ({ positionTypeId, permissionId }));
|
||||
});
|
||||
|
||||
if (rows.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await repository.insert(rows);
|
||||
this.logger.log(`Granted ${rows.length} position type permissions`);
|
||||
}
|
||||
|
||||
private async seedOrganizationSettings(manager: EntityManager) {
|
||||
const defaults = IAM_BASELINE_SEED.organizationSettings;
|
||||
if (defaults.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const organizations = await manager
|
||||
.getRepository(Organization)
|
||||
.find({ select: { id: true } });
|
||||
const existing = await manager
|
||||
.getRepository(OrganizationSetting)
|
||||
.find({ select: { organizationId: true, key: true } });
|
||||
const existingPairs = new Set(
|
||||
existing.map((setting) => `${setting.organizationId}:${setting.key}`),
|
||||
);
|
||||
|
||||
const rows = organizations.flatMap((organization) =>
|
||||
missingSettings(defaults, existingPairs, organization.id as string).map(
|
||||
(setting) => ({
|
||||
...setting,
|
||||
type: setting.type as ESettingType,
|
||||
organizationId: organization.id as string,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await manager.getRepository(OrganizationSetting).insert(rows);
|
||||
this.logger.log(
|
||||
`Seeded ${rows.length} organization settings across ${organizations.length} organizations`,
|
||||
);
|
||||
}
|
||||
|
||||
private async seedUnitSettings(manager: EntityManager) {
|
||||
const defaults = IAM_BASELINE_SEED.unitSettings;
|
||||
if (defaults.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const units = await manager.getRepository(Unit).find({ select: { id: true } });
|
||||
const existing = await manager
|
||||
.getRepository(UnitSetting)
|
||||
.find({ select: { unitId: true, key: true } });
|
||||
const existingPairs = new Set(
|
||||
existing.map((setting) => `${setting.unitId}:${setting.key}`),
|
||||
);
|
||||
|
||||
const rows = units.flatMap((unit) =>
|
||||
missingSettings(defaults, existingPairs, unit.id as string).map(
|
||||
(setting) => ({
|
||||
...setting,
|
||||
type: setting.type as ESettingType,
|
||||
unitId: unit.id as string,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await manager.getRepository(UnitSetting).insert(rows);
|
||||
this.logger.log(
|
||||
`Seeded ${rows.length} unit settings across ${units.length} units`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* User + credential + employee + `super_admin` UserRole. Every step is
|
||||
* skip-if-present: an existing account keeps its password, its organization
|
||||
* and any extra roles it was given through the IAM UI.
|
||||
*/
|
||||
private async seedSuperAdmin(manager: EntityManager) {
|
||||
const seed = IAM_BASELINE_SEED.superAdmin;
|
||||
if (!seed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const email = process.env.SUPER_ADMIN_EMAIL?.trim() || seed.fallbackEmail;
|
||||
// SUPER_ADMIN_PASSWORD wins, DEFAULT_PASSWORD is the shared fallback.
|
||||
const password =
|
||||
process.env.SUPER_ADMIN_PASSWORD?.trim() ||
|
||||
process.env.DEFAULT_PASSWORD?.trim() ||
|
||||
seed.fallbackPassword;
|
||||
const phoneNumber = process.env.SUPER_ADMIN_PHONE?.trim() || undefined;
|
||||
|
||||
const roleIdByKey = await this.loadIdsByKey(
|
||||
manager,
|
||||
Role,
|
||||
[seed.roleKey],
|
||||
"missing_roles",
|
||||
);
|
||||
|
||||
const organization = await manager.getRepository(Organization).findOne({
|
||||
where: { key: seed.organizationKey },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!organization) {
|
||||
throw new Error(`missing_organization:${seed.organizationKey}`);
|
||||
}
|
||||
|
||||
const unit = await manager.getRepository(Unit).findOne({
|
||||
where: { key: seed.unitKey, organizationId: organization.id },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!unit) {
|
||||
throw new Error(`missing_unit:${seed.unitKey}`);
|
||||
}
|
||||
|
||||
const userRepository = manager.getRepository(User);
|
||||
const existingUser = await userRepository.findOne({
|
||||
where: [{ username: seed.username }, { email }],
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
let userId = existingUser?.id as string | undefined;
|
||||
if (!userId) {
|
||||
const inserted = await userRepository.insert({
|
||||
username: seed.username,
|
||||
email,
|
||||
phoneNumber,
|
||||
name: { ...seed.name },
|
||||
userType: EUserType.EMPLOYEE,
|
||||
status: EUserStatus.ACCEPTED,
|
||||
isActive: true,
|
||||
hasSetPassword: true,
|
||||
});
|
||||
userId = inserted.identifiers[0]?.id as string;
|
||||
this.logger.log(`Seeded super admin user '${seed.username}' (${email})`);
|
||||
}
|
||||
|
||||
const credentialRepository = manager.getRepository(UserCredential);
|
||||
if (!(await credentialRepository.existsBy({ userId }))) {
|
||||
await credentialRepository.insert({
|
||||
userId,
|
||||
password: await hashPassword(password),
|
||||
isActive: true,
|
||||
});
|
||||
this.logger.log(`Seeded super admin credential for '${seed.username}'`);
|
||||
}
|
||||
|
||||
const employeeRepository = manager.getRepository(Employee);
|
||||
if (
|
||||
!(await employeeRepository.existsBy({
|
||||
userId,
|
||||
organizationId: organization.id,
|
||||
}))
|
||||
) {
|
||||
await employeeRepository.insert({
|
||||
userId,
|
||||
organizationId: organization.id,
|
||||
unitId: unit.id,
|
||||
isCurrent: true,
|
||||
status: EEmployeeStatus.ACCEPTED,
|
||||
name: { ...seed.name },
|
||||
});
|
||||
this.logger.log(
|
||||
`Attached super admin to organization '${seed.organizationKey}'`,
|
||||
);
|
||||
}
|
||||
|
||||
// user_roles is UNIQUE (user_id, role_id).
|
||||
await manager.getRepository(UserRole).upsert(
|
||||
{
|
||||
userId,
|
||||
roleId: roleIdByKey.get(seed.roleKey) as string,
|
||||
organizationId: organization.id,
|
||||
unitId: unit.id,
|
||||
},
|
||||
{ conflictPaths: { userId: true, roleId: true } },
|
||||
);
|
||||
|
||||
this.logger.log(`Ensured '${seed.roleKey}' role on '${seed.username}'`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve `key → id` from the database for the given entity, throwing when a
|
||||
* key the seed references has no row (the seed order is wrong, or the caller
|
||||
* emptied the section that creates it).
|
||||
*/
|
||||
private async loadIdsByKey(
|
||||
manager: EntityManager,
|
||||
entity: EntityTarget<ObjectLiteral>,
|
||||
keys: string[],
|
||||
errorPrefix: string,
|
||||
): Promise<Map<string, string>> {
|
||||
const wanted = [...new Set(keys)];
|
||||
const rows = (await manager
|
||||
.getRepository(entity)
|
||||
.find({
|
||||
where: { key: In(wanted) },
|
||||
select: { id: true, key: true },
|
||||
})) as { id?: string; key: string }[];
|
||||
|
||||
const idByKey = new Map(rows.map((row) => [row.key, row.id as string]));
|
||||
|
||||
const missing = wanted.filter((key) => !idByKey.has(key));
|
||||
if (missing.length > 0) {
|
||||
throw new Error(`${errorPrefix}:${missing.join(",")}`);
|
||||
}
|
||||
|
||||
return idByKey;
|
||||
}
|
||||
}
|
||||
27
apps/edr-freight-api/src/seed/missing-settings.util.spec.ts
Normal file
27
apps/edr-freight-api/src/seed/missing-settings.util.spec.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { missingSettings } from "./missing-settings.util";
|
||||
|
||||
const defaults = [
|
||||
{ key: "primaryColor", displayName: "Primary Color", value: null },
|
||||
{ key: "logoFileUrl", displayName: "Logo", value: null },
|
||||
];
|
||||
|
||||
describe("missingSettings", () => {
|
||||
it("returns every default when the owner has none", () => {
|
||||
expect(missingSettings(defaults, new Set(), "org-1")).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("skips keys the owner has, but not the same key on another owner", () => {
|
||||
const existing = new Set(["org-1:primaryColor"]);
|
||||
|
||||
expect(
|
||||
missingSettings(defaults, existing, "org-1").map((s) => s.key),
|
||||
).toEqual(["logoFileUrl"]);
|
||||
expect(missingSettings(defaults, existing, "org-2")).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("drops nulls so the column default applies", () => {
|
||||
expect(
|
||||
missingSettings(defaults, new Set(), "org-1")[0].value,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
20
apps/edr-freight-api/src/seed/missing-settings.util.ts
Normal file
20
apps/edr-freight-api/src/seed/missing-settings.util.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Settings of `ownerId` that are not in `existingPairs` (`"<ownerId>:<key>"`)
|
||||
* yet. This is what keeps the IAM baseline seed idempotent: `organization_settings`
|
||||
* and `unit_settings` have no unique index on (owner, key), so re-running an
|
||||
* INSERT (or the package seeder's upsert-on-id, which never supplies an id)
|
||||
* silently duplicates every row.
|
||||
*
|
||||
* `value: null` becomes `undefined` so the column default applies on insert.
|
||||
*/
|
||||
export function missingSettings<
|
||||
TSetting extends { key: string; value?: string | null },
|
||||
>(
|
||||
defaults: TSetting[],
|
||||
existingPairs: Set<string>,
|
||||
ownerId: string,
|
||||
): (Omit<TSetting, "value"> & { value?: string })[] {
|
||||
return defaults
|
||||
.filter((setting) => !existingPairs.has(`${ownerId}:${setting.key}`))
|
||||
.map((setting) => ({ ...setting, value: setting.value ?? undefined }));
|
||||
}
|
||||
Reference in New Issue
Block a user