refactor: ( passenger-api.auth ) remove the existing auth module

This commit is contained in:
Abubeker Yasin
2026-06-03 10:12:08 +03:00
parent 092199f536
commit 785623fcbb
9 changed files with 209 additions and 115 deletions

View File

@@ -36,8 +36,8 @@
"@nestjs/swagger": "^7.4.0",
"@nestjs/typeorm": "^11.0.1",
"@sendgrid/mail": "^8.1.0",
"@tria-plc/api-common": "^0.1.4",
"@tria-plc/iamapi-common": "^0.1.6",
"@tria-plc/api-common": "1.2.3",
"@tria-plc/iamapi-common": "^0.4.1",
"amqp-connection-manager": "^5.0.0",
"amqplib": "^2.0.1",
"axios": "^1.7.7",

View File

@@ -1,4 +1,9 @@
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import {
MiddlewareConsumer,
Module,
NestModule,
OnApplicationBootstrap,
} from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ScheduleModule } from '@nestjs/schedule';
import { EventEmitterModule } from '@nestjs/event-emitter';
@@ -6,6 +11,7 @@ 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 { PrismaModule } from './common/prisma.module';
import { I18nModule } from './common/i18n/i18n.module';
import { LocaleMiddleware } from './common/i18n/locale.middleware';
@@ -18,7 +24,6 @@ import ebirrConfig from './config/ebirr.config';
import cardConfig from './config/card.config';
import waafiConfig from './config/waafi.config';
import faydaConfig from './config/fayda.config';
import { AuthModule } from './modules/auth/auth.module';
import { StationsModule } from './modules/stations/stations.module';
import { FleetModule } from './modules/fleet/fleet.module';
import { SchedulesModule } from './modules/schedules/schedules.module';
@@ -61,22 +66,14 @@ import { VerifaydaModule } from './modules/verifayda/verifayda.module';
}),
ScheduleModule.forRoot(),
EventEmitterModule.forRoot(),
// TypeORM root DataSource for the shared `iam` schema (coexists with Prisma's `passenger`
// schema). Required so @tria-plc/api-common's JwtGuard can read `iam.sessions`, and so
// IamModule's forFeature repositories resolve. Options come from the `database` config
// namespace (see config/database.config.ts → iam-typeorm.config.ts).
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService): TypeOrmModuleOptions =>
config.get<TypeOrmModuleOptions>('iamDatabase')!,
}),
// Mount the @tria-plc IAM module (auth/user/org-structure). Registers the IAM REST API under
// /v1/* (URI versioning). NOTE: SharedAuthModule (global JwtGuard) is intentionally added later
// in the route-protection phase, so public passenger routes are not 401'd before then.
TriaIamModule.forRoot(),
PrismaModule,
I18nModule,
// AuthModule,
StationsModule,
FleetModule,
SchedulesModule,
@@ -102,8 +99,16 @@ import { VerifaydaModule } from './modules/verifayda/verifayda.module';
VerifaydaModule,
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LocaleMiddleware).forRoutes('*');
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

@@ -1,5 +1,4 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtGuard extends AuthGuard('jwt') {}
// Compatibility alias while passenger auth moves to @tria-plc IAM.
// Existing controllers can keep importing `../../common/jwt.guard`, but the
// guard now validates IAM-issued session tokens from `iam.sessions`.
export { JwtGuard } from '@tria-plc/api-common/modules/auth/services/jwt.guard';

View File

@@ -1,21 +1,30 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { InjectDataSource } from '@nestjs/typeorm';
import { JwtGuard as IamJwtGuard } from '@tria-plc/api-common/modules/auth/services/jwt.guard';
import { DataSource } from 'typeorm';
/**
* Like {@link JwtGuard}, but never rejects the request.
* Like the IAM JwtGuard, but never rejects the request.
*
* When a valid `Authorization: Bearer <jwt>` is present, `request.user` is
* populated from the JWT strategy (`{ userId, ... }`). When the token is
* missing or invalid, the request still proceeds with `request.user`
* undefined — the handler decides what to do.
*
* Used on `POST /fayda/verification/start`, which must work for both
* logged-in users (who can opt to save the verification to their account)
* and guests (anchored to a booking only).
* When a valid IAM bearer token is present, `request.user` is populated with
* the package `TCurrentUser`. Missing or invalid tokens continue as guests.
*/
@Injectable()
export class OptionalJwtGuard extends AuthGuard('jwt') {
handleRequest<TUser = unknown>(_err: unknown, user: TUser): TUser {
return (user ?? null) as TUser;
export class OptionalJwtGuard extends IamJwtGuard implements CanActivate {
constructor(
reflector: Reflector,
@InjectDataSource() dataSource: DataSource,
) {
super(reflector, dataSource);
}
async canActivate(context: ExecutionContext): Promise<boolean> {
try {
await super.canActivate(context);
} catch {
context.switchToHttp().getRequest().user = undefined;
}
return true;
}
}

View File

@@ -15,6 +15,7 @@ import {
ApiOperation,
ApiTags,
} from '@nestjs/swagger';
import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type';
import { JwtGuard } from '../../common/jwt.guard';
import { OptionalJwtGuard } from './optional-jwt.guard';
import {
@@ -25,21 +26,13 @@ import {
} from './verifayda.dto';
import { VerifaydaService } from './verifayda.service';
/** Shape the JWT strategy puts on `request.user` (see common/jwt.strategy.ts). */
interface AuthedUser {
userId: string;
email?: string;
role?: string;
passengerId?: string;
}
/** Minimal slices of the Express req we touch (avoids a hard dependency on
* `@types/express`, which isn't resolved in this package). */
interface RequestWithOptionalUser {
user?: AuthedUser;
user?: TCurrentUser;
}
interface RequestWithUser {
user: AuthedUser;
user: TCurrentUser;
}
@ApiTags('Fayda Verification')
@@ -75,7 +68,7 @@ export class VerifaydaController {
const authorizationUrl = await this.service.startVerification({
purpose: dto.purpose ?? 'PURCHASE',
platform: dto.platform ?? 'WEB',
userId: req.user?.userId,
userId: req.user?.id,
bookingId: dto.bookingId,
saveToAccount: dto.saveToAccount,
});
@@ -106,6 +99,6 @@ export class VerifaydaController {
async status(
@Req() req: RequestWithUser,
): Promise<VerificationStatusDto> {
return this.service.getVerificationStatus(req.user.userId);
return this.service.getVerificationStatus(req.user.id);
}
}

View File

@@ -2,12 +2,9 @@ import { Module } from '@nestjs/common';
import { VerifaydaController } from './verifayda.controller';
import { VerifaydaService } from './verifayda.service';
import { PrismaModule } from '../../common/prisma.module';
import { AuthModule } from '../auth/auth.module';
@Module({
// AuthModule re-exports JwtModule, giving us JwtService (same secret/expiry
// config as /auth/login) to mint tokens for the LOGIN flow.
imports: [PrismaModule, AuthModule],
imports: [PrismaModule],
controllers: [VerifaydaController],
providers: [VerifaydaService],
exports: [VerifaydaService],

View File

@@ -1,5 +1,4 @@
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { exportJWK, generateKeyPair, type JWK } from 'jose';
import { PrismaService } from '../../common/prisma.service';
import { FaydaConfig } from '../../config/fayda.config';
@@ -30,12 +29,6 @@ function buildPrismaMock() {
};
}
function buildJwtMock(): jest.Mocked<JwtService> {
return {
sign: jest.fn(() => 'signed.jwt.token'),
} as unknown as jest.Mocked<JwtService>;
}
function buildConfig(overrides?: Partial<FaydaConfig>): FaydaConfig {
return {
enabled: true,
@@ -65,7 +58,6 @@ function buildConfigService(faydaConfig: FaydaConfig): jest.Mocked<ConfigService
describe('VerifaydaService (OIDC, client-callback)', () => {
let prisma: ReturnType<typeof buildPrismaMock>;
let jwt: jest.Mocked<JwtService>;
let service: VerifaydaService;
let realPrivateJwk: JWK;
@@ -77,12 +69,10 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
beforeEach(() => {
prisma = buildPrismaMock();
jwt = buildJwtMock();
const cfg = buildConfig({ privateJwk: realPrivateJwk as FaydaConfig['privateJwk'] });
service = new VerifaydaService(
buildConfigService(cfg),
prisma as unknown as PrismaService,
jwt,
);
(global as any).fetch = jest.fn();
});
@@ -136,7 +126,6 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
const disabledService = new VerifaydaService(
buildConfigService(buildConfig({ enabled: false })),
prisma as unknown as PrismaService,
jwt,
);
await expect(
disabledService.startVerification({ purpose: 'PURCHASE' }),
@@ -433,7 +422,7 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
prisma.faydaVerificationSession.findUnique.mockResolvedValue(loginSession());
});
it('creates a new user when no match and returns { token, user }', async () => {
it('creates a new user but rejects legacy local token issuance', async () => {
const fullUser = {
id: 'new-user',
email: 'new@example.com',
@@ -452,17 +441,12 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
mockLoginFetch({ sub: 'login-sub-1', name: 'New Person', email: 'new@example.com' });
const result = await service.completeVerification({
code: 'c',
state: 'state-login',
});
expect(result).toMatchObject({
purpose: 'LOGIN',
verified: true,
token: 'signed.jwt.token',
user: { id: 'new-user', passengerId: 'p-new' },
});
await expect(
service.completeVerification({
code: 'c',
state: 'state-login',
}),
).rejects.toMatchObject({ status: 401 });
expect(prisma.user.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
@@ -473,12 +457,9 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
}),
);
expect(prisma.passenger.create).toHaveBeenCalled();
expect(jwt.sign).toHaveBeenCalledWith(
expect.objectContaining({ sub: 'new-user', passengerId: 'p-new' }),
);
});
it('logs in an existing user already linked by faydaSub', async () => {
it('resolves an existing linked user but rejects legacy local token issuance', async () => {
const fullUser = {
id: 'known-user',
email: 'k@example.com',
@@ -491,16 +472,16 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
mockLoginFetch({ sub: 'login-sub-2', name: 'Known' });
const result = await service.completeVerification({
code: 'c',
state: 'state-login',
});
expect(result.user?.id).toBe('known-user');
await expect(
service.completeVerification({
code: 'c',
state: 'state-login',
}),
).rejects.toMatchObject({ status: 401 });
expect(prisma.user.create).not.toHaveBeenCalled();
});
it('links Fayda to an existing account matched by email', async () => {
it('links Fayda to an existing account matched by email but rejects legacy local token issuance', async () => {
const fullUser = {
id: 'acc-1',
email: 'match@example.com',
@@ -515,12 +496,12 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
mockLoginFetch({ sub: 'login-sub-3', email: 'match@example.com' });
const result = await service.completeVerification({
code: 'c',
state: 'state-login',
});
expect(result.user?.id).toBe('acc-1');
await expect(
service.completeVerification({
code: 'c',
state: 'state-login',
}),
).rejects.toMatchObject({ status: 401 });
expect(prisma.user.update).toHaveBeenCalledWith(
expect.objectContaining({
where: { id: 'acc-1' },

View File

@@ -6,7 +6,6 @@ import {
UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import axios, { AxiosInstance } from 'axios';
import * as bcrypt from 'bcrypt';
import { randomBytes } from 'crypto';
@@ -88,7 +87,6 @@ export class VerifaydaService {
constructor(
private readonly config: ConfigService,
private readonly prisma: PrismaService,
private readonly jwt: JwtService,
) {
const fayda = this.config.get<FaydaConfig>('fayda');
if (!fayda) {
@@ -274,16 +272,15 @@ export class VerifaydaService {
passengerId: user.passenger?.id,
agentId: user.agent?.id,
};
const token = this.jwt.sign({
sub: summary.id,
email: summary.email,
role: summary.role,
passengerId: summary.passengerId,
agentId: summary.agentId,
});
this.logger.log(`Fayda login issued token for user ${user.id}`);
return { token, user: summary };
this.logger.warn(
`Legacy passenger Fayda login reached for user ${user.id}; use IAM /v1/auth Fayda login to issue tokens.`,
);
throw new UnauthorizedException({
code: 'FAYDA_LOGIN_MIGRATED_TO_IAM',
message: 'Fayda login tokens are issued by the IAM package auth endpoints.',
user: summary,
});
}
async getVerificationStatus(userId: string): Promise<VerificationStatusDto> {