mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 03:05:42 +00:00
refactor: ( passenger-api.auth ) remove the existing auth module
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user