mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 19:00:55 +00:00
feat: ( passenger ) wire IAM global guard, org seeder, and backoffice auth
This commit is contained in:
@@ -8,10 +8,15 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';
|
||||
// Subpath import: the @tria-plc/iamapi-common barrel does not resolve under moduleResolution:"Node".
|
||||
// Aliased to avoid clashing with the app's existing custom ./common/iam.module (remote IamGuard).
|
||||
import { IamModule as TriaIamModule } from '@tria-plc/iamapi-common/iam.module';
|
||||
import { DataSeeder } from '@tria-plc/iamapi-common/db/seed/seeder';
|
||||
import { SharedAuthModule } from '@tria-plc/api-common/modules/auth/shared-auth.module';
|
||||
import {
|
||||
EDR_PASSENGER_APPLICATION,
|
||||
EDR_PASSENGER_PERMISSIONS,
|
||||
} from './seed/edr-passenger.seed';
|
||||
import { EdrPassengerOrgSeeder } from './seed/edr-passenger-org.seeder';
|
||||
import { PassengerStaffUsersSeeder } from './seed/passenger-staff-users.seeder';
|
||||
import { PrismaModule } from './common/prisma.module';
|
||||
import { AuditModule } from './common/audit.module';
|
||||
import { I18nModule } from './common/i18n/i18n.module';
|
||||
@@ -77,7 +82,11 @@ import { CurrenciesModule } from './modules/currencies/currencies.module';
|
||||
useFactory: (config: ConfigService): TypeOrmModuleOptions =>
|
||||
config.get<TypeOrmModuleOptions>('iamDatabase')!,
|
||||
}),
|
||||
TriaIamModule.forRoot(),
|
||||
TriaIamModule.forRoot({
|
||||
applications: [EDR_PASSENGER_APPLICATION],
|
||||
permissions: EDR_PASSENGER_PERMISSIONS,
|
||||
}),
|
||||
SharedAuthModule,
|
||||
PrismaModule,
|
||||
AuditModule,
|
||||
I18nModule,
|
||||
@@ -108,21 +117,25 @@ import { CurrenciesModule } from './modules/currencies/currencies.module';
|
||||
AuditModuleFeature,
|
||||
CurrenciesModule,
|
||||
],
|
||||
providers: [
|
||||
EdrPassengerOrgSeeder,
|
||||
PassengerStaffUsersSeeder,
|
||||
],
|
||||
})
|
||||
export class AppModule implements OnApplicationBootstrap {
|
||||
constructor(
|
||||
private readonly seeder: DataSeeder,
|
||||
// private readonly edrOrgSeeder: EdrOrgSeeder,
|
||||
// private readonly demoUsersSeeder: DemoUsersSeeder,
|
||||
) { }
|
||||
private readonly seeder: DataSeeder,
|
||||
private readonly edrPassengerOrgSeeder: EdrPassengerOrgSeeder,
|
||||
private readonly passengerStaffUsersSeeder: PassengerStaffUsersSeeder,
|
||||
) {}
|
||||
|
||||
async onApplicationBootstrap() {
|
||||
try {
|
||||
await this.seeder.run();
|
||||
} catch (err) {
|
||||
console.error('[DataSeeder] Seed failed (non-fatal during IAM migration phase):', (err as Error).message);
|
||||
console.error('[DataSeeder] Seed failed (non-fatal):', (err as Error).message);
|
||||
}
|
||||
// await this.edrOrgSeeder.run();
|
||||
// await this.demoUsersSeeder.run();
|
||||
await this.edrPassengerOrgSeeder.run();
|
||||
await this.passengerStaffUsersSeeder.run();
|
||||
}
|
||||
}
|
||||
|
||||
14
apps/edr-passenger-api/src/common/passenger-guards.ts
Normal file
14
apps/edr-passenger-api/src/common/passenger-guards.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { applyDecorators, UseGuards } from '@nestjs/common';
|
||||
import { JwtGuard } from '@tria-plc/api-common/modules/auth/services/jwt.guard';
|
||||
import { PassengerPermissionGuard } from './passenger-permission.guard';
|
||||
import { PASSENGER_PERMS } from '../seed/passenger-permissions.registry';
|
||||
|
||||
export const PassengerStaff = (permission: string | string[]) =>
|
||||
applyDecorators(
|
||||
UseGuards(
|
||||
JwtGuard,
|
||||
PassengerPermissionGuard(Array.isArray(permission) ? permission : [permission]),
|
||||
),
|
||||
);
|
||||
|
||||
export const PassengerAdmin = () => PassengerStaff(PASSENGER_PERMS.admin);
|
||||
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
Type,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { hasPassengerPermission } from './passenger-permission.util';
|
||||
|
||||
export function PassengerPermissionGuard(permissions: string[]): Type<CanActivate> {
|
||||
@Injectable()
|
||||
class PassengerPermissionsGuard implements CanActivate {
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const request = context.switchToHttp().getRequest<{ user?: any }>();
|
||||
const user = request.user;
|
||||
|
||||
if (!permissions?.length) return true;
|
||||
if (!user) throw new UnauthorizedException('Authentication required');
|
||||
|
||||
if (permissions.some((p) => hasPassengerPermission(user, p))) return true;
|
||||
|
||||
throw new ForbiddenException(
|
||||
`Missing permission. Required one of: ${permissions.join(', ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return PassengerPermissionsGuard;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { ForbiddenException } from '@nestjs/common';
|
||||
|
||||
const SUPER_ADMIN_ROLE = 'super_admin';
|
||||
const ORGANIZATION_ADMIN_ROLE = 'organization_admin';
|
||||
|
||||
type PermissionLike = { key?: string };
|
||||
type MeLikeUser = {
|
||||
roles?: { key?: string }[];
|
||||
permissions?: PermissionLike[];
|
||||
employee?:
|
||||
| { position?: { permissions?: PermissionLike[] }; delegatedPositions?: { permissions?: PermissionLike[] }[] }
|
||||
| { positions?: { permissions?: PermissionLike[] }[] }[]
|
||||
| null;
|
||||
};
|
||||
|
||||
export function isSuperAdmin(user: MeLikeUser | null | undefined): boolean {
|
||||
return user?.roles?.some((r) => r.key === SUPER_ADMIN_ROLE) ?? false;
|
||||
}
|
||||
|
||||
export function isOrganizationAdmin(user: MeLikeUser | null | undefined): boolean {
|
||||
return user?.roles?.some((r) => r.key === ORGANIZATION_ADMIN_ROLE) ?? false;
|
||||
}
|
||||
|
||||
export function collectPermissionKeys(user: MeLikeUser | null | undefined): string[] {
|
||||
if (!user) return [];
|
||||
|
||||
const keys = new Set<string>();
|
||||
|
||||
for (const p of user.permissions ?? []) {
|
||||
if (p.key) keys.add(p.key);
|
||||
}
|
||||
|
||||
const employee = user.employee;
|
||||
if (!employee) return [...keys];
|
||||
|
||||
if (Array.isArray(employee)) {
|
||||
for (const emp of employee) {
|
||||
for (const pos of emp.positions ?? []) {
|
||||
for (const p of pos.permissions ?? []) {
|
||||
if (p.key) keys.add(p.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...keys];
|
||||
}
|
||||
|
||||
for (const p of employee.position?.permissions ?? []) {
|
||||
if (p.key) keys.add(p.key);
|
||||
}
|
||||
for (const delegated of employee.delegatedPositions ?? []) {
|
||||
for (const p of delegated.permissions ?? []) {
|
||||
if (p.key) keys.add(p.key);
|
||||
}
|
||||
}
|
||||
|
||||
return [...keys];
|
||||
}
|
||||
|
||||
export function hasPassengerPermission(
|
||||
user: MeLikeUser | null | undefined,
|
||||
permissionKey: string,
|
||||
): boolean {
|
||||
if (!user) return false;
|
||||
if (isSuperAdmin(user) || isOrganizationAdmin(user)) return true;
|
||||
return collectPermissionKeys(user).includes(permissionKey);
|
||||
}
|
||||
|
||||
export function assertPassengerPermission(
|
||||
user: MeLikeUser | null | undefined,
|
||||
permissionKey: string,
|
||||
): void {
|
||||
if (hasPassengerPermission(user, permissionKey)) return;
|
||||
throw new ForbiddenException(`Missing permission: ${permissionKey}`);
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
|
||||
import { Controller, Get, Param, Query } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
|
||||
import { AuditService } from '../../common/audit.service';
|
||||
import { IamGuard } from '../../common/iam-adapter';
|
||||
import { PassengerStaff } from '../../common/passenger-guards';
|
||||
import { PASSENGER_PERMS } from '../../seed/passenger-permissions.registry';
|
||||
|
||||
@ApiTags('Audit')
|
||||
@Controller('audit')
|
||||
@UseGuards(IamGuard)
|
||||
@PassengerStaff([PASSENGER_PERMS.audit.view, PASSENGER_PERMS.admin])
|
||||
@ApiBearerAuth('IAM-auth')
|
||||
export class AuditController {
|
||||
constructor(private auditService: AuditService) {}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Body, Controller, Post, HttpCode, HttpStatus, UseGuards, Get, Request, UnauthorizedException } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBody, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { IsPublic } from '@tria-plc/api-common/modules/auth/decorators/public.decorator';
|
||||
import { PassengerAuthService } from './passenger-auth.service';
|
||||
import { RegisterDto, LoginDto } from './auth.dto';
|
||||
import { JwtGuard } from '../../common/jwt.guard';
|
||||
@@ -10,6 +11,7 @@ export class AuthController {
|
||||
constructor(private passengerAuthService: PassengerAuthService) {}
|
||||
|
||||
@Post('register')
|
||||
@IsPublic()
|
||||
@ApiOperation({ summary: 'Register new passenger account' })
|
||||
@ApiResponse({ status: 201, description: 'Account created. Returns token + user.' })
|
||||
@ApiResponse({ status: 409, description: 'Email or phone already registered' })
|
||||
@@ -19,6 +21,7 @@ export class AuthController {
|
||||
}
|
||||
|
||||
@Post('login')
|
||||
@IsPublic()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: 'Login with email and password' })
|
||||
@ApiResponse({ status: 200, description: 'Login successful. Returns token + passengerId.' })
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { Controller, Get, Post, Patch, Delete, Body, Param, HttpCode, UseGuards } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Patch, Delete, Body, Param, HttpCode } from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { CurrenciesService } from './currencies.service';
|
||||
import { CreateCurrencyDto, UpdateCurrencyDto } from './currencies.dto';
|
||||
import { IamGuard } from '../../common/iam-adapter';
|
||||
import { Roles } from '../../common/roles.decorator';
|
||||
import { RolesGuard } from '../../common/roles.guard';
|
||||
import { PassengerAdmin, PassengerStaff } from '../../common/passenger-guards';
|
||||
import { PASSENGER_PERMS } from '../../seed/passenger-permissions.registry';
|
||||
|
||||
@ApiTags('Currencies')
|
||||
@Controller('currencies')
|
||||
@@ -17,8 +16,7 @@ export class CurrenciesController {
|
||||
}
|
||||
|
||||
@Post()
|
||||
@UseGuards(IamGuard, RolesGuard)
|
||||
@Roles('ADMIN')
|
||||
@PassengerStaff(PASSENGER_PERMS.currencies.manage)
|
||||
@ApiBearerAuth('IAM-auth')
|
||||
@HttpCode(201)
|
||||
createCurrency(@Body() dto: CreateCurrencyDto) {
|
||||
@@ -26,24 +24,21 @@ export class CurrenciesController {
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@UseGuards(IamGuard, RolesGuard)
|
||||
@Roles('ADMIN')
|
||||
@PassengerStaff(PASSENGER_PERMS.currencies.manage)
|
||||
@ApiBearerAuth('IAM-auth')
|
||||
updateCurrency(@Param('id') id: string, @Body() dto: UpdateCurrencyDto) {
|
||||
return this.currenciesService.updateCurrency(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@UseGuards(IamGuard, RolesGuard)
|
||||
@Roles('ADMIN')
|
||||
@PassengerAdmin()
|
||||
@ApiBearerAuth('IAM-auth')
|
||||
deleteCurrency(@Param('id') id: string) {
|
||||
return this.currenciesService.deleteCurrency(id);
|
||||
}
|
||||
|
||||
@Post('sync-rates')
|
||||
@UseGuards(IamGuard, RolesGuard)
|
||||
@Roles('ADMIN')
|
||||
@PassengerStaff(PASSENGER_PERMS.currencies.manage)
|
||||
@ApiBearerAuth('IAM-auth')
|
||||
@HttpCode(200)
|
||||
syncRates() {
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { Controller, Get, Post, Body, Query, UseGuards, Logger } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Body, Query, Logger } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { FraudService, FraudRuleConfig } from './fraud.service';
|
||||
// IAM auth: validate the IAM session token via @tria-plc/api-common's DB-backed JwtGuard.
|
||||
import { JwtGuard as IamJwtGuard } from '@tria-plc/api-common/modules/auth/services/jwt.guard';
|
||||
import { PassengerStaff } from '../../common/passenger-guards';
|
||||
import { PASSENGER_PERMS } from '../../seed/passenger-permissions.registry';
|
||||
|
||||
@ApiTags('Fraud Detection')
|
||||
@Controller('fraud')
|
||||
// TODO(iam-authz): restrict per route via @UseGuards(PermissionGuard([...])) once the IAM
|
||||
// role→permission mapping (EIamPermissionKey) is confirmed. For now: authenticated IAM users only.
|
||||
@UseGuards(IamJwtGuard)
|
||||
@PassengerStaff([PASSENGER_PERMS.fraud.view, PASSENGER_PERMS.admin])
|
||||
@ApiBearerAuth('IAM-auth')
|
||||
export class FraudController {
|
||||
private readonly logger = new Logger(FraudController.name);
|
||||
@@ -43,6 +41,7 @@ export class FraudController {
|
||||
* Create or update fraud rule
|
||||
*/
|
||||
@Post('rules')
|
||||
@PassengerStaff([PASSENGER_PERMS.fraud.manage, PASSENGER_PERMS.admin])
|
||||
@ApiOperation({ summary: 'Create or update fraud rule' })
|
||||
async upsertRule(@Body() body: { type: string; config: FraudRuleConfig }) {
|
||||
const rule = await this.fraudService.upsertRule(body.type, body.config);
|
||||
@@ -53,6 +52,7 @@ export class FraudController {
|
||||
* Block user temporarily
|
||||
*/
|
||||
@Post('actions/block')
|
||||
@PassengerStaff([PASSENGER_PERMS.fraud.manage, PASSENGER_PERMS.admin])
|
||||
@ApiOperation({ summary: 'Block user temporarily' })
|
||||
async blockUser(@Body() body: { iamUserId: string; durationMinutes: number }) {
|
||||
await this.fraudService.blockUserTemporarily(body.iamUserId, body.durationMinutes);
|
||||
@@ -63,6 +63,7 @@ export class FraudController {
|
||||
* Unblock user
|
||||
*/
|
||||
@Post('actions/unblock')
|
||||
@PassengerStaff([PASSENGER_PERMS.fraud.manage, PASSENGER_PERMS.admin])
|
||||
@ApiOperation({ summary: 'Unblock user' })
|
||||
async unblockUser(@Body() body: { iamUserId: string }) {
|
||||
await this.fraudService.unblockUser(body.iamUserId);
|
||||
|
||||
@@ -2,9 +2,8 @@ import { Controller, Get, Param, Patch, Post, Body, UseGuards } from '@nestjs/co
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiBody } from '@nestjs/swagger';
|
||||
import { NotificationsService } from './notifications.service';
|
||||
import { JwtGuard } from '../../common/jwt.guard';
|
||||
import { IamGuard } from '../../common/iam-adapter';
|
||||
import { Roles } from '../../common/roles.decorator';
|
||||
import { RolesGuard } from '../../common/roles.guard';
|
||||
import { PassengerStaff } from '../../common/passenger-guards';
|
||||
import { PASSENGER_PERMS } from '../../seed/passenger-permissions.registry';
|
||||
import { TestNotificationDto } from './notifications.dto';
|
||||
import { EmailClientService } from './email-client.service';
|
||||
import { SmsClientService } from './sms-client.service';
|
||||
@@ -41,8 +40,7 @@ export class NotificationsController {
|
||||
}
|
||||
|
||||
@Post('send/email')
|
||||
@UseGuards(IamGuard, RolesGuard)
|
||||
@Roles('ADMIN', 'STAFF')
|
||||
@PassengerStaff([PASSENGER_PERMS.notifications.send, PASSENGER_PERMS.admin])
|
||||
@ApiOperation({ summary: 'Send a direct email via the email microservice' })
|
||||
@ApiBody({ type: SendEmail })
|
||||
sendEmail(@Body() dto: SendEmail) {
|
||||
@@ -50,8 +48,7 @@ export class NotificationsController {
|
||||
}
|
||||
|
||||
@Post('send/sms')
|
||||
@UseGuards(IamGuard, RolesGuard)
|
||||
@Roles('ADMIN', 'STAFF')
|
||||
@PassengerStaff([PASSENGER_PERMS.notifications.send, PASSENGER_PERMS.admin])
|
||||
@ApiOperation({ summary: 'Send a direct SMS via the SMS microservice' })
|
||||
@ApiBody({ type: SingleMessageDto })
|
||||
sendSms(@Body() dto: SingleMessageDto) {
|
||||
@@ -59,8 +56,7 @@ export class NotificationsController {
|
||||
}
|
||||
|
||||
@Post('send/sms/bulk')
|
||||
@UseGuards(IamGuard, RolesGuard)
|
||||
@Roles('ADMIN', 'STAFF')
|
||||
@PassengerStaff([PASSENGER_PERMS.notifications.send, PASSENGER_PERMS.admin])
|
||||
@ApiOperation({ summary: 'Send bulk SMS messages via the SMS microservice' })
|
||||
@ApiBody({ type: BulkMessagesDto })
|
||||
sendBulkSms(@Body() dto: BulkMessagesDto) {
|
||||
@@ -68,8 +64,6 @@ export class NotificationsController {
|
||||
}
|
||||
|
||||
@Post('test')
|
||||
// TODO(iam-authz): restrict to admin/staff via IAM PermissionGuard once role→permission mapping
|
||||
// is confirmed. Currently protected by the class-level JwtGuard only.
|
||||
@ApiOperation({ summary: 'Test notification delivery (Admin only)' })
|
||||
async testNotification(@Body() dto: TestNotificationDto) {
|
||||
return this.service.send(
|
||||
|
||||
@@ -28,9 +28,8 @@ import {
|
||||
PaymentMethodTypeEnum,
|
||||
PaymentPlatformDto,
|
||||
} from "./payments.dto";
|
||||
import { JwtGuard } from "../../common/jwt.guard";
|
||||
import { RolesGuard } from "../../common/roles.guard";
|
||||
import { Roles } from "../../common/roles.decorator";
|
||||
import { PassengerStaff } from "../../common/passenger-guards";
|
||||
import { PASSENGER_PERMS } from "../../seed/passenger-permissions.registry";
|
||||
|
||||
@ApiTags("Payment")
|
||||
@Controller("payments")
|
||||
@@ -38,9 +37,8 @@ export class PaymentsController {
|
||||
constructor(private service: PaymentsService) {}
|
||||
|
||||
@Get("all")
|
||||
@UseGuards(JwtGuard, RolesGuard)
|
||||
@Roles('ADMIN', 'SUPERVISOR', 'STAFF')
|
||||
@ApiBearerAuth("JWT-auth")
|
||||
@PassengerStaff([PASSENGER_PERMS.payments.viewAll, PASSENGER_PERMS.admin])
|
||||
@ApiBearerAuth("IAM-auth")
|
||||
@ApiOperation({ summary: "Get all payments with filters (staff/admin only)" })
|
||||
@ApiQuery({ name: "search", required: false })
|
||||
@ApiQuery({ name: "status", required: false })
|
||||
@@ -101,18 +99,16 @@ export class PaymentsController {
|
||||
}
|
||||
|
||||
@Post("refund")
|
||||
@UseGuards(JwtGuard, RolesGuard)
|
||||
@Roles('ADMIN', 'STAFF', 'AGENT')
|
||||
@ApiBearerAuth("JWT-auth")
|
||||
@PassengerStaff([PASSENGER_PERMS.payments.refund, PASSENGER_PERMS.admin])
|
||||
@ApiBearerAuth("IAM-auth")
|
||||
@ApiOperation({ summary: "Refund a confirmed booking (staff/agent only)" })
|
||||
refund(@Body() dto: RefundDto) {
|
||||
return this.service.refund(dto);
|
||||
}
|
||||
|
||||
@Post("methods")
|
||||
@UseGuards(JwtGuard, RolesGuard)
|
||||
@Roles('ADMIN', 'STAFF')
|
||||
@ApiBearerAuth("JWT-auth")
|
||||
@PassengerStaff([PASSENGER_PERMS.payments.manageMethods, PASSENGER_PERMS.admin])
|
||||
@ApiBearerAuth("IAM-auth")
|
||||
@ApiOperation({
|
||||
summary: "Add a payment system to the platform catalog (admin only)",
|
||||
})
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { ReportsService } from './reports.service';
|
||||
import { GenerateReportDto } from './reports.dto';
|
||||
// IAM auth: validate the IAM session token via @tria-plc/api-common's DB-backed JwtGuard.
|
||||
import { JwtGuard as IamJwtGuard } from '@tria-plc/api-common/modules/auth/services/jwt.guard';
|
||||
import { PassengerStaff } from '../../common/passenger-guards';
|
||||
import { PASSENGER_PERMS } from '../../seed/passenger-permissions.registry';
|
||||
|
||||
@ApiTags('Reports')
|
||||
@Controller('reports')
|
||||
// TODO(iam-authz): restrict per route via @UseGuards(PermissionGuard([...])) once the IAM
|
||||
// role→permission mapping (EIamPermissionKey) is confirmed. For now: authenticated IAM users only.
|
||||
@UseGuards(IamJwtGuard)
|
||||
@PassengerStaff([PASSENGER_PERMS.reports.view, PASSENGER_PERMS.admin])
|
||||
@ApiBearerAuth('IAM-auth')
|
||||
export class ReportsController {
|
||||
constructor(private service: ReportsService) {}
|
||||
|
||||
165
apps/edr-passenger-api/src/seed/edr-passenger-org.seeder.ts
Normal file
165
apps/edr-passenger-api/src/seed/edr-passenger-org.seeder.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
Application,
|
||||
Organization,
|
||||
OrganizationConfiguration,
|
||||
Permission,
|
||||
Role,
|
||||
RolePermission,
|
||||
} from '@tria-plc/iamapi-common';
|
||||
import { DataSource, EntityManager, In } from 'typeorm';
|
||||
import { ERoleKey } from '@tria-plc/api-common/utils/enums/seed.enum';
|
||||
import {
|
||||
PASSENGER_PERMISSIONS,
|
||||
PASSENGER_PERMISSION_KEYS,
|
||||
} from './passenger-permissions.registry';
|
||||
import { EDR_PASSENGER_APPLICATION, EDR_PASSENGER_ROLES, type PassengerSeedRole } from './edr-passenger.seed';
|
||||
|
||||
const EDR_ORG_KEY = 'edr';
|
||||
const EDR_ORG_NAME = { am: 'EDR', en: 'EDR' };
|
||||
const SEED_FLAG = 'SEED_EDR_PASSENGER_ORG';
|
||||
|
||||
type SeedOrganization = { id: string; key: string };
|
||||
|
||||
@Injectable()
|
||||
export class EdrPassengerOrgSeeder {
|
||||
private readonly logger = new Logger(EdrPassengerOrgSeeder.name);
|
||||
|
||||
constructor(private readonly dataSource: DataSource) {}
|
||||
|
||||
async run() {
|
||||
if (process.env[SEED_FLAG]?.trim().toLowerCase() !== 'true') {
|
||||
this.logger.log(`Skipping passenger org seed because ${SEED_FLAG} is not enabled`);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
await this.ensureApplication(manager);
|
||||
await this.ensurePermissions(manager);
|
||||
const organization = await this.ensureOrganization(manager);
|
||||
await this.ensureOrganizationConfiguration(manager, organization.id);
|
||||
await this.ensureRoles(manager, EDR_PASSENGER_ROLES);
|
||||
await this.ensureRolePermissions(manager, EDR_PASSENGER_ROLES);
|
||||
await this.ensureSuperAdminPermissions(manager);
|
||||
});
|
||||
|
||||
this.logger.log(`Ensured EDR passenger organization seed for '${EDR_ORG_KEY}'`);
|
||||
}
|
||||
|
||||
private async ensureApplication(manager: EntityManager) {
|
||||
await manager.getRepository(Application).upsert(
|
||||
{
|
||||
id: EDR_PASSENGER_APPLICATION.id,
|
||||
key: EDR_PASSENGER_APPLICATION.key,
|
||||
name: EDR_PASSENGER_APPLICATION.name,
|
||||
},
|
||||
{ conflictPaths: { key: true } },
|
||||
);
|
||||
this.logger.log(`Ensured application '${EDR_PASSENGER_APPLICATION.key}'`);
|
||||
}
|
||||
|
||||
private async ensurePermissions(manager: EntityManager) {
|
||||
await manager.getRepository(Permission).upsert(
|
||||
PASSENGER_PERMISSIONS.map((p) => ({
|
||||
id: p.id,
|
||||
key: p.key,
|
||||
name: p.name,
|
||||
applicationId: EDR_PASSENGER_APPLICATION.id,
|
||||
})),
|
||||
{ conflictPaths: { key: true } },
|
||||
);
|
||||
this.logger.log(`Ensured ${PASSENGER_PERMISSIONS.length} passenger permissions`);
|
||||
}
|
||||
|
||||
private async ensureOrganization(manager: EntityManager): Promise<SeedOrganization> {
|
||||
const repo = manager.getRepository(Organization);
|
||||
let org = await repo.findOne({ where: { key: EDR_ORG_KEY }, select: { id: true, key: true } });
|
||||
|
||||
if (!org) {
|
||||
const result = await repo.insert({
|
||||
key: EDR_ORG_KEY,
|
||||
name: EDR_ORG_NAME,
|
||||
isGovernmentOrganization: true,
|
||||
});
|
||||
this.logger.log(`Seeded EDR passenger organization '${EDR_ORG_KEY}'`);
|
||||
return { id: result.identifiers[0]?.id as string, key: EDR_ORG_KEY };
|
||||
}
|
||||
|
||||
this.logger.log(`Ensured EDR passenger organization '${EDR_ORG_KEY}'`);
|
||||
return { id: org.id as string, key: EDR_ORG_KEY };
|
||||
}
|
||||
|
||||
private async ensureOrganizationConfiguration(manager: EntityManager, organizationId: string) {
|
||||
await manager.getRepository(OrganizationConfiguration).upsert(
|
||||
{ organizationId, canCreateBranchByItself: true, canStartReceivingRecord: true },
|
||||
{ conflictPaths: { organizationId: true } },
|
||||
);
|
||||
this.logger.log(`Ensured organization configuration for '${EDR_ORG_KEY}'`);
|
||||
}
|
||||
|
||||
private async ensureRoles(manager: EntityManager, seedRoles: PassengerSeedRole[]) {
|
||||
await manager.getRepository(Role).upsert(
|
||||
seedRoles.map(({ key, name }) => ({ key, name })),
|
||||
{ conflictPaths: { key: true } },
|
||||
);
|
||||
this.logger.log(`Ensured passenger roles: ${seedRoles.map((r) => r.key).join(', ')}`);
|
||||
}
|
||||
|
||||
private async ensureRolePermissions(manager: EntityManager, seedRoles: PassengerSeedRole[]) {
|
||||
const allPermissionKeys = [...new Set(seedRoles.flatMap((r) => r.permissionKeys))];
|
||||
if (!allPermissionKeys.length) return;
|
||||
|
||||
const roles = await manager.getRepository(Role).find({
|
||||
where: { key: In(seedRoles.map((r) => r.key)) },
|
||||
select: { id: true, key: true },
|
||||
});
|
||||
const permissions = await manager.getRepository(Permission).find({
|
||||
where: { key: In(allPermissionKeys) },
|
||||
select: { id: true, key: true },
|
||||
});
|
||||
|
||||
const roleByKey = new Map(roles.map((r) => [r.key, r]));
|
||||
const permByKey = new Map(permissions.map((p) => [p.key, p]));
|
||||
|
||||
const links = seedRoles.flatMap((seedRole) => {
|
||||
const role = roleByKey.get(seedRole.key);
|
||||
if (!role) throw new Error(`missing_role:${seedRole.key}`);
|
||||
|
||||
return seedRole.permissionKeys.map((key) => {
|
||||
const perm = permByKey.get(key);
|
||||
if (!perm) throw new Error(`missing_permission:${key}`);
|
||||
return { roleId: role.id, permissionId: perm.id };
|
||||
});
|
||||
});
|
||||
|
||||
await manager.getRepository(RolePermission).upsert(links, {
|
||||
conflictPaths: { roleId: true, permissionId: true },
|
||||
});
|
||||
this.logger.log(`Ensured ${links.length} passenger role-permission links`);
|
||||
}
|
||||
|
||||
private async ensureSuperAdminPermissions(manager: EntityManager) {
|
||||
const role = await manager.getRepository(Role).findOne({
|
||||
where: { key: ERoleKey.SUPER_ADMIN },
|
||||
select: { id: true, key: true },
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
this.logger.warn(`Role ${ERoleKey.SUPER_ADMIN} not found; skipping super_admin permission links`);
|
||||
return;
|
||||
}
|
||||
|
||||
const permissions = await manager.getRepository(Permission).find({
|
||||
where: { key: In(PASSENGER_PERMISSION_KEYS) },
|
||||
select: { id: true, key: true },
|
||||
});
|
||||
|
||||
if (!permissions.length) return;
|
||||
|
||||
await manager.getRepository(RolePermission).upsert(
|
||||
permissions.map((p) => ({ roleId: role.id, permissionId: p.id })),
|
||||
{ conflictPaths: { roleId: true, permissionId: true } },
|
||||
);
|
||||
this.logger.log(`Ensured ${permissions.length} passenger permissions on super_admin`);
|
||||
}
|
||||
}
|
||||
47
apps/edr-passenger-api/src/seed/edr-passenger.seed.ts
Normal file
47
apps/edr-passenger-api/src/seed/edr-passenger.seed.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import {
|
||||
PASSENGER_PERMISSIONS,
|
||||
PASSENGER_PERMISSION_KEYS,
|
||||
ROLE_PERMISSION_PRESETS,
|
||||
} from './passenger-permissions.registry';
|
||||
|
||||
export type PassengerSeedRole = {
|
||||
key: string;
|
||||
name: { en: string };
|
||||
permissionKeys: string[];
|
||||
};
|
||||
|
||||
export const EDR_PASSENGER_APPLICATION = {
|
||||
id: 'd2000001-0001-4000-8000-000000000001',
|
||||
key: 'edr_passenger_app',
|
||||
name: {
|
||||
am: 'EDR Passenger App',
|
||||
en: 'EDR Passenger App',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const EDR_PASSENGER_PERMISSIONS = [...PASSENGER_PERMISSIONS];
|
||||
|
||||
export { PASSENGER_PERMISSION_KEYS } from './passenger-permissions.registry';
|
||||
|
||||
export const EDR_PASSENGER_ROLES: PassengerSeedRole[] = [
|
||||
{
|
||||
key: 'edr_passenger_backoffice_admin',
|
||||
name: { en: 'EDR Passenger Backoffice Admin' },
|
||||
permissionKeys: [...ROLE_PERMISSION_PRESETS.backofficeAdmin],
|
||||
},
|
||||
{
|
||||
key: 'edr_passenger_backoffice_staff',
|
||||
name: { en: 'EDR Passenger Backoffice Staff' },
|
||||
permissionKeys: [...ROLE_PERMISSION_PRESETS.backofficeStaff],
|
||||
},
|
||||
{
|
||||
key: 'edr_passenger_agent',
|
||||
name: { en: 'EDR Passenger Agent' },
|
||||
permissionKeys: [...ROLE_PERMISSION_PRESETS.agent],
|
||||
},
|
||||
{
|
||||
key: 'edr_passenger_finance',
|
||||
name: { en: 'EDR Passenger Finance' },
|
||||
permissionKeys: [...ROLE_PERMISSION_PRESETS.finance],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,121 @@
|
||||
const APP_KEY = 'edr_passenger_app';
|
||||
|
||||
export type PassengerPermissionSeed = {
|
||||
id: string;
|
||||
key: string;
|
||||
name: { am: string; en: string };
|
||||
applicationKey: string;
|
||||
};
|
||||
|
||||
const perm = (id: string, key: string, en: string): PassengerPermissionSeed => ({
|
||||
id,
|
||||
key,
|
||||
name: { am: en, en },
|
||||
applicationKey: APP_KEY,
|
||||
});
|
||||
|
||||
export const PASSENGER_PERMISSIONS: PassengerPermissionSeed[] = [
|
||||
perm('c1000001-0001-4000-8000-000000000001', 'edr_passenger_app:bookings:view', 'View bookings'),
|
||||
perm('c1000001-0001-4000-8000-000000000002', 'edr_passenger_app:bookings:manage', 'Manage bookings'),
|
||||
perm('c1000001-0001-4000-8000-000000000003', 'edr_passenger_app:bookings:cancel', 'Cancel bookings'),
|
||||
perm('c1000001-0001-4000-8000-000000000004', 'edr_passenger_app:passengers:view', 'View passengers'),
|
||||
perm('c1000001-0001-4000-8000-000000000005', 'edr_passenger_app:passengers:manage', 'Manage passengers'),
|
||||
perm('c1000001-0001-4000-8000-000000000006', 'edr_passenger_app:tickets:view', 'View tickets'),
|
||||
perm('c1000001-0001-4000-8000-000000000007', 'edr_passenger_app:tickets:manage', 'Manage tickets'),
|
||||
perm('c1000001-0001-4000-8000-000000000008', 'edr_passenger_app:payments:view_all', 'View all payments'),
|
||||
perm('c1000001-0001-4000-8000-000000000009', 'edr_passenger_app:payments:refund', 'Refund payments'),
|
||||
perm('c1000001-0001-4000-8000-00000000000a', 'edr_passenger_app:payments:manage_methods', 'Manage payment methods'),
|
||||
perm('c1000001-0001-4000-8000-00000000000b', 'edr_passenger_app:reports:view', 'View reports'),
|
||||
perm('c1000001-0001-4000-8000-00000000000c', 'edr_passenger_app:fraud:view', 'View fraud alerts'),
|
||||
perm('c1000001-0001-4000-8000-00000000000d', 'edr_passenger_app:fraud:manage', 'Manage fraud rules'),
|
||||
perm('c1000001-0001-4000-8000-00000000000e', 'edr_passenger_app:audit:view', 'View audit logs'),
|
||||
perm('c1000001-0001-4000-8000-00000000000f', 'edr_passenger_app:agents:view', 'View agents'),
|
||||
perm('c1000001-0001-4000-8000-000000000010', 'edr_passenger_app:agents:manage', 'Manage agents'),
|
||||
perm('c1000001-0001-4000-8000-000000000011', 'edr_passenger_app:currencies:manage', 'Manage currencies'),
|
||||
perm('c1000001-0001-4000-8000-000000000012', 'edr_passenger_app:notifications:send', 'Send notifications'),
|
||||
perm('c1000001-0001-4000-8000-000000000013', 'edr_passenger_app:dashboard:view', 'View dashboard'),
|
||||
perm('c1000001-0001-4000-8000-000000000014', 'edr_passenger_app:admin', 'Full admin access'),
|
||||
];
|
||||
|
||||
export const PASSENGER_PERMISSION_KEYS = PASSENGER_PERMISSIONS.map((p) => p.key);
|
||||
|
||||
export const PASSENGER_PERMS = {
|
||||
bookings: {
|
||||
view: 'edr_passenger_app:bookings:view',
|
||||
manage: 'edr_passenger_app:bookings:manage',
|
||||
cancel: 'edr_passenger_app:bookings:cancel',
|
||||
},
|
||||
passengers: {
|
||||
view: 'edr_passenger_app:passengers:view',
|
||||
manage: 'edr_passenger_app:passengers:manage',
|
||||
},
|
||||
tickets: {
|
||||
view: 'edr_passenger_app:tickets:view',
|
||||
manage: 'edr_passenger_app:tickets:manage',
|
||||
},
|
||||
payments: {
|
||||
viewAll: 'edr_passenger_app:payments:view_all',
|
||||
refund: 'edr_passenger_app:payments:refund',
|
||||
manageMethods: 'edr_passenger_app:payments:manage_methods',
|
||||
},
|
||||
reports: {
|
||||
view: 'edr_passenger_app:reports:view',
|
||||
},
|
||||
fraud: {
|
||||
view: 'edr_passenger_app:fraud:view',
|
||||
manage: 'edr_passenger_app:fraud:manage',
|
||||
},
|
||||
audit: {
|
||||
view: 'edr_passenger_app:audit:view',
|
||||
},
|
||||
agents: {
|
||||
view: 'edr_passenger_app:agents:view',
|
||||
manage: 'edr_passenger_app:agents:manage',
|
||||
},
|
||||
currencies: {
|
||||
manage: 'edr_passenger_app:currencies:manage',
|
||||
},
|
||||
notifications: {
|
||||
send: 'edr_passenger_app:notifications:send',
|
||||
},
|
||||
dashboard: {
|
||||
view: 'edr_passenger_app:dashboard:view',
|
||||
},
|
||||
admin: 'edr_passenger_app:admin',
|
||||
} as const;
|
||||
|
||||
export const ROLE_PERMISSION_PRESETS = {
|
||||
backofficeAdmin: [...PASSENGER_PERMISSION_KEYS],
|
||||
|
||||
backofficeStaff: [
|
||||
PASSENGER_PERMS.bookings.view,
|
||||
PASSENGER_PERMS.bookings.manage,
|
||||
PASSENGER_PERMS.bookings.cancel,
|
||||
PASSENGER_PERMS.passengers.view,
|
||||
PASSENGER_PERMS.passengers.manage,
|
||||
PASSENGER_PERMS.tickets.view,
|
||||
PASSENGER_PERMS.tickets.manage,
|
||||
PASSENGER_PERMS.payments.viewAll,
|
||||
PASSENGER_PERMS.reports.view,
|
||||
PASSENGER_PERMS.dashboard.view,
|
||||
PASSENGER_PERMS.notifications.send,
|
||||
PASSENGER_PERMS.agents.view,
|
||||
PASSENGER_PERMS.fraud.view,
|
||||
PASSENGER_PERMS.audit.view,
|
||||
],
|
||||
|
||||
agent: [
|
||||
PASSENGER_PERMS.bookings.view,
|
||||
PASSENGER_PERMS.bookings.manage,
|
||||
PASSENGER_PERMS.passengers.view,
|
||||
PASSENGER_PERMS.tickets.view,
|
||||
PASSENGER_PERMS.payments.refund,
|
||||
],
|
||||
|
||||
finance: [
|
||||
PASSENGER_PERMS.payments.viewAll,
|
||||
PASSENGER_PERMS.payments.refund,
|
||||
PASSENGER_PERMS.reports.view,
|
||||
PASSENGER_PERMS.dashboard.view,
|
||||
],
|
||||
} as const;
|
||||
106
apps/edr-passenger-api/src/seed/passenger-staff-users.seeder.ts
Normal file
106
apps/edr-passenger-api/src/seed/passenger-staff-users.seeder.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
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 {
|
||||
Employee,
|
||||
Organization,
|
||||
Role,
|
||||
User,
|
||||
UserCredential,
|
||||
UserRole,
|
||||
} from '@tria-plc/iamapi-common';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
const SEED_FLAG = 'SEED_PASSENGER_STAFF';
|
||||
const EDR_ORG_KEY = 'edr';
|
||||
|
||||
const STAFF_USERS = [
|
||||
{ email: 'passenger.admin@edr.local', username: 'passenger_admin', roleKey: 'edr_passenger_backoffice_admin' },
|
||||
{ email: 'passenger.staff@edr.local', username: 'passenger_staff', roleKey: 'edr_passenger_backoffice_staff' },
|
||||
{ email: 'passenger.agent@edr.local', username: 'passenger_agent', roleKey: 'edr_passenger_agent' },
|
||||
{ email: 'passenger.finance@edr.local', username: 'passenger_finance', roleKey: 'edr_passenger_finance' },
|
||||
] as const;
|
||||
|
||||
@Injectable()
|
||||
export class PassengerStaffUsersSeeder {
|
||||
private readonly logger = new Logger(PassengerStaffUsersSeeder.name);
|
||||
|
||||
constructor(private readonly dataSource: DataSource) {}
|
||||
|
||||
async run() {
|
||||
if (process.env[SEED_FLAG]?.trim().toLowerCase() !== 'true') {
|
||||
this.logger.log(`Skipping passenger staff seed because ${SEED_FLAG} is not enabled`);
|
||||
return;
|
||||
}
|
||||
|
||||
const password = process.env.DEFAULT_PASSWORD?.trim() || '12345678';
|
||||
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
const organization = await manager.getRepository(Organization).findOne({
|
||||
where: { key: EDR_ORG_KEY },
|
||||
select: { id: true, key: true },
|
||||
});
|
||||
|
||||
if (!organization) throw new Error(`missing_organization:${EDR_ORG_KEY}`);
|
||||
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
for (const staff of STAFF_USERS) {
|
||||
const role = await manager.getRepository(Role).findOne({
|
||||
where: { key: staff.roleKey },
|
||||
select: { id: true, key: true },
|
||||
});
|
||||
if (!role) throw new Error(`missing_role:${staff.roleKey}`);
|
||||
|
||||
let user = await manager.getRepository(User).findOne({
|
||||
where: { email: staff.email },
|
||||
select: { id: true, email: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
user = await manager.getRepository(User).save(
|
||||
manager.getRepository(User).create({
|
||||
email: staff.email,
|
||||
username: staff.username,
|
||||
name: { en: staff.username },
|
||||
isActive: true,
|
||||
hasSetPassword: true,
|
||||
status: EUserStatus.ACCEPTED,
|
||||
}),
|
||||
);
|
||||
this.logger.log(`Seeded passenger staff user ${staff.email}`);
|
||||
}
|
||||
|
||||
const credentialExists = await manager.getRepository(UserCredential).exists({
|
||||
where: { userId: user.id, isActive: true },
|
||||
});
|
||||
if (!credentialExists) {
|
||||
await manager.getRepository(UserCredential).insert({
|
||||
userId: user.id,
|
||||
password: hashedPassword,
|
||||
isActive: true,
|
||||
});
|
||||
}
|
||||
|
||||
await manager.getRepository(UserRole).upsert(
|
||||
{ userId: user.id, roleId: role.id, organizationId: organization.id },
|
||||
{ conflictPaths: { userId: true, roleId: true } },
|
||||
);
|
||||
|
||||
const employeeExists = await manager.getRepository(Employee).exists({
|
||||
where: { userId: user.id, organizationId: organization.id, isCurrent: true },
|
||||
});
|
||||
if (!employeeExists) {
|
||||
await manager.getRepository(Employee).insert({
|
||||
userId: user.id,
|
||||
organizationId: organization.id,
|
||||
isCurrent: true,
|
||||
name: { en: staff.username },
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.logger.log('Ensured passenger staff users');
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@
|
||||
"incremental": true,
|
||||
"tsBuildInfoFile": "./.tsbuildinfo",
|
||||
"paths": { "@/*": ["./src/*"] },
|
||||
"module": "node16",
|
||||
"moduleResolution": "node16",
|
||||
"strictPropertyInitialization": false,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false
|
||||
|
||||
@@ -4,9 +4,17 @@ import axios from 'axios';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000';
|
||||
|
||||
function mapIamRole(roles: { key?: string }[]): 'ADMIN' | 'AGENT' | 'SUPERVISOR' {
|
||||
const keys = roles.map((r) => r.key ?? '');
|
||||
if (keys.some((k) => k.includes('admin') || k === 'super_admin' || k === 'organization_admin')) return 'ADMIN';
|
||||
if (keys.some((k) => k.includes('agent'))) return 'AGENT';
|
||||
return 'SUPERVISOR';
|
||||
}
|
||||
|
||||
interface AuthState {
|
||||
user: AdminUser | null;
|
||||
token: string | null;
|
||||
refreshToken: string | null;
|
||||
isAuthenticated: boolean;
|
||||
login: (email: string, password: string) => Promise<void>;
|
||||
logout: () => void;
|
||||
@@ -17,6 +25,7 @@ interface AuthState {
|
||||
export const useAuthStore = create<AuthState>((set) => ({
|
||||
user: null,
|
||||
token: null,
|
||||
refreshToken: null,
|
||||
isAuthenticated: false,
|
||||
|
||||
initialize: () => {
|
||||
@@ -27,57 +36,47 @@ export const useAuthStore = create<AuthState>((set) => ({
|
||||
try {
|
||||
const user = JSON.parse(userStr);
|
||||
set({ user, token, isAuthenticated: true });
|
||||
} catch (e) {
|
||||
} catch {
|
||||
localStorage.removeItem('auth_token');
|
||||
localStorage.removeItem('auth_refresh_token');
|
||||
localStorage.removeItem('auth_user');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
login: async (email: string, password: string) => {
|
||||
try {
|
||||
console.log('Attempting login to:', `${API_URL}/auth/login`);
|
||||
const response = await axios.post(`${API_URL}/auth/login`, { email, password });
|
||||
console.log('Full response:', response.data);
|
||||
|
||||
// Backend wraps response in { success, data: { token, user }, timestamp }
|
||||
const responseData = response.data.data || response.data;
|
||||
|
||||
if (!responseData || !responseData.token || !responseData.user) {
|
||||
console.error('Invalid response structure:', response.data);
|
||||
throw new Error('Invalid response from server');
|
||||
}
|
||||
|
||||
const { token, user: apiUser } = responseData;
|
||||
|
||||
const user: AdminUser = {
|
||||
id: apiUser.id,
|
||||
email: apiUser.email,
|
||||
fullName: apiUser.fullName,
|
||||
role: apiUser.role,
|
||||
active: true,
|
||||
};
|
||||
|
||||
console.log('Login successful! User:', user);
|
||||
|
||||
localStorage.setItem('auth_token', token);
|
||||
localStorage.setItem('auth_user', JSON.stringify(user));
|
||||
|
||||
set({ user, token, isAuthenticated: true });
|
||||
} catch (error: any) {
|
||||
console.error('Login error details:', {
|
||||
message: error.message,
|
||||
response: error.response?.data,
|
||||
status: error.response?.status,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
// Step 1: IAM login — returns token + refreshToken only
|
||||
const loginRes = await axios.post(`${API_URL}/v1/auth/login`, { email, password });
|
||||
const loginData = loginRes.data?.data ?? loginRes.data;
|
||||
const { token, refreshToken } = loginData;
|
||||
if (!token) throw new Error('No token received from server');
|
||||
|
||||
// Step 2: fetch full user info with the token
|
||||
const meRes = await axios.get(`${API_URL}/v1/auth/me`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
const iamUser = meRes.data?.data ?? meRes.data;
|
||||
|
||||
const user: AdminUser = {
|
||||
id: iamUser.id,
|
||||
email: iamUser.email,
|
||||
fullName: iamUser.name?.en ?? iamUser.name?.am ?? iamUser.email,
|
||||
role: mapIamRole(iamUser.roles ?? []),
|
||||
active: true,
|
||||
};
|
||||
|
||||
localStorage.setItem('auth_token', token);
|
||||
localStorage.setItem('auth_user', JSON.stringify(user));
|
||||
if (refreshToken) localStorage.setItem('auth_refresh_token', refreshToken);
|
||||
|
||||
set({ user, token, refreshToken: refreshToken ?? null, isAuthenticated: true });
|
||||
},
|
||||
|
||||
logout: () => {
|
||||
localStorage.removeItem('auth_token');
|
||||
localStorage.removeItem('auth_refresh_token');
|
||||
localStorage.removeItem('auth_user');
|
||||
set({ user: null, token: null, isAuthenticated: false });
|
||||
set({ user: null, token: null, refreshToken: null, isAuthenticated: false });
|
||||
},
|
||||
|
||||
setUser: (user: AdminUser, token: string) => {
|
||||
|
||||
Reference in New Issue
Block a user