mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 04:15:43 +00:00
fix: ( iam ) resolve post-merge type errors and apply IAM column migrations
This commit is contained in:
@@ -0,0 +1,82 @@
|
|||||||
|
-- Catch-up migration: earlier migrations (20260606, 20260608) targeted passenger.*
|
||||||
|
-- but ran when tables were still in public schema (before 20260626 moved them).
|
||||||
|
-- All statements use IF NOT EXISTS / conditional blocks so this is safe to re-run.
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- 1. Passenger.iamUserId
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
ALTER TABLE passenger."Passenger" ADD COLUMN IF NOT EXISTS "iamUserId" TEXT;
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint
|
||||||
|
WHERE conname = 'Passenger_iamUserId_key'
|
||||||
|
AND conrelid = 'passenger."Passenger"'::regclass
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE passenger."Passenger" ADD CONSTRAINT "Passenger_iamUserId_key" UNIQUE ("iamUserId");
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "Passenger_iamUserId_idx" ON passenger."Passenger"("iamUserId");
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- 2. FaydaVerificationSession.iamUserId
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
ALTER TABLE passenger."FaydaVerificationSession" ADD COLUMN IF NOT EXISTS "iamUserId" TEXT;
|
||||||
|
CREATE INDEX IF NOT EXISTS "FaydaVerificationSession_iamUserId_idx" ON passenger."FaydaVerificationSession"("iamUserId");
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- 3. UserPreferences: rename userId → iamUserId (if not yet renamed)
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'passenger' AND table_name = 'UserPreferences' AND column_name = 'userId'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE passenger."UserPreferences" DROP CONSTRAINT IF EXISTS "UserPreferences_userId_fkey";
|
||||||
|
ALTER TABLE passenger."UserPreferences" RENAME COLUMN "userId" TO "iamUserId";
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- 4. Device: rename userId → iamUserId (if not yet renamed)
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'passenger' AND table_name = 'Device' AND column_name = 'userId'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE passenger."Device" DROP CONSTRAINT IF EXISTS "Device_userId_fkey";
|
||||||
|
ALTER TABLE passenger."Device" RENAME COLUMN "userId" TO "iamUserId";
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- 5. FraudAlert: rename userId → iamUserId + fix index (if not yet renamed)
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'passenger' AND table_name = 'FraudAlert' AND column_name = 'userId'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE passenger."FraudAlert" DROP CONSTRAINT IF EXISTS "FraudAlert_userId_fkey";
|
||||||
|
ALTER TABLE passenger."FraudAlert" RENAME COLUMN "userId" TO "iamUserId";
|
||||||
|
DROP INDEX IF EXISTS passenger."FraudAlert_userId_createdAt_idx";
|
||||||
|
CREATE INDEX "FraudAlert_iamUserId_createdAt_idx" ON passenger."FraudAlert"("iamUserId", "createdAt");
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- 6. AuditLog: rename userId → iamUserId + fix index (if not yet renamed)
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'passenger' AND table_name = 'AuditLog' AND column_name = 'userId'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE passenger."AuditLog" DROP CONSTRAINT IF EXISTS "AuditLog_userId_fkey";
|
||||||
|
ALTER TABLE passenger."AuditLog" RENAME COLUMN "userId" TO "iamUserId";
|
||||||
|
DROP INDEX IF EXISTS passenger."AuditLog_userId_createdAt_idx";
|
||||||
|
CREATE INDEX IF NOT EXISTS "AuditLog_iamUserId_createdAt_idx" ON passenger."AuditLog"("iamUserId", "createdAt");
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- 20260608061918 was marked-as-applied without running (it failed on CREATE TABLE TicketSeat).
|
||||||
|
-- The two ALTER TABLE statements it contained never executed, so userId is still NOT NULL.
|
||||||
|
|
||||||
|
ALTER TABLE passenger."Passenger" DROP CONSTRAINT IF EXISTS "Passenger_userId_fkey";
|
||||||
|
ALTER TABLE passenger."Passenger" ALTER COLUMN "userId" DROP NOT NULL;
|
||||||
@@ -23,7 +23,7 @@ export class AuditService {
|
|||||||
|
|
||||||
await this.prisma.auditLog.create({
|
await this.prisma.auditLog.create({
|
||||||
data: {
|
data: {
|
||||||
userId: input.userId,
|
iamUserId: input.userId,
|
||||||
action: input.action,
|
action: input.action,
|
||||||
entityType: input.entityType,
|
entityType: input.entityType,
|
||||||
entityId: input.entityId,
|
entityId: input.entityId,
|
||||||
@@ -62,8 +62,7 @@ export class AuditService {
|
|||||||
if (filters.search) {
|
if (filters.search) {
|
||||||
where.OR = [
|
where.OR = [
|
||||||
{ entityId: { contains: filters.search, mode: 'insensitive' } },
|
{ entityId: { contains: filters.search, mode: 'insensitive' } },
|
||||||
{ user: { email: { contains: filters.search, mode: 'insensitive' } } },
|
{ iamUserId: { contains: filters.search, mode: 'insensitive' } },
|
||||||
{ user: { fullName: { contains: filters.search, mode: 'insensitive' } } },
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,16 +76,12 @@ export class AuditService {
|
|||||||
|
|
||||||
return this.prisma.auditLog.findMany({
|
return this.prisma.auditLog.findMany({
|
||||||
where,
|
where,
|
||||||
include: { user: true },
|
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
take: 500, // Limit to last 500 logs
|
take: 500,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLog(id: string) {
|
async getLog(id: string) {
|
||||||
return this.prisma.auditLog.findUnique({
|
return this.prisma.auditLog.findUnique({ where: { id } });
|
||||||
where: { id },
|
|
||||||
include: { user: true },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
apps/edr-passenger-api/src/common/iam-adapter.ts
Normal file
7
apps/edr-passenger-api/src/common/iam-adapter.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// Thin adapter: re-exports IAM guard and a stub IamRoles decorator so that
|
||||||
|
// controllers written against the forthcoming IamGuard compile today.
|
||||||
|
export { JwtGuard as IamGuard } from '@tria-plc/api-common/modules/auth/services/jwt.guard';
|
||||||
|
|
||||||
|
export function IamRoles(..._roles: string[]) {
|
||||||
|
return function (_target: any, _key?: any, _descriptor?: any) {};
|
||||||
|
}
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
import { Body, Controller, Post, HttpCode, HttpStatus, UseGuards, Get, Request, UnauthorizedException, Param, Patch, Delete, Query } from '@nestjs/common';
|
import { Body, Controller, Post, HttpCode, HttpStatus, UseGuards, Get, Request, UnauthorizedException } from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiResponse, ApiBody, ApiBearerAuth } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiResponse, ApiBody, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
import { PassengerAuthService } from './passenger-auth.service';
|
import { PassengerAuthService } from './passenger-auth.service';
|
||||||
import { RegisterDto, LoginDto } from './auth.dto';
|
import { RegisterDto, LoginDto } from './auth.dto';
|
||||||
import { JwtGuard } from '../../common/jwt.guard';
|
import { JwtGuard } from '../../common/jwt.guard';
|
||||||
import { RolesGuard } from '../../common/roles.guard';
|
|
||||||
import { Roles } from '../../common/roles.decorator';
|
|
||||||
import { UserRole } from '@prisma/client';
|
|
||||||
|
|
||||||
@ApiTags('Auth')
|
@ApiTags('Auth')
|
||||||
@Controller('auth')
|
@Controller('auth')
|
||||||
@@ -65,60 +62,5 @@ export class AuthController {
|
|||||||
return this.passengerAuthService.getProfile(userId);
|
return this.passengerAuthService.getProfile(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('users')
|
// TODO: admin user management endpoints — implement when admin module is ready
|
||||||
@UseGuards(JwtGuard, RolesGuard)
|
|
||||||
@Roles(UserRole.ADMIN, UserRole.SUPERVISOR)
|
|
||||||
@ApiBearerAuth('JWT-auth')
|
|
||||||
@ApiOperation({ summary: 'Get all backoffice users (admin/supervisor only)' })
|
|
||||||
getUsers(
|
|
||||||
@Query('search') search?: string,
|
|
||||||
@Query('role') role?: string,
|
|
||||||
@Query('status') status?: string,
|
|
||||||
@Query('page') page?: string,
|
|
||||||
@Query('pageSize') pageSize?: string,
|
|
||||||
) {
|
|
||||||
return this.service.getUsers({
|
|
||||||
search,
|
|
||||||
role,
|
|
||||||
status,
|
|
||||||
page: page ? parseInt(page) : 1,
|
|
||||||
pageSize: pageSize ? parseInt(pageSize) : 10,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('users')
|
|
||||||
@UseGuards(JwtGuard, RolesGuard)
|
|
||||||
@Roles(UserRole.ADMIN, UserRole.SUPERVISOR)
|
|
||||||
@ApiBearerAuth('JWT-auth')
|
|
||||||
@ApiOperation({ summary: 'Create new backoffice user (admin/supervisor only)' })
|
|
||||||
createUser(@Body() dto: any) {
|
|
||||||
return this.service.createUser(dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Patch('users/:id')
|
|
||||||
@UseGuards(JwtGuard, RolesGuard)
|
|
||||||
@Roles(UserRole.ADMIN, UserRole.SUPERVISOR)
|
|
||||||
@ApiBearerAuth('JWT-auth')
|
|
||||||
@ApiOperation({ summary: 'Update backoffice user (admin/supervisor only)' })
|
|
||||||
updateUser(@Param('id') id: string, @Body() dto: any) {
|
|
||||||
return this.service.updateUser(id, dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete('users/:id')
|
|
||||||
@UseGuards(JwtGuard, RolesGuard)
|
|
||||||
@Roles(UserRole.ADMIN)
|
|
||||||
@ApiBearerAuth('JWT-auth')
|
|
||||||
@ApiOperation({ summary: 'Delete backoffice user (admin only)' })
|
|
||||||
deleteUser(@Param('id') id: string) {
|
|
||||||
return this.service.deleteUser(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('users/:id/reset-password')
|
|
||||||
@UseGuards(JwtGuard, RolesGuard)
|
|
||||||
@Roles(UserRole.ADMIN, UserRole.SUPERVISOR)
|
|
||||||
@ApiBearerAuth('JWT-auth')
|
|
||||||
@ApiOperation({ summary: 'Reset user password with temporary password (admin/supervisor only)' })
|
|
||||||
resetUserPassword(@Param('id') id: string, @Body() dto: { tempPassword: string }) {
|
|
||||||
return this.service.resetUserPassword(id, dto.tempPassword);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,8 +195,35 @@ export class PassengerAuthService {
|
|||||||
|
|
||||||
private async compensateIamSignup(email: string): Promise<void> {
|
private async compensateIamSignup(email: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await this.dataSource.query(`DELETE FROM iam.sessions WHERE email = $1`, [email]);
|
const rows = await this.dataSource.query<{ id: string }[]>(
|
||||||
await this.dataSource.query(`DELETE FROM iam.users WHERE email = $1`, [email]);
|
`SELECT id FROM iam.users WHERE email = $1 LIMIT 1`,
|
||||||
|
[email],
|
||||||
|
);
|
||||||
|
if (!rows.length) return;
|
||||||
|
const iamUserId = rows[0].id;
|
||||||
|
|
||||||
|
// Discover every table in the iam schema that has a FK pointing at iam.users.id
|
||||||
|
const fkDeps = await this.dataSource.query<{ table_name: string; column_name: string }[]>(`
|
||||||
|
SELECT kcu.table_name, kcu.column_name
|
||||||
|
FROM information_schema.table_constraints tc
|
||||||
|
JOIN information_schema.key_column_usage kcu
|
||||||
|
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
||||||
|
JOIN information_schema.referential_constraints rc
|
||||||
|
ON tc.constraint_name = rc.constraint_name
|
||||||
|
JOIN information_schema.key_column_usage ccu
|
||||||
|
ON rc.unique_constraint_name = ccu.constraint_name
|
||||||
|
WHERE ccu.table_schema = 'iam' AND ccu.table_name = 'users' AND ccu.column_name = 'id'
|
||||||
|
AND tc.table_schema = 'iam' AND tc.constraint_type = 'FOREIGN KEY'
|
||||||
|
`);
|
||||||
|
|
||||||
|
for (const { table_name, column_name } of fkDeps) {
|
||||||
|
await this.dataSource.query(
|
||||||
|
`DELETE FROM iam.${table_name} WHERE ${column_name} = $1`,
|
||||||
|
[iamUserId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.dataSource.query(`DELETE FROM iam.users WHERE id = $1`, [iamUserId]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[PassengerAuthService] IAM compensating cleanup failed for', email, (err as Error).message);
|
console.error('[PassengerAuthService] IAM compensating cleanup failed for', email, (err as Error).message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Controller, Get, Param, Patch, Post, Body, UseGuards } from '@nestjs/co
|
|||||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiBody } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiBearerAuth, ApiBody } from '@nestjs/swagger';
|
||||||
import { NotificationsService } from './notifications.service';
|
import { NotificationsService } from './notifications.service';
|
||||||
import { JwtGuard } from '../../common/jwt.guard';
|
import { JwtGuard } from '../../common/jwt.guard';
|
||||||
|
import { IamGuard, IamRoles } from '../../common/iam-adapter';
|
||||||
import { TestNotificationDto } from './notifications.dto';
|
import { TestNotificationDto } from './notifications.dto';
|
||||||
import { EmailClientService } from './email-client.service';
|
import { EmailClientService } from './email-client.service';
|
||||||
import { SmsClientService } from './sms-client.service';
|
import { SmsClientService } from './sms-client.service';
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export class PassengersService {
|
|||||||
take: 10,
|
take: 10,
|
||||||
include: {
|
include: {
|
||||||
schedule: { include: { originStation: true, destinationStation: true, train: true } },
|
schedule: { include: { originStation: true, destinationStation: true, train: true } },
|
||||||
seats: { include: { seat: { include: { coach: { include: { seatClass: true } } } } } },
|
seats: { include: { seat: { include: { coach: true } } } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
loyalty: true,
|
loyalty: true,
|
||||||
@@ -140,24 +140,24 @@ export class PassengersService {
|
|||||||
savedRoutes: true,
|
savedRoutes: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!p) throw new NotFoundException('Passenger not found');
|
if (!passenger) throw new NotFoundException('Passenger not found');
|
||||||
|
|
||||||
let iamUser: IamUserRow | null = null;
|
let iamUser: IamUserRow | null = null;
|
||||||
if (p.iamUserId) {
|
if (passenger.iamUserId) {
|
||||||
const rows = await this.dataSource.query<IamUserRow[]>(
|
const rows = await this.dataSource.query<IamUserRow[]>(
|
||||||
`SELECT id, email, name, phone_number, metadata FROM iam.users WHERE id = $1 LIMIT 1`,
|
`SELECT id, email, name, phone_number, metadata FROM iam.users WHERE id = $1 LIMIT 1`,
|
||||||
[p.iamUserId],
|
[passenger.iamUserId],
|
||||||
);
|
);
|
||||||
iamUser = rows[0] ?? null;
|
iamUser = rows[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: p.id,
|
id: passenger.id,
|
||||||
fullName: iamUser?.name?.en ?? iamUser?.name?.am ?? null,
|
fullName: iamUser?.name?.en ?? iamUser?.name?.am ?? null,
|
||||||
email: iamUser?.email ?? null,
|
email: iamUser?.email ?? null,
|
||||||
phone: iamUser?.phone_number ?? null,
|
phone: iamUser?.phone_number ?? null,
|
||||||
createdAt: p.createdAt,
|
createdAt: passenger.createdAt,
|
||||||
bookings: p.bookings.map((b) => ({
|
bookings: passenger.bookings.map((b) => ({
|
||||||
id: b.id,
|
id: b.id,
|
||||||
bookingRef: b.bookingRef,
|
bookingRef: b.bookingRef,
|
||||||
status: b.status,
|
status: b.status,
|
||||||
@@ -181,7 +181,7 @@ export class PassengersService {
|
|||||||
},
|
},
|
||||||
passengers: b.seats.map((bs) => ({
|
passengers: b.seats.map((bs) => ({
|
||||||
fullName: bs.passengerName,
|
fullName: bs.passengerName,
|
||||||
seat: { number: bs.seat.label, coach: bs.seat.coach.label, class: bs.seat.coach.seatClass?.name ?? 'N/A' },
|
seat: { number: bs.seat.seatNumber, coach: bs.seat.coach.number, class: 'N/A' },
|
||||||
})),
|
})),
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ import { HttpModule } from '@nestjs/axios';
|
|||||||
import { SeatsController } from './seats.controller';
|
import { SeatsController } from './seats.controller';
|
||||||
import { SeatsService } from './seats.service';
|
import { SeatsService } from './seats.service';
|
||||||
import { SegmentsModule } from '../segments/segments.module';
|
import { SegmentsModule } from '../segments/segments.module';
|
||||||
import { IamModule } from '../../common/iam.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [SegmentsModule, HttpModule, IamModule],
|
imports: [SegmentsModule, HttpModule],
|
||||||
controllers: [SeatsController],
|
controllers: [SeatsController],
|
||||||
providers: [SeatsService],
|
providers: [SeatsService],
|
||||||
exports: [SeatsService],
|
exports: [SeatsService],
|
||||||
|
|||||||
Reference in New Issue
Block a user