complete rule engine and booking flow

This commit is contained in:
marshal
2026-05-30 10:28:51 +03:00
84 changed files with 4622 additions and 13074 deletions

View File

@@ -20,8 +20,9 @@ import { DropdownSettingsModule } from "./modules/dropdown-settings/dropdown-set
import { OtpModule } from './modules/otp/otp.module';
import { RuleEngineModule } from "./modules/rule-engine/rule-engine.module";
import { BackofficeModule } from "./modules/backoffice/backoffice.module";
import { DemoPermissionsModule } from "./modules/demo-permissions/demo-permissions.module";
import { EdrOrgSeeder } from "./seed/edr-org.seeder";
import { DemoUsersSeeder } from "./seed/demo-users.seeder";
@Module({
imports: [
@@ -49,17 +50,20 @@ import { EdrOrgSeeder } from "./seed/edr-org.seeder";
OtpModule,
RuleEngineModule,
BackofficeModule,
DemoPermissionsModule,
],
providers: [EdrOrgSeeder],
providers: [EdrOrgSeeder, DemoUsersSeeder],
})
export class AppModule implements OnApplicationBootstrap {
constructor(
private readonly seeder: DataSeeder,
private readonly edrOrgSeeder: EdrOrgSeeder,
) {}
private readonly demoUsersSeeder: DemoUsersSeeder,
) { }
async onApplicationBootstrap() {
await this.seeder.run();
await this.edrOrgSeeder.run();
await this.demoUsersSeeder.run();
}
}

View File

@@ -0,0 +1,22 @@
import { Controller, Get, UseGuards } from "@nestjs/common";
import { ApiOperation, ApiTags } from "@nestjs/swagger";
import { PermissionGuard } from "@tria-plc/api-common/modules/auth/services/permission.guard";
@ApiTags("demo-permissions")
@Controller()
export class DemoPermissionsController {
@Get("test_user1")
@ApiOperation({ summary: "Permission demo (can:demo:user1)" })
@UseGuards(PermissionGuard(["can:demo:user1"]))
testUser1() {
return { ok: true, permission: "can:demo:user1" };
}
@Get("test_user2")
@ApiOperation({ summary: "Permission demo (can:demo:user2)" })
@UseGuards(PermissionGuard(["can:demo:user2"]))
testUser2() {
return { ok: true, permission: "can:demo:user2" };
}
}

View File

@@ -0,0 +1,8 @@
import { Module } from "@nestjs/common";
import { DemoPermissionsController } from "./demo-permissions.controller";
@Module({
controllers: [DemoPermissionsController],
})
export class DemoPermissionsModule {}

View File

@@ -0,0 +1,213 @@
import { Injectable, Logger } from "@nestjs/common";
import { hashPassword } from "@tria-plc/api-common/utils/argon";
import { EUserStatus } from "@tria-plc/api-common/utils/enums/user.enum";
import { ERoleKey } from "@tria-plc/api-common/utils/enums/seed.enum";
import {
Employee,
Organization,
Permission,
Role,
RolePermission,
User,
UserCredential,
UserRole,
} from "@tria-plc/iamapi-common";
import { DataSource } from "typeorm";
const SEED_FLAG = "SEED_DEMO_USERS";
const DEMO_ORG_KEY = "demo_iam";
const DEMO_ORG_NAME = { en: "Demo IAM" };
const DEMO_PERMISSIONS = [
{ key: "can:demo:user1", name: { en: "Can access demo user1" } },
{ key: "can:demo:user2", name: { en: "Can access demo user2" } },
];
const DEMO_ROLES = [
{ key: "demo_user1", name: { en: "Demo User1" } },
{ key: "demo_user2", name: { en: "Demo User2" } },
];
const DEMO_USERS = [
{
email: "user@gmail.com",
username: "user",
name: { en: "Demo User 1" },
roleKey: "demo_user1",
},
{
email: "user2@gmail.com",
username: "user2",
name: { en: "Demo User 2" },
roleKey: "demo_user2",
},
];
@Injectable()
export class DemoUsersSeeder {
private readonly logger = new Logger(DemoUsersSeeder.name);
constructor(private readonly dataSource: DataSource) {}
async run() {
const shouldSeed = process.env[SEED_FLAG]?.trim().toLowerCase() === "true";
if (!shouldSeed) {
this.logger.log(`Skipping demo user seed because ${SEED_FLAG} is not enabled`);
return;
}
await this.dataSource.transaction(async (manager) => {
const organizationRepository = manager.getRepository(Organization);
const employeeRepository = manager.getRepository(Employee);
const permissionRepository = manager.getRepository(Permission);
const roleRepository = manager.getRepository(Role);
const rolePermissionRepository = manager.getRepository(RolePermission);
const userRepository = manager.getRepository(User);
const userCredentialRepository = manager.getRepository(UserCredential);
const userRoleRepository = manager.getRepository(UserRole);
await organizationRepository.upsert(
{
key: DEMO_ORG_KEY,
name: DEMO_ORG_NAME,
// status defaults to ACTIVE in IAM entity
isGovernmentOrganization: true,
},
{ conflictPaths: { key: true } },
);
const organization = await organizationRepository.findOne({
where: { key: DEMO_ORG_KEY },
select: { id: true, key: true },
});
if (!organization) {
throw new Error("demo_org_seed_failed");
}
await permissionRepository.upsert(DEMO_PERMISSIONS, {
conflictPaths: { key: true },
});
await roleRepository.upsert(DEMO_ROLES, {
conflictPaths: { key: true },
});
const roles = await roleRepository.find({ where: DEMO_ROLES.map((r) => ({ key: r.key })) });
const permissions = await permissionRepository.find({
where: DEMO_PERMISSIONS.map((p) => ({ key: p.key })),
});
const roleByKey = new Map(roles.map((r) => [r.key, r]));
const permissionByKey = new Map(permissions.map((p) => [p.key, p]));
const superAdminRole = await roleRepository.findOne({
where: { key: ERoleKey.SUPER_ADMIN },
select: { id: true, key: true },
});
const rolePermissionsToUpsert = [
{
roleId: roleByKey.get("demo_user1")!.id,
permissionId: permissionByKey.get("can:demo:user1")!.id,
},
{
roleId: roleByKey.get("demo_user2")!.id,
permissionId: permissionByKey.get("can:demo:user2")!.id,
},
...(superAdminRole
? ([
{
roleId: superAdminRole.id,
permissionId: permissionByKey.get("can:demo:user1")!.id,
},
{
roleId: superAdminRole.id,
permissionId: permissionByKey.get("can:demo:user2")!.id,
},
] as Array<{ roleId: string; permissionId: string }>)
: []),
];
await rolePermissionRepository.upsert(rolePermissionsToUpsert, {
conflictPaths: { roleId: true, permissionId: true },
});
const hashedPassword = await hashPassword("12345678");
for (const demoUser of DEMO_USERS) {
const existingUser = await userRepository.findOne({
where: { email: demoUser.email },
select: { id: true, email: true },
});
let user = existingUser;
if (!user) {
user = await userRepository.save(
userRepository.create({
email: demoUser.email,
username: demoUser.username,
name: demoUser.name,
isActive: true,
hasSetPassword: true,
status: EUserStatus.ACCEPTED,
}),
);
}
// Ensure an active credential exists for login.
const activeCredentialExists = await userCredentialRepository.exists({
where: {
userId: user.id,
isActive: true,
},
});
if (!activeCredentialExists) {
await userCredentialRepository.insert({
userId: user.id,
password: hashedPassword,
isActive: true,
});
}
// Login query requires a current employee in an ACTIVE organization.
const employeeExists = await employeeRepository.exists({
where: {
userId: user.id,
organizationId: organization.id,
isCurrent: true,
},
});
if (!employeeExists) {
await employeeRepository.insert({
userId: user.id,
organizationId: organization.id,
isCurrent: true,
name: demoUser.name,
});
}
const role = roleByKey.get(demoUser.roleKey);
if (!role) {
throw new Error(`missing_role:${demoUser.roleKey}`);
}
await userRoleRepository.upsert(
{
userId: user.id,
roleId: role.id,
organizationId: organization.id,
},
{ conflictPaths: { userId: true, roleId: true } },
);
}
});
this.logger.log(
"Seeded demo users + permissions (user@gmail.com, user2@gmail.com; permissions can:demo:user1/can:demo:user2)",
);
}
}