From e2ec82e69bef98468ccd3f238e056cb679c41185 Mon Sep 17 00:00:00 2001 From: Olanite Olalekan Date: Mon, 17 Mar 2025 12:44:23 +0100 Subject: [PATCH 1/2] fix: correct endpoint route from '/stimulate' to '/simulate' in TenderlyController --- src/modules/tenderly/tenderly.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/tenderly/tenderly.controller.ts b/src/modules/tenderly/tenderly.controller.ts index 2715837..b1dda2a 100644 --- a/src/modules/tenderly/tenderly.controller.ts +++ b/src/modules/tenderly/tenderly.controller.ts @@ -12,7 +12,7 @@ import { TenderlyService } from './tenderly.service'; export class TenderlyController { constructor(@Inject() private readonly tenderlyService: TenderlyService) {} - @Get('/stimulate') + @Get('/simulate') async list( @QueryParam('chainId') chainId: number, @Body({ validate: true }) payload: SimulationsRequest, From e42c5fa9cf7b2050d9ca700fe334008431c83a32 Mon Sep 17 00:00:00 2001 From: Olanite Olalekan Date: Sun, 6 Apr 2025 10:05:27 +0100 Subject: [PATCH 2/2] feat: add websocket login functionality and related DTOs --- src/middleware/auth.ts | 2 +- src/modules/auth/auth.controller.ts | 16 ++++++++++-- src/modules/auth/auth.interface.ts | 8 ++++++ src/modules/auth/auth.service.test.ts | 34 ++++++++++++++++++++++++++ src/modules/auth/auth.service.ts | 35 ++++++++++++++++++++++++++- src/modules/auth/dto/login.dto.ts | 8 +++--- src/modules/auth/dto/wslogin.dto.ts | 7 ++++++ 7 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 src/modules/auth/dto/wslogin.dto.ts diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts index 8e0ffe0..a0d3f13 100644 --- a/src/middleware/auth.ts +++ b/src/middleware/auth.ts @@ -5,7 +5,7 @@ import { ExpressMiddlewareInterface, Middleware } from 'routing-controllers'; import { Inject, Service } from 'typedi'; import { diConstants } from '@bonadocs/di'; -import { BonadocsLogger } from '@bonadocs/logger'; +import type { BonadocsLogger } from '@bonadocs/logger'; import { AuthService } from '../modules/auth/auth.service'; import { ApplicationError } from '../modules/errors/ApplicationError'; diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 6b9c68c..7257d38 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -3,21 +3,23 @@ import { Inject, Service } from 'typedi'; import { JsonResponse } from '../shared'; -import { +import type { LoginUserRequest, LoginUserResponse, RefreshTokenRequest, RefreshTokenResponse, + WSTokenResponse, } from './auth.interface'; import { AuthService } from './auth.service'; import { LoginDto } from './dto/login.dto'; +import { WSLoginDto } from './dto/wslogin.dto'; @Service() @JsonController('/auth') export class AuthController { constructor(@Inject() private readonly authService: AuthService) {} - @Post('/login') + @Post('') async login( @Body({ validate: true }) payload: LoginDto, ): Promise> { @@ -41,4 +43,14 @@ export class AuthController { message: 'Refresh successful', }; } + + @Post('/ws') + async ws(@Body({ validate: true }) payload: WSLoginDto): Promise> { + const response = await this.authService.wsLogin(payload); + return { + data: response, + status: 'successful', + message: 'Refresh successful', + }; + } } diff --git a/src/modules/auth/auth.interface.ts b/src/modules/auth/auth.interface.ts index ad6f53f..ed327ce 100644 --- a/src/modules/auth/auth.interface.ts +++ b/src/modules/auth/auth.interface.ts @@ -17,3 +17,11 @@ export interface RefreshTokenRequest { export interface RefreshTokenResponse { token: string; } + +export interface WSTokenRequest { + token: string; +} + +export interface WSTokenResponse { + token: string; +} diff --git a/src/modules/auth/auth.service.test.ts b/src/modules/auth/auth.service.test.ts index e5af343..23cb838 100644 --- a/src/modules/auth/auth.service.test.ts +++ b/src/modules/auth/auth.service.test.ts @@ -94,5 +94,39 @@ describe('AuthService', () => { }); } }); + + describe('wsLogin', () => { + it('should generate a websocket token when given valid API token', async () => { + // arrange + const token = authService.generateJWT({ + userId: '123', + authSource: 'firebase', + }); + + // act + const result = await authService.wsLogin({ + token, + }); + + // assert + expect(result).toBeDefined(); + expect(result.token).toEqual(expect.any(String)); + + const decoded = authService.validateJWT(result.token); + expect(decoded).toMatchObject({ + purpose: 'ws-api', + sub: '123', + }); + }); + + it('should throw error when API token is invalid', async () => { + // act & assert + await expect( + authService.wsLogin({ + token: 'invalid-token', + }), + ).rejects.toHaveProperty('errorCode', 'UNAUTHORIZED'); + }); + }); }); }); diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index af2123a..72af9d3 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -4,7 +4,7 @@ import { createHash } from 'crypto'; import { Inject, Service } from 'typedi'; import { diConstants } from '@bonadocs/di'; -import { BonadocsLogger } from '@bonadocs/logger'; +import type { BonadocsLogger } from '@bonadocs/logger'; import { ConfigService } from '../configuration/config.service'; import { ApplicationError, applicationErrorCodes } from '../errors/ApplicationError'; @@ -16,6 +16,8 @@ import { LoginUserResponse, RefreshTokenRequest, RefreshTokenResponse, + WSTokenRequest, + WSTokenResponse, } from './auth.interface'; import { AuthSource } from './auth.types'; import { FirebaseJWTProvider } from './firebase'; @@ -87,6 +89,30 @@ export class AuthService { }; } + async wsLogin(request: WSTokenRequest): Promise { + const isValid = this.validateJWT(request.token); + if (!isValid) { + throw new ApplicationError({ + logger: this.logger, + message: 'Api token provided not valid', + errorCode: applicationErrorCodes.unauthorized, + }); + } + const payload = JSON.parse(Buffer.from(request.token.split('.')[1], 'base64url').toString()); + const validityPeriod = this.validityFromEnv ? Number(this.validityFromEnv) : 6 * 3600; + const token = this.generateJWT( + { + purpose: 'ws-api', + sub: payload.userId, + }, + validityPeriod, + ); + + return { + token, + }; + } + getHandler(logger: BonadocsLogger, authSource: AuthSource): AuthHandler { const func = this.authSourceHandlers[authSource]; if (!func || typeof func !== 'function') { @@ -206,6 +232,13 @@ export class AuthService { } const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString()); + if (payload.purpose === 'ws-api') { + throw new ApplicationError({ + message: 'Invalid JWT - purpose ws server', + logger: this.logger, + errorCode: applicationErrorCodes.unauthorized, + }); + } if (!payload.userId) { throw new ApplicationError({ message: 'Invalid JWT - missing user ID', diff --git a/src/modules/auth/dto/login.dto.ts b/src/modules/auth/dto/login.dto.ts index 0bc44bd..d9735c4 100644 --- a/src/modules/auth/dto/login.dto.ts +++ b/src/modules/auth/dto/login.dto.ts @@ -1,12 +1,14 @@ import { IsIn, IsNotEmpty, IsNumber, IsString } from 'class-validator'; -import { AuthSource, authSources } from '../auth.types'; +import * as authTypes from '../auth.types'; export class LoginDto { @IsString() @IsNotEmpty({ message: 'Auth source not provided' }) - @IsIn(authSources, { message: `authSource must be one of: ${authSources.join(', ')}` }) - authSource: AuthSource; + @IsIn(authTypes.authSources, { + message: `authSource must be one of: ${authTypes.authSources.join(', ')}`, + }) + authSource: authTypes.AuthSource; @IsNotEmpty({ message: 'Auth data not provided' }) @IsString() diff --git a/src/modules/auth/dto/wslogin.dto.ts b/src/modules/auth/dto/wslogin.dto.ts new file mode 100644 index 0000000..a01545f --- /dev/null +++ b/src/modules/auth/dto/wslogin.dto.ts @@ -0,0 +1,7 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class WSLoginDto { + @IsNotEmpty({ message: 'Token not provided' }) + @IsString() + token: string; +}