From 12bd7e6779becf417d8294bd29bb518e26d955e9 Mon Sep 17 00:00:00 2001 From: Behzad-rabiei Date: Mon, 6 Jan 2025 14:40:45 +0100 Subject: [PATCH 1/6] feat: add jwt module --- src/jwt/config/jwt.config.ts | 20 ++++++++ src/jwt/jwt.module.ts | 13 +++++ src/jwt/jwt.service.spec.ts | 19 +++++++ src/jwt/jwt.service.ts | 83 +++++++++++++++++++++++++++++++ src/jwt/types/jwt-payload.type.ts | 17 +++++++ 5 files changed, 152 insertions(+) create mode 100644 src/jwt/config/jwt.config.ts create mode 100644 src/jwt/jwt.module.ts create mode 100644 src/jwt/jwt.service.spec.ts create mode 100644 src/jwt/jwt.service.ts create mode 100644 src/jwt/types/jwt-payload.type.ts diff --git a/src/jwt/config/jwt.config.ts b/src/jwt/config/jwt.config.ts new file mode 100644 index 0000000..93d3338 --- /dev/null +++ b/src/jwt/config/jwt.config.ts @@ -0,0 +1,20 @@ +import * as Joi from 'joi'; + +import { registerAs } from '@nestjs/config'; + +export default registerAs('jwt', () => ({ + secret: process.env.JWT_SECRET, + authExpiresIn: parseInt(process.env.JWT_AUTH_EXPIRATION_MINUTES, 10) || 60, + discourseVerificationExpiresIn: + parseInt(process.env.JWT_VERIFICATION_MINUTES, 10) || 10, +})); + +export const jwtConfigSchema = { + JWT_SECRET: Joi.string().required().description('JWT secret'), + JWT_AUTH_EXPIRATION_MINUTES: Joi.number() + .default(60) + .description('JWT expiration time in minutes for the auth'), + JWT_VERIFICATION_MINUTES: Joi.number() + .default(10) + .description('JWT expiration time in minutes for the verification'), +}; diff --git a/src/jwt/jwt.module.ts b/src/jwt/jwt.module.ts new file mode 100644 index 0000000..2234cae --- /dev/null +++ b/src/jwt/jwt.module.ts @@ -0,0 +1,13 @@ +// src/jwt/jwt.module.ts + +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; + +import { JwtService } from './jwt.service'; + +@Module({ + imports: [ConfigModule], + providers: [JwtService], + exports: [JwtService], +}) +export class JwtModule {} diff --git a/src/jwt/jwt.service.spec.ts b/src/jwt/jwt.service.spec.ts new file mode 100644 index 0000000..96d2719 --- /dev/null +++ b/src/jwt/jwt.service.spec.ts @@ -0,0 +1,19 @@ +import { Test, TestingModule } from '@nestjs/testing'; + +import { JwtService } from './jwt.service'; + +describe('JwtService', () => { + let service: JwtService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [JwtService], + }).compile(); + + service = module.get(JwtService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/jwt/jwt.service.ts b/src/jwt/jwt.service.ts new file mode 100644 index 0000000..0104391 --- /dev/null +++ b/src/jwt/jwt.service.ts @@ -0,0 +1,83 @@ +// src/jwt/jwt.service.ts + +import * as jwt from 'jsonwebtoken'; +import * as moment from 'moment'; +import { InjectPinoLogger, PinoLogger } from 'nestjs-pino'; + +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +import { + AuthJwtPayload, + JwtPayload, + VerificationJwtPayload, +} from './types/jwt-payload.type'; + +@Injectable() +export class JwtService { + constructor( + private readonly configService: ConfigService, + @InjectPinoLogger(JwtService.name) + private readonly logger: PinoLogger + ) {} + + async signPayload(payload: JwtPayload): Promise { + return jwt.sign(payload, this.configService.get('jwt.secret'), { + algorithm: 'HS256', + }); + } + + async validateToken(token: string): Promise { + try { + return jwt.verify( + token, + this.configService.get('jwt.secret'), + { + algorithms: ['HS256'], + } + ) as JwtPayload; + } catch (error) { + this.logger.error(error, 'Failed to validate token'); + throw new UnauthorizedException(error.message); + } + } + + async generateAuthJwt( + identifier: string, + provider: string, + metadata?: Record + ): Promise { + const now = moment().unix(); + const exp = moment() + .add(this.configService.get('jwt.authExpiresIn'), 'minutes') + .unix(); + + const payload: AuthJwtPayload = { + sub: identifier, + provider, + iat: now, + exp, + iss: this.configService.get('wallet.publicKey'), + metadata, + }; + + return this.signPayload(payload); + } + + async generateVerificationToken(verificationCode: string): Promise { + const now = moment().unix(); + const exp = moment() + .add(this.configService.get('jwt.authExpiresIn'), 'minutes') + .unix(); + + const payload: VerificationJwtPayload = { + sub: 'Verification', + code: verificationCode, + iat: now, + exp, + iss: this.configService.get('wallet.publicKey'), + }; + + return this.signPayload(payload); + } +} diff --git a/src/jwt/types/jwt-payload.type.ts b/src/jwt/types/jwt-payload.type.ts new file mode 100644 index 0000000..7e6b9fb --- /dev/null +++ b/src/jwt/types/jwt-payload.type.ts @@ -0,0 +1,17 @@ +export interface BaseJwtPayload { + sub: string; + iat?: number; + exp?: number; + iss?: string; +} + +export interface VerificationJwtPayload extends BaseJwtPayload { + code: string; +} + +export interface AuthJwtPayload extends BaseJwtPayload { + provider?: string; + metadata?: Record; +} + +export type JwtPayload = AuthJwtPayload | VerificationJwtPayload; From dcfd0b8708f19511eb9495455d01ee2e84bce7db Mon Sep 17 00:00:00 2001 From: Behzad-rabiei Date: Mon, 6 Jan 2025 14:41:14 +0100 Subject: [PATCH 2/6] refactor: adjust code to use jwt module --- .prettierrc | 12 +- src/app.module.ts | 24 +-- .../auth-discord-controller.spec.ts | 70 +++---- src/auth-discord/auth-discord.controller.ts | 37 ++-- src/auth-discord/auth-discord.module.ts | 9 +- .../config/auth-discord.config.ts | 8 +- .../dto/handle-oauth-callback-dto.ts | 8 +- .../auth-discourse.controller.ts | 0 src/auth-discourse/auth-discourse.module.ts | 0 src/auth-discourse/auth-discourse.service.ts | 0 .../auth-google.controller.spec.ts | 70 +++---- src/auth-google/auth-google.controller.ts | 38 ++-- src/auth-google/auth-google.module.ts | 8 +- src/auth-google/config/google.config.ts | 8 +- .../dto/handle-oauth-callback-dto.ts | 8 +- src/auth-siwe/auth-siwe.controller.spec.ts | 74 ++++---- src/auth-siwe/auth-siwe.controller.ts | 48 ++--- src/auth-siwe/auth-siwe.module.ts | 14 +- src/auth-siwe/dto/nonce.dto.ts | 4 +- src/auth-siwe/dto/verify-siwe.dto.ts | 14 +- src/auth-siwe/siwe.service.spec.ts | 66 +++---- src/auth-siwe/siwe.service.ts | 24 +-- src/auth/auth.module.ts | 21 ++- src/auth/auth.service.spec.ts | 88 --------- src/auth/auth.service.ts | 51 ----- src/auth/config/auth.config.ts | 8 +- src/auth/constants/oAuth.constants.ts | 2 +- src/auth/constants/provider.constants.ts | 2 +- src/auth/dto/jwt-response.dto.ts | 4 +- src/auth/oAuth.service.spec.ts | 86 ++++----- src/auth/oAuth.service.ts | 85 +++++---- src/auth/types/auth-provider.type.ts | 4 +- src/auth/types/jwt-payload.type.ts | 13 +- src/config/app.config.ts | 8 +- src/config/index.ts | 29 +-- src/config/logger.config.ts | 8 +- src/config/pino.config.ts | 12 +- src/config/wallet.config.ts | 8 +- src/doc/index.ts | 14 +- src/doc/swagger.constants.ts | 8 +- src/eas/constants/attestation.constants.ts | 14 +- src/eas/constants/sepolia.constants.ts | 8 +- src/eas/dto/decrypt-attestation-secret.dto.ts | 12 +- src/eas/dto/sign-delegated-attestation.dto.ts | 14 +- src/eas/dto/sign-delegated-revocation.dto.ts | 12 +- src/eas/eas.controller.ts | 20 +- src/eas/eas.module.ts | 16 +- src/eas/eas.service.spec.ts | 43 +++-- src/eas/eas.service.ts | 175 +++++++++--------- src/eas/interfaces/attestation.interfaces.ts | 36 ++-- src/lit/config/lit.config.ts | 10 +- src/lit/constants/network.constants.ts | 4 +- src/lit/lit.module.ts | 6 +- src/lit/lit.service.ts | 90 ++++----- src/main.ts | 54 +++--- src/shared/constants/chain.constants.ts | 8 +- .../decorators/jwt-provider.decorator.ts | 18 +- src/shared/filters/http-exception.filter.ts | 20 +- src/shared/types/chain.type.ts | 8 +- src/utils/crypto-utils.service.spec.ts | 54 +++--- src/utils/crypto-utils.service.ts | 16 +- src/utils/data-utils.service.spec.ts | 108 +++++------ src/utils/data-utils.service.ts | 22 +-- src/utils/encode-utils.service.spec.ts | 28 +-- src/utils/encode-utils.service.ts | 4 +- src/utils/ethers.utils.service.spec.ts | 42 ++--- src/utils/ethers.utils.service.ts | 8 +- src/utils/utils.module.ts | 12 +- src/utils/viem.service.spec.ts | 54 +++--- src/utils/viem.utils.service.ts | 28 +-- 70 files changed, 921 insertions(+), 1018 deletions(-) delete mode 100644 src/auth-discourse/auth-discourse.controller.ts delete mode 100644 src/auth-discourse/auth-discourse.module.ts delete mode 100644 src/auth-discourse/auth-discourse.service.ts delete mode 100644 src/auth/auth.service.spec.ts delete mode 100644 src/auth/auth.service.ts diff --git a/.prettierrc b/.prettierrc index fcef829..0a3b3e7 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,7 +1,7 @@ { - "trailingComma": "es5", - "tabWidth": 4, - "semi": false, - "singleQuote": true, - "printWidth": 80 -} \ No newline at end of file + "trailingComma": "es5", + "tabWidth": 4, + "semi": true, + "singleQuote": true, + "printWidth": 80 +} diff --git a/src/app.module.ts b/src/app.module.ts index 5b0efa5..c93a067 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,14 +1,15 @@ -import { Module } from '@nestjs/common' -import { ConfigModule, ConfigService } from '@nestjs/config' -import { LoggerModule } from 'nestjs-pino' -import { AuthModule } from './auth/auth.module' -import { AuthDiscordModule } from './auth-discord/auth-discord.module' -import { AuthGoogleModule } from './auth-google/auth-google.module' -import { AuthSiweModule } from './auth-siwe/auth-siwe.module' -import { configModules, configValidationSchema } from './config' -import { pinoConfig } from './config/pino.config' -import { LitModule } from './lit/lit.module' -import { EasModule } from './eas/eas.module' +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { LoggerModule } from 'nestjs-pino'; +import { AuthModule } from './auth/auth.module'; +import { AuthDiscordModule } from './auth-discord/auth-discord.module'; +import { AuthGoogleModule } from './auth-google/auth-google.module'; +import { AuthSiweModule } from './auth-siwe/auth-siwe.module'; +import { configModules, configValidationSchema } from './config'; +import { pinoConfig } from './config/pino.config'; +import { LitModule } from './lit/lit.module'; +import { EasModule } from './eas/eas.module'; +import { JwtModule } from './jwt/jwt.module'; @Module({ imports: [ @@ -28,6 +29,7 @@ import { EasModule } from './eas/eas.module' AuthSiweModule, LitModule, EasModule, + JwtModule, ], controllers: [], providers: [], diff --git a/src/auth-discord/auth-discord-controller.spec.ts b/src/auth-discord/auth-discord-controller.spec.ts index 48a9adf..8fa5a90 100644 --- a/src/auth-discord/auth-discord-controller.spec.ts +++ b/src/auth-discord/auth-discord-controller.spec.ts @@ -1,21 +1,21 @@ -import { Test, TestingModule } from '@nestjs/testing' -import { AuthDiscordController } from './auth-discord.controller' -import { OAuthService } from '../auth/oAuth.service' -import { CryptoUtilsService } from '../utils/crypto-utils.service' -import { HttpStatus, ForbiddenException } from '@nestjs/common' -import { AUTH_PROVIDERS } from '../auth/constants/provider.constants' +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthDiscordController } from './auth-discord.controller'; +import { OAuthService } from '../auth/oAuth.service'; +import { CryptoUtilsService } from '../utils/crypto-utils.service'; +import { HttpStatus, ForbiddenException } from '@nestjs/common'; +import { AUTH_PROVIDERS } from '../auth/constants/provider.constants'; describe('AuthDiscordController', () => { - let controller: AuthDiscordController + let controller: AuthDiscordController; const mockCryptoService = { generateState: jest.fn(), validateState: jest.fn(), - } + }; const mockOAuthService = { generateRedirectUrl: jest.fn(), handleOAuth2Callback: jest.fn(), - } + }; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [AuthDiscordController], @@ -23,62 +23,62 @@ describe('AuthDiscordController', () => { { provide: CryptoUtilsService, useValue: mockCryptoService }, { provide: OAuthService, useValue: mockOAuthService }, ], - }).compile() + }).compile(); - controller = module.get(AuthDiscordController) - }) + controller = module.get(AuthDiscordController); + }); describe('redirectToDiscord', () => { it('should redirect to Discord authentication URL', () => { - mockCryptoService.generateState.mockReturnValue('mock-state') - mockOAuthService.generateRedirectUrl.mockReturnValue('mock-url') - const mockSession = { state: null } - const result = controller.redirectToDiscord(mockSession) + mockCryptoService.generateState.mockReturnValue('mock-state'); + mockOAuthService.generateRedirectUrl.mockReturnValue('mock-url'); + const mockSession = { state: null }; + const result = controller.redirectToDiscord(mockSession); expect(result).toEqual({ url: 'mock-url', statusCode: HttpStatus.FOUND, - }) - expect(mockSession.state).toEqual('mock-state') - expect(mockCryptoService.generateState).toHaveBeenCalled() + }); + expect(mockSession.state).toEqual('mock-state'); + expect(mockCryptoService.generateState).toHaveBeenCalled(); expect(mockOAuthService.generateRedirectUrl).toHaveBeenCalledWith( AUTH_PROVIDERS.DISCORD, 'mock-state' - ) - }) - }) + ); + }); + }); describe('handleOAuthCallback', () => { it('should handle OAuth callback successfully', async () => { mockOAuthService.handleOAuth2Callback.mockResolvedValue( 'mock-redirect-url' - ) - const mockSession = { state: 'mock-state' } + ); + const mockSession = { state: 'mock-state' }; const result = await controller.handleOAuthCallback( { code: 'valid-code', state: 'mock-state' }, mockSession - ) + ); expect(result).toEqual({ url: 'mock-redirect-url', statusCode: HttpStatus.FOUND, - }) + }); expect(mockOAuthService.handleOAuth2Callback).toHaveBeenCalledWith( 'mock-state', 'mock-state', 'valid-code', AUTH_PROVIDERS.DISCORD - ) - }) + ); + }); it('should throw HttpException if state is invalid', async () => { - const mockSession = { state: 'invalid' } + const mockSession = { state: 'invalid' }; mockOAuthService.handleOAuth2Callback.mockImplementation(() => { - throw new ForbiddenException('Invalid state') - }) + throw new ForbiddenException('Invalid state'); + }); await expect( controller.handleOAuthCallback( { code: 'valid-code', state: 'mock-state' }, mockSession ) - ).rejects.toThrow(ForbiddenException) - }) - }) -}) + ).rejects.toThrow(ForbiddenException); + }); + }); +}); diff --git a/src/auth-discord/auth-discord.controller.ts b/src/auth-discord/auth-discord.controller.ts index 090cfba..cb44c74 100644 --- a/src/auth-discord/auth-discord.controller.ts +++ b/src/auth-discord/auth-discord.controller.ts @@ -1,25 +1,26 @@ import { Controller, Get, + HttpStatus, Query, Redirect, - HttpStatus, Session, -} from '@nestjs/common' +} from '@nestjs/common'; import { - ApiTags, - ApiOperation, ApiFoundResponse, ApiOkResponse, -} from '@nestjs/swagger' -import { OAuthService } from '../auth/oAuth.service' -import { HandleOAuthCallback } from './dto/handle-oauth-callback-dto' -import { CryptoUtilsService } from '../utils/crypto-utils.service' -import { AUTH_PROVIDERS } from '../auth/constants/provider.constants' -import { JwtResponse } from '../auth//dto/jwt-response.dto' + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; + +import { AUTH_PROVIDERS } from '../auth/constants/provider.constants'; +import { JwtResponse } from '../auth/dto/jwt-response.dto'; +import { OAuthService } from '../auth/oAuth.service'; +import { CryptoUtilsService } from '../utils/crypto-utils.service'; +import { HandleOAuthCallback } from './dto/handle-oauth-callback-dto'; -@ApiTags(`${AUTH_PROVIDERS.DISCORD} Authentication`) -@Controller(`auth/${AUTH_PROVIDERS.DISCORD}`) +@ApiTags('Discord Authentication') +@Controller('auth/discord') export class AuthDiscordController { constructor( private readonly oAuthService: OAuthService, @@ -31,13 +32,13 @@ export class AuthDiscordController { @ApiOperation({ summary: 'Redirect to Discord OAuth' }) @ApiFoundResponse({ description: 'Redirection to Discord OAuth.' }) redirectToDiscord(@Session() session: any) { - const state = this.cryptoService.generateState() + const state = this.cryptoService.generateState(); const url = this.oAuthService.generateRedirectUrl( AUTH_PROVIDERS.DISCORD, state - ) - session.state = state - return { url, statusCode: HttpStatus.FOUND } + ); + session.state = state; + return { url, statusCode: HttpStatus.FOUND }; } @Get('authenticate/callback') @@ -56,10 +57,10 @@ export class AuthDiscordController { session.state, code, AUTH_PROVIDERS.DISCORD - ) + ); return { url: redirectUrl, statusCode: HttpStatus.FOUND, - } + }; } } diff --git a/src/auth-discord/auth-discord.module.ts b/src/auth-discord/auth-discord.module.ts index fff59e2..6bb7cf5 100644 --- a/src/auth-discord/auth-discord.module.ts +++ b/src/auth-discord/auth-discord.module.ts @@ -1,7 +1,8 @@ -import { Module } from '@nestjs/common' -import { AuthDiscordController } from './auth-discord.controller' -import { AuthModule } from 'src/auth/auth.module' -import { UtilsModule } from '../utils/utils.module' +import { Module } from '@nestjs/common'; + +import { AuthModule } from '../auth/auth.module'; +import { UtilsModule } from '../utils/utils.module'; +import { AuthDiscordController } from './auth-discord.controller'; @Module({ imports: [AuthModule, UtilsModule], diff --git a/src/auth-discord/config/auth-discord.config.ts b/src/auth-discord/config/auth-discord.config.ts index a304699..0534439 100644 --- a/src/auth-discord/config/auth-discord.config.ts +++ b/src/auth-discord/config/auth-discord.config.ts @@ -1,6 +1,6 @@ // config/discord.config.ts -import * as Joi from 'joi' -import { registerAs } from '@nestjs/config' +import * as Joi from 'joi'; +import { registerAs } from '@nestjs/config'; export default registerAs('discord', () => ({ clientId: process.env.DISCORD_CLIENT_ID, @@ -9,7 +9,7 @@ export default registerAs('discord', () => ({ scopes: process.env.DISCORD_SCOPES ? process.env.DISCORD_SCOPES.split(' ') : ['identify'], -})) +})); export const discordConfigSchema = { DISCORD_CLIENT_ID: Joi.string().required().description('Discord client ID'), @@ -23,4 +23,4 @@ export const discordConfigSchema = { DISCORD_SCOPES: Joi.string() .default('identify') .description('Discord OAuth scopes'), -} +}; diff --git a/src/auth-discord/dto/handle-oauth-callback-dto.ts b/src/auth-discord/dto/handle-oauth-callback-dto.ts index 140813b..440fb2c 100644 --- a/src/auth-discord/dto/handle-oauth-callback-dto.ts +++ b/src/auth-discord/dto/handle-oauth-callback-dto.ts @@ -1,11 +1,11 @@ -import { ApiProperty } from '@nestjs/swagger' -import { IsString } from 'class-validator' +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; export class HandleOAuthCallback { @ApiProperty() @IsString() - readonly code: string + readonly code: string; @ApiProperty() @IsString() - readonly state: string + readonly state: string; } diff --git a/src/auth-discourse/auth-discourse.controller.ts b/src/auth-discourse/auth-discourse.controller.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/auth-discourse/auth-discourse.module.ts b/src/auth-discourse/auth-discourse.module.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/auth-discourse/auth-discourse.service.ts b/src/auth-discourse/auth-discourse.service.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/auth-google/auth-google.controller.spec.ts b/src/auth-google/auth-google.controller.spec.ts index 0ebcd9e..ce784ad 100644 --- a/src/auth-google/auth-google.controller.spec.ts +++ b/src/auth-google/auth-google.controller.spec.ts @@ -1,20 +1,20 @@ -import { Test, TestingModule } from '@nestjs/testing' -import { AuthGoogleController } from './auth-google.controller' -import { OAuthService } from '../auth/oAuth.service' -import { CryptoUtilsService } from '../utils/crypto-utils.service' -import { HttpStatus, ForbiddenException } from '@nestjs/common' -import { AUTH_PROVIDERS } from '../auth/constants/provider.constants' +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthGoogleController } from './auth-google.controller'; +import { OAuthService } from '../auth/oAuth.service'; +import { CryptoUtilsService } from '../utils/crypto-utils.service'; +import { HttpStatus, ForbiddenException } from '@nestjs/common'; +import { AUTH_PROVIDERS } from '../auth/constants/provider.constants'; describe('AuthGoogleController', () => { - let controller: AuthGoogleController + let controller: AuthGoogleController; const mockCryptoService = { generateState: jest.fn(), validateState: jest.fn(), - } + }; const mockOAuthService = { generateRedirectUrl: jest.fn(), handleOAuth2Callback: jest.fn(), - } + }; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -23,63 +23,63 @@ describe('AuthGoogleController', () => { { provide: CryptoUtilsService, useValue: mockCryptoService }, { provide: OAuthService, useValue: mockOAuthService }, ], - }).compile() + }).compile(); - controller = module.get(AuthGoogleController) - }) + controller = module.get(AuthGoogleController); + }); describe('redirectToGoogle', () => { it('should redirect to Google authentication URL', () => { - mockCryptoService.generateState.mockReturnValue('mock-state') - mockOAuthService.generateRedirectUrl.mockReturnValue('mock-url') - const mockSession = { state: null } - const result = controller.redirectToGoogle(mockSession) + mockCryptoService.generateState.mockReturnValue('mock-state'); + mockOAuthService.generateRedirectUrl.mockReturnValue('mock-url'); + const mockSession = { state: null }; + const result = controller.redirectToGoogle(mockSession); expect(result).toEqual({ url: 'mock-url', statusCode: HttpStatus.FOUND, - }) - expect(mockSession.state).toEqual('mock-state') - expect(mockCryptoService.generateState).toHaveBeenCalled() + }); + expect(mockSession.state).toEqual('mock-state'); + expect(mockCryptoService.generateState).toHaveBeenCalled(); expect(mockOAuthService.generateRedirectUrl).toHaveBeenCalledWith( AUTH_PROVIDERS.GOOGLE, 'mock-state' - ) - }) - }) + ); + }); + }); describe('handleOAuthCallback', () => { it('should handle OAuth callback successfully', async () => { mockOAuthService.handleOAuth2Callback.mockResolvedValue( 'mock-redirect-url' - ) - const mockSession = { state: 'mock-state' } + ); + const mockSession = { state: 'mock-state' }; const result = await controller.handleOAuthCallback( { code: 'valid-code', state: 'mock-state' }, mockSession - ) + ); expect(result).toEqual({ url: 'mock-redirect-url', statusCode: HttpStatus.FOUND, - }) + }); expect(mockOAuthService.handleOAuth2Callback).toHaveBeenCalledWith( 'mock-state', 'mock-state', 'valid-code', AUTH_PROVIDERS.GOOGLE - ) - }) + ); + }); it('should throw HttpException if state is invalid', async () => { - const mockSession = { state: 'invalid' } + const mockSession = { state: 'invalid' }; mockOAuthService.handleOAuth2Callback.mockImplementation(() => { - throw new ForbiddenException('Invalid state') - }) + throw new ForbiddenException('Invalid state'); + }); await expect( controller.handleOAuthCallback( { code: 'valid-code', state: 'mock-state' }, mockSession ) - ).rejects.toThrow(ForbiddenException) - }) - }) -}) + ).rejects.toThrow(ForbiddenException); + }); + }); +}); diff --git a/src/auth-google/auth-google.controller.ts b/src/auth-google/auth-google.controller.ts index f6fd1f8..3150058 100644 --- a/src/auth-google/auth-google.controller.ts +++ b/src/auth-google/auth-google.controller.ts @@ -1,24 +1,26 @@ import { Controller, Get, + HttpStatus, Query, Redirect, - HttpStatus, Session, -} from '@nestjs/common' +} from '@nestjs/common'; import { - ApiTags, - ApiOperation, ApiFoundResponse, ApiOkResponse, -} from '@nestjs/swagger' -import { HandleOAuthCallback } from './dto/handle-oauth-callback-dto' -import { JwtResponse } from '../auth/dto/jwt-response.dto' -import { AUTH_PROVIDERS } from '../auth/constants/provider.constants' -import { CryptoUtilsService } from '../utils/crypto-utils.service' -import { OAuthService } from '../auth/oAuth.service' -@ApiTags(`${AUTH_PROVIDERS.GOOGLE} Authentication`) -@Controller(`auth/${AUTH_PROVIDERS.GOOGLE}`) + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; + +import { AUTH_PROVIDERS } from '../auth/constants/provider.constants'; +import { JwtResponse } from '../auth/dto/jwt-response.dto'; +import { OAuthService } from '../auth/oAuth.service'; +import { CryptoUtilsService } from '../utils/crypto-utils.service'; +import { HandleOAuthCallback } from './dto/handle-oauth-callback-dto'; + +@ApiTags('Google Authentication') +@Controller('auth/google') export class AuthGoogleController { constructor( private readonly oAuthService: OAuthService, @@ -30,13 +32,13 @@ export class AuthGoogleController { @ApiOperation({ summary: 'Redirect to Google OAuth' }) @ApiFoundResponse({ description: 'Redirection to Google OAuth.' }) redirectToGoogle(@Session() session: any) { - const state = this.cryptoService.generateState() + const state = this.cryptoService.generateState(); const url = this.oAuthService.generateRedirectUrl( AUTH_PROVIDERS.GOOGLE, state - ) - session.state = state - return { url, statusCode: HttpStatus.FOUND } + ); + session.state = state; + return { url, statusCode: HttpStatus.FOUND }; } @Get('authenticate/callback') @@ -55,11 +57,11 @@ export class AuthGoogleController { session.state, code, AUTH_PROVIDERS.GOOGLE - ) + ); return { url: redirectUrl, statusCode: HttpStatus.FOUND, - } + }; } } diff --git a/src/auth-google/auth-google.module.ts b/src/auth-google/auth-google.module.ts index e7e89ce..af7cc13 100644 --- a/src/auth-google/auth-google.module.ts +++ b/src/auth-google/auth-google.module.ts @@ -1,7 +1,7 @@ -import { Module } from '@nestjs/common' -import { AuthGoogleController } from './auth-google.controller' -import { AuthModule } from '../auth/auth.module' -import { UtilsModule } from '../utils/utils.module' +import { Module } from '@nestjs/common'; +import { AuthGoogleController } from './auth-google.controller'; +import { AuthModule } from '../auth/auth.module'; +import { UtilsModule } from '../utils/utils.module'; @Module({ imports: [AuthModule, UtilsModule], diff --git a/src/auth-google/config/google.config.ts b/src/auth-google/config/google.config.ts index dad0209..6c36183 100644 --- a/src/auth-google/config/google.config.ts +++ b/src/auth-google/config/google.config.ts @@ -1,5 +1,5 @@ -import { registerAs } from '@nestjs/config' -import * as Joi from 'joi' +import { registerAs } from '@nestjs/config'; +import * as Joi from 'joi'; export default registerAs('google', () => ({ clientId: process.env.GOOGLE_CLIENT_ID, @@ -8,7 +8,7 @@ export default registerAs('google', () => ({ scopes: process.env.GOOGLE_SCOPES ? process.env.GOOGLE_SCOPES.split(' ') : ['identify'], -})) +})); export const googleConfigSchema = { GOOGLE_CLIENT_ID: Joi.string().required().description('Google clinet id'), @@ -22,4 +22,4 @@ export const googleConfigSchema = { GOOGLE_SCOPES: Joi.string() .default('profile') .description('Google OAuth scopes'), -} +}; diff --git a/src/auth-google/dto/handle-oauth-callback-dto.ts b/src/auth-google/dto/handle-oauth-callback-dto.ts index 140813b..440fb2c 100644 --- a/src/auth-google/dto/handle-oauth-callback-dto.ts +++ b/src/auth-google/dto/handle-oauth-callback-dto.ts @@ -1,11 +1,11 @@ -import { ApiProperty } from '@nestjs/swagger' -import { IsString } from 'class-validator' +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; export class HandleOAuthCallback { @ApiProperty() @IsString() - readonly code: string + readonly code: string; @ApiProperty() @IsString() - readonly state: string + readonly state: string; } diff --git a/src/auth-siwe/auth-siwe.controller.spec.ts b/src/auth-siwe/auth-siwe.controller.spec.ts index 85acb92..a1f012b 100644 --- a/src/auth-siwe/auth-siwe.controller.spec.ts +++ b/src/auth-siwe/auth-siwe.controller.spec.ts @@ -1,19 +1,21 @@ -import { Test, TestingModule } from '@nestjs/testing' -import { AuthSiweController } from './auth-siwe.controller' -import { SiweService } from './siwe.service' -import { AuthService } from '../auth/auth.service' -import { VerifySiweDto } from './dto/verify-siwe.dto' -import { AUTH_PROVIDERS } from '../auth/constants/provider.constants' -import { parseSiweMessage } from 'viem/siwe' +import { parseSiweMessage } from 'viem/siwe'; + +import { Test, TestingModule } from '@nestjs/testing'; + +import { AUTH_PROVIDERS } from '../auth/constants/provider.constants'; +import { AuthService } from '../auth/jwt.service'; +import { AuthSiweController } from './auth-siwe.controller'; +import { VerifySiweDto } from './dto/verify-siwe.dto'; +import { SiweService } from './siwe.service'; jest.mock('viem/siwe', () => ({ parseSiweMessage: jest.fn(), -})) +})); describe('AuthSiweController', () => { - let controller: AuthSiweController - let siweService: SiweService - let authService: AuthService + let controller: AuthSiweController; + let siweService: SiweService; + let authService: AuthService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -33,25 +35,25 @@ describe('AuthSiweController', () => { }, }, ], - }).compile() + }).compile(); - controller = module.get(AuthSiweController) - siweService = module.get(SiweService) - authService = module.get(AuthService) - }) + controller = module.get(AuthSiweController); + siweService = module.get(SiweService); + authService = module.get(AuthService); + }); it('should be defined', () => { - expect(controller).toBeDefined() - }) + expect(controller).toBeDefined(); + }); describe('getNonce', () => { it('should return a nonce', () => { - const nonce = 'nonce' - jest.spyOn(siweService, 'getNonce').mockReturnValue(nonce) + const nonce = 'nonce'; + jest.spyOn(siweService, 'getNonce').mockReturnValue(nonce); - expect(controller.getNonce()).toEqual({ nonce }) - }) - }) + expect(controller.getNonce()).toEqual({ nonce }); + }); + }); describe('verifySiwe', () => { it('should verify SIWE message and return a JWT', async () => { @@ -59,28 +61,28 @@ describe('AuthSiweController', () => { message: '0xmessage', signature: '0xsignature', chainId: 1, - } - const jwt = 'jwt' - const address = '0xaddress' + }; + const jwt = 'jwt'; + const address = '0xaddress'; jest.spyOn(siweService, 'verifySiweMessage').mockResolvedValue( undefined - ) - jest.spyOn(authService, 'generateJwt').mockResolvedValue(jwt) - ;(parseSiweMessage as jest.Mock).mockReturnValue({ address }) + ); + jest.spyOn(authService, 'generateJwt').mockResolvedValue(jwt); + (parseSiweMessage as jest.Mock).mockReturnValue({ address }); - const result = await controller.verifySiwe(verifySiweDto) + const result = await controller.verifySiwe(verifySiweDto); - expect(result).toEqual({ jwt }) + expect(result).toEqual({ jwt }); expect(siweService.verifySiweMessage).toHaveBeenCalledWith( verifySiweDto.message, verifySiweDto.signature, verifySiweDto.chainId - ) + ); expect(authService.generateJwt).toHaveBeenCalledWith( address, AUTH_PROVIDERS.SIWE - ) - }) - }) -}) + ); + }); + }); +}); diff --git a/src/auth-siwe/auth-siwe.controller.ts b/src/auth-siwe/auth-siwe.controller.ts index c264095..eb48574 100644 --- a/src/auth-siwe/auth-siwe.controller.ts +++ b/src/auth-siwe/auth-siwe.controller.ts @@ -1,31 +1,33 @@ +import { parseSiweMessage } from 'viem/siwe'; + import { + Body, Controller, Get, - Post, - Body, - HttpStatus, HttpCode, -} from '@nestjs/common' -import { SiweService } from './siwe.service' + HttpStatus, + Post, +} from '@nestjs/common'; import { - ApiTags, - ApiOperation, ApiBadRequestResponse, ApiOkResponse, -} from '@nestjs/swagger' -import { AuthService } from '../auth/auth.service' -import { VerifySiweDto } from './dto/verify-siwe.dto' -import { AUTH_PROVIDERS } from '../auth/constants/provider.constants' -import { JwtResponse } from '../auth//dto/jwt-response.dto' -import { parseSiweMessage } from 'viem/siwe' -import { NonceResponse } from './dto/nonce.dto' + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; + +import { AUTH_PROVIDERS } from '../auth/constants/provider.constants'; +import { JwtResponse } from '../auth/dto/jwt-response.dto'; +import { JwtService } from '../jwt/jwt.service'; +import { NonceResponse } from './dto/nonce.dto'; +import { VerifySiweDto } from './dto/verify-siwe.dto'; +import { SiweService } from './siwe.service'; -@ApiTags(`${AUTH_PROVIDERS.SIWE} Authentication`) -@Controller(`auth/${AUTH_PROVIDERS.SIWE}`) +@ApiTags('Siwe Authentication') +@Controller(`auth/siwe`) export class AuthSiweController { constructor( private readonly siweService: SiweService, - private readonly authService: AuthService + private readonly jwtService: JwtService ) {} @Get('nonce') @@ -35,7 +37,7 @@ export class AuthSiweController { type: NonceResponse, }) getNonce() { - return { nonce: this.siweService.getNonce() } + return { nonce: this.siweService.getNonce() }; } @Post('verify') @@ -47,12 +49,12 @@ export class AuthSiweController { @ApiBadRequestResponse({ description: 'SIWE verification failed.' }) @HttpCode(HttpStatus.OK) async verifySiwe(@Body() verifySiweDto: VerifySiweDto) { - const { message, signature, chainId } = verifySiweDto - await this.siweService.verifySiweMessage(message, signature, chainId) - const jwt = await this.authService.generateJwt( + const { message, signature, chainId } = verifySiweDto; + await this.siweService.verifySiweMessage(message, signature, chainId); + const jwt = await this.jwtService.generateAuthJwt( parseSiweMessage(message).address, AUTH_PROVIDERS.SIWE - ) - return { jwt } + ); + return { jwt }; } } diff --git a/src/auth-siwe/auth-siwe.module.ts b/src/auth-siwe/auth-siwe.module.ts index 1935a79..39e47b7 100644 --- a/src/auth-siwe/auth-siwe.module.ts +++ b/src/auth-siwe/auth-siwe.module.ts @@ -1,11 +1,13 @@ -import { Module } from '@nestjs/common' -import { AuthModule } from '../auth/auth.module' -import { AuthSiweController } from './auth-siwe.controller' -import { SiweService } from './siwe.service' -import { UtilsModule } from '../utils/utils.module' +import { Module } from '@nestjs/common'; + +import { AuthModule } from '../auth/auth.module'; +import { JwtModule } from '../jwt/jwt.module'; +import { UtilsModule } from '../utils/utils.module'; +import { AuthSiweController } from './auth-siwe.controller'; +import { SiweService } from './siwe.service'; @Module({ - imports: [AuthModule, UtilsModule], + imports: [AuthModule, UtilsModule, JwtModule], controllers: [AuthSiweController], providers: [SiweService], }) diff --git a/src/auth-siwe/dto/nonce.dto.ts b/src/auth-siwe/dto/nonce.dto.ts index a5afb40..3a99c70 100644 --- a/src/auth-siwe/dto/nonce.dto.ts +++ b/src/auth-siwe/dto/nonce.dto.ts @@ -1,6 +1,6 @@ -import { ApiProperty } from '@nestjs/swagger' +import { ApiProperty } from '@nestjs/swagger'; export class NonceResponse { @ApiProperty({ description: 'Nonce generated successfully.' }) - nonce: string + nonce: string; } diff --git a/src/auth-siwe/dto/verify-siwe.dto.ts b/src/auth-siwe/dto/verify-siwe.dto.ts index 4dae029..609a5dd 100644 --- a/src/auth-siwe/dto/verify-siwe.dto.ts +++ b/src/auth-siwe/dto/verify-siwe.dto.ts @@ -1,7 +1,7 @@ -import { ApiProperty } from '@nestjs/swagger' -import { IsString, IsNotEmpty, IsNumber, IsIn } from 'class-validator' -import { SignMessageReturnType, Hex } from 'viem' -import { SUPPORTED_CHAINS } from '../../shared/constants/chain.constants' +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsNotEmpty, IsNumber, IsIn } from 'class-validator'; +import { SignMessageReturnType, Hex } from 'viem'; +import { SUPPORTED_CHAINS } from '../../shared/constants/chain.constants'; export class VerifySiweDto { @ApiProperty({ @@ -10,7 +10,7 @@ export class VerifySiweDto { }) @IsString() @IsNotEmpty() - readonly message: SignMessageReturnType + readonly message: SignMessageReturnType; @ApiProperty({ description: 'Signature of the SIWE message.', @@ -18,7 +18,7 @@ export class VerifySiweDto { }) @IsString() @IsNotEmpty() - readonly signature: Hex + readonly signature: Hex; @ApiProperty({ description: 'Chain Id', @@ -29,5 +29,5 @@ export class VerifySiweDto { @IsNumber() @IsNotEmpty() @IsIn(SUPPORTED_CHAINS) - readonly chainId: number + readonly chainId: number; } diff --git a/src/auth-siwe/siwe.service.spec.ts b/src/auth-siwe/siwe.service.spec.ts index f9e10d5..f901509 100644 --- a/src/auth-siwe/siwe.service.spec.ts +++ b/src/auth-siwe/siwe.service.spec.ts @@ -1,18 +1,18 @@ -import { Test, TestingModule } from '@nestjs/testing' -import { SiweService } from './siwe.service' -import { ViemUtilsService } from '../utils/viem.utils.service' -import { PinoLogger, LoggerModule } from 'nestjs-pino' -import { HttpException } from '@nestjs/common' +import { Test, TestingModule } from '@nestjs/testing'; +import { SiweService } from './siwe.service'; +import { ViemUtilsService } from '../utils/viem.utils.service'; +import { PinoLogger, LoggerModule } from 'nestjs-pino'; +import { HttpException } from '@nestjs/common'; describe('SiweService', () => { - let service: SiweService - let publicClientMock: { verifySiweMessage: jest.Mock } - let loggerMock: PinoLogger + let service: SiweService; + let publicClientMock: { verifySiweMessage: jest.Mock }; + let loggerMock: PinoLogger; beforeAll(async () => { publicClientMock = { verifySiweMessage: jest.fn(), - } + }; const module: TestingModule = await Test.createTestingModule({ imports: [LoggerModule.forRoot()], @@ -31,47 +31,47 @@ describe('SiweService', () => { useValue: loggerMock, }, ], - }).compile() + }).compile(); - service = module.get(SiweService) - }) + service = module.get(SiweService); + }); it('should be defined', () => { - expect(service).toBeDefined() - }) + expect(service).toBeDefined(); + }); describe('createNonce', () => { it('should generate a nonce', () => { - const nonce = 'nonce' - jest.spyOn(service, 'getNonce').mockReturnValue(nonce) + const nonce = 'nonce'; + jest.spyOn(service, 'getNonce').mockReturnValue(nonce); - expect(service.getNonce()).toBe(nonce) - }) - }) + expect(service.getNonce()).toBe(nonce); + }); + }); describe('verifySiweMessage', () => { it('should verify a valid SIWE message', async () => { - const message = 'valid message' - const signature = '0xvalidsignature' - const chainId = 1 + const message = 'valid message'; + const signature = '0xvalidsignature'; + const chainId = 1; - publicClientMock.verifySiweMessage.mockResolvedValue(true) + publicClientMock.verifySiweMessage.mockResolvedValue(true); await expect( service.verifySiweMessage(message, signature, chainId) - ).resolves.not.toThrow() - }) + ).resolves.not.toThrow(); + }); it('should throw an error for an invalid SIWE message', async () => { - const message = 'invalid message' - const signature = '0xinvalidsignature' - const chainId = 1 + const message = 'invalid message'; + const signature = '0xinvalidsignature'; + const chainId = 1; - publicClientMock.verifySiweMessage.mockResolvedValue(false) + publicClientMock.verifySiweMessage.mockResolvedValue(false); await expect( service.verifySiweMessage(message, signature, chainId) - ).rejects.toThrow(HttpException) - }) - }) -}) + ).rejects.toThrow(HttpException); + }); + }); +}); diff --git a/src/auth-siwe/siwe.service.ts b/src/auth-siwe/siwe.service.ts index e826094..1eb8d05 100644 --- a/src/auth-siwe/siwe.service.ts +++ b/src/auth-siwe/siwe.service.ts @@ -1,9 +1,9 @@ -import { Injectable, HttpException, HttpStatus } from '@nestjs/common' -import { PinoLogger, InjectPinoLogger } from 'nestjs-pino' -import { AUTH_PROVIDERS } from '../auth/constants/provider.constants' -import { generateSiweNonce } from 'viem/siwe' -import { ViemUtilsService } from '../utils/viem.utils.service' -import { Hex } from 'viem' +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { PinoLogger, InjectPinoLogger } from 'nestjs-pino'; +import { AUTH_PROVIDERS } from '../auth/constants/provider.constants'; +import { generateSiweNonce } from 'viem/siwe'; +import { ViemUtilsService } from '../utils/viem.utils.service'; +import { Hex } from 'viem'; @Injectable() export class SiweService { @@ -14,25 +14,25 @@ export class SiweService { ) {} getNonce(): string { - return generateSiweNonce() + return generateSiweNonce(); } async verifySiweMessage(message: string, signature: Hex, chainId: number) { try { - const publicClient = this.viemUtilsService.getPublicClient(chainId) + const publicClient = this.viemUtilsService.getPublicClient(chainId); const isValid = await publicClient.verifySiweMessage({ message, signature, - }) + }); if (!isValid) { - throw new Error() + throw new Error(); } } catch (error) { - this.logger.error(error, `Siwe Verification Failed`) + this.logger.error(error, `Siwe Verification Failed`); throw new HttpException( `${AUTH_PROVIDERS.SIWE} verification Failed`, HttpStatus.BAD_REQUEST - ) + ); } } } diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 00fb1a7..ee9e343 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -1,10 +1,12 @@ -import { Module } from '@nestjs/common' -import { JwtModule } from '@nestjs/jwt' -import { AuthService } from './auth.service' -import { OAuthService } from './oAuth.service' -import { HttpModule } from '@nestjs/axios' -import { ConfigService } from '@nestjs/config' -import { UtilsModule } from 'src/utils/utils.module' +import { UtilsModule } from 'src/utils/utils.module'; + +import { HttpModule } from '@nestjs/axios'; +import { Module } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { JwtModule } from '@nestjs/jwt'; + +import { JwtModule as customJwtModule } from '../jwt/jwt.module'; +import { OAuthService } from './oAuth.service'; @Module({ imports: [ @@ -16,8 +18,9 @@ import { UtilsModule } from 'src/utils/utils.module' }), HttpModule, UtilsModule, + customJwtModule, ], - providers: [AuthService, OAuthService], - exports: [AuthService, OAuthService], + providers: [OAuthService], + exports: [OAuthService], }) export class AuthModule {} diff --git a/src/auth/auth.service.spec.ts b/src/auth/auth.service.spec.ts deleted file mode 100644 index 264f08f..0000000 --- a/src/auth/auth.service.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -// test/auth/auth.service.spec.ts -import { Test, TestingModule } from '@nestjs/testing' -import { JwtService } from '@nestjs/jwt' -import { AuthService } from './auth.service' -import { ConfigService } from '@nestjs/config' -import * as moment from 'moment' -import { JwtPayload } from './types/jwt-payload.type' -import * as jwt from 'jsonwebtoken' -import { PinoLogger, LoggerModule } from 'nestjs-pino' -import { UnauthorizedException } from '@nestjs/common' - -const mockPublicKey = - '0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef' -const mockJwtSecret = 'jwtSecret' -const mockJwtExpiresIn = '60' - -const mockConfigService = { - get: jest.fn((key: string) => { - if (key === 'wallet.publicKey') return mockPublicKey - if (key === 'jwt.secret') return mockJwtSecret - if (key === 'jwt.expiresIn') return mockJwtExpiresIn - }), -} - -describe('AuthService', () => { - let service: AuthService - let loggerMock: PinoLogger - - beforeAll(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports: [LoggerModule.forRoot()], - providers: [ - AuthService, - JwtService, - { provide: ConfigService, useValue: mockConfigService }, - { provide: ConfigService, useValue: mockConfigService }, - { provide: PinoLogger, useValue: loggerMock }, - ], - }).compile() - - service = module.get(AuthService) - }) - - it('should be defined', () => { - expect(service).toBeDefined() - }) - - describe('signPayload', () => { - it('should return a valid JWT', async () => { - const payload: JwtPayload = { - sub: '1', - provider: 'google', - iat: moment().unix(), - exp: moment().add(mockJwtExpiresIn, 'minutes').unix(), - iss: mockPublicKey, - } - const token = await service.signPayload(payload) - expect(typeof token).toBe('string') - const decoded = jwt.verify(token, mockJwtSecret, { - algorithms: ['HS256'], - }) - expect(decoded).toMatchObject(payload) - }) - }) - - describe('validateToken', () => { - it('should validate a token correctly', async () => { - const payload: JwtPayload = { - sub: '1', - provider: 'google', - iat: moment().unix(), - exp: moment().add(mockJwtExpiresIn, 'minutes').unix(), - iss: mockPublicKey, - } - const token = jwt.sign(payload, mockJwtSecret, { - algorithm: 'HS256', - }) - const decoded = await service.validateToken(token) - expect(decoded).toMatchObject(payload) - }) - - it('should return null if token is invalid', async () => { - await expect( - service.validateToken('invalid.token.here') - ).rejects.toThrow(UnauthorizedException) - }) - }) -}) diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts deleted file mode 100644 index 2a2d1c1..0000000 --- a/src/auth/auth.service.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Injectable, UnauthorizedException } from '@nestjs/common' -import * as jwt from 'jsonwebtoken' -import { JwtPayload } from './types/jwt-payload.type' -import { ConfigService } from '@nestjs/config' -import { PinoLogger, InjectPinoLogger } from 'nestjs-pino' -import * as moment from 'moment' - -@Injectable() -export class AuthService { - constructor( - private readonly configService: ConfigService, - @InjectPinoLogger(AuthService.name) - private readonly logger: PinoLogger - ) {} - - async signPayload(payload: JwtPayload): Promise { - return jwt.sign(payload, this.configService.get('jwt.secret'), { - algorithm: 'HS256', - }) - } - - async validateToken(token: string): Promise { - try { - return jwt.verify( - token, - this.configService.get('jwt.secret'), - { - algorithms: ['HS256'], - } - ) as JwtPayload - } catch (error) { - this.logger.error(error, `Failed to validtae token`) - throw new UnauthorizedException(error.message) - } - } - - async generateJwt(identifier: string, provider: string): Promise { - const now = moment().unix() - const expiration = moment() - .add(this.configService.get('jwt.expiresIn'), 'minutes') - .unix() - const payload: JwtPayload = { - sub: identifier, - provider, - iat: now, - exp: expiration, - iss: this.configService.get('wallet.publicKey'), - } - return this.signPayload(payload) - } -} diff --git a/src/auth/config/auth.config.ts b/src/auth/config/auth.config.ts index 0162396..bfb440b 100644 --- a/src/auth/config/auth.config.ts +++ b/src/auth/config/auth.config.ts @@ -1,14 +1,14 @@ -import * as Joi from 'joi' -import { registerAs } from '@nestjs/config' +import * as Joi from 'joi'; +import { registerAs } from '@nestjs/config'; export default registerAs('jwt', () => ({ secret: process.env.JWT_SECRET, expiresIn: parseInt(process.env.JWT_EXPIRATION_MINUTES, 10) || 60, -})) +})); export const authConfigSchema = { JWT_SECRET: Joi.string().required().description('JWT secret'), JWT_EXPIRATION_MINUTES: Joi.number() .default(60) .description('JWT expiration time in minutes'), -} +}; diff --git a/src/auth/constants/oAuth.constants.ts b/src/auth/constants/oAuth.constants.ts index 66f8d88..af84f45 100644 --- a/src/auth/constants/oAuth.constants.ts +++ b/src/auth/constants/oAuth.constants.ts @@ -9,4 +9,4 @@ export const OAUTH_URLS = { tokenUrl: 'https://discord.com/api/oauth2/token', userInfoUrl: 'https://discord.com/api/users/@me', }, -} +}; diff --git a/src/auth/constants/provider.constants.ts b/src/auth/constants/provider.constants.ts index f842803..7c67383 100644 --- a/src/auth/constants/provider.constants.ts +++ b/src/auth/constants/provider.constants.ts @@ -2,4 +2,4 @@ export const AUTH_PROVIDERS = { DISCORD: 'discord', GOOGLE: 'google', SIWE: 'siwe', -} +}; diff --git a/src/auth/dto/jwt-response.dto.ts b/src/auth/dto/jwt-response.dto.ts index f6ba4e2..afe9b62 100644 --- a/src/auth/dto/jwt-response.dto.ts +++ b/src/auth/dto/jwt-response.dto.ts @@ -1,6 +1,6 @@ -import { ApiProperty } from '@nestjs/swagger' +import { ApiProperty } from '@nestjs/swagger'; export class JwtResponse { @ApiProperty({ description: 'The JSON Web Token' }) - jwt: string + jwt: string; } diff --git a/src/auth/oAuth.service.spec.ts b/src/auth/oAuth.service.spec.ts index 859ac56..a805d8e 100644 --- a/src/auth/oAuth.service.spec.ts +++ b/src/auth/oAuth.service.spec.ts @@ -1,17 +1,19 @@ -import { Test, TestingModule } from '@nestjs/testing' -import { OAuthService } from './oAuth.service' -import { HttpService } from '@nestjs/axios' -import { ConfigService } from '@nestjs/config' -import { of } from 'rxjs' -import { AxiosResponse } from 'axios' -import { CryptoUtilsService } from '../utils/crypto-utils.service' -import { AuthService } from './auth.service' -import { AUTH_PROVIDERS } from './constants/provider.constants' -import { PinoLogger, LoggerModule } from 'nestjs-pino' +import { AxiosResponse } from 'axios'; +import { LoggerModule, PinoLogger } from 'nestjs-pino'; +import { of } from 'rxjs'; + +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; + +import { CryptoUtilsService } from '../utils/crypto-utils.service'; +import { AUTH_PROVIDERS } from './constants/provider.constants'; +import { AuthService } from './jwt.service'; +import { OAuthService } from './oAuth.service'; describe('OAuthService', () => { - let service: OAuthService - let loggerMock: PinoLogger + let service: OAuthService; + let loggerMock: PinoLogger; const mockHttpService = { post: jest.fn().mockImplementation(() => @@ -32,26 +34,26 @@ describe('OAuthService', () => { config: {}, } as AxiosResponse<{ id: string }>) ), - } - const mockFrontEndURL = 'http://localhost:3000' + }; + const mockFrontEndURL = 'http://localhost:3000'; const mockConfigService = { get: jest.fn((key: string) => { if (key.startsWith('google.')) { - return 'test-value' + return 'test-value'; } - if (key === 'app.frontEndURL') return mockFrontEndURL - return null + if (key === 'app.frontEndURL') return mockFrontEndURL; + return null; }), - } + }; const mockAuthService = { generateJwt: jest.fn().mockResolvedValue('mock-jwt'), - } + }; const mockCryptoService = { validateState: jest.fn().mockReturnValue(true), - } + }; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -64,72 +66,72 @@ describe('OAuthService', () => { { provide: CryptoUtilsService, useValue: mockCryptoService }, { provide: PinoLogger, useValue: loggerMock }, ], - }).compile() + }).compile(); - service = module.get(OAuthService) - }) + service = module.get(OAuthService); + }); it('should be defined', () => { - expect(service).toBeDefined() - }) + expect(service).toBeDefined(); + }); describe('OAuth token exchange', () => { it('should exchange code for token successfully', async () => { const result = await service.exchangeCodeForToken( AUTH_PROVIDERS.GOOGLE, 'mock-code' - ) - expect(result).toEqual({ access_token: 'mock-access-token' }) - }) + ); + expect(result).toEqual({ access_token: 'mock-access-token' }); + }); // it('should handle errors during token exchange', async () => { // jest.spyOn(httpService, 'post').mockReturnValue(throwError(() => new Error('Request failed'))); // await expect(service.exchangeCodeForToken('google', 'mock-code')).rejects.toThrow({}); // }); - }) + }); describe('Retrieve user information', () => { it('should retrieve user information successfully', async () => { const result = await service.getUserInfo( AUTH_PROVIDERS.GOOGLE, 'mock-access-token' - ) - expect(result).toEqual({ id: 'mock-id' }) - }) + ); + expect(result).toEqual({ id: 'mock-id' }); + }); // it('should handle errors during user information retrieval', async () => { // jest.spyOn(httpService, 'get').mockReturnValue(throwError(() => new Error('Request failed'))); // await expect(service.getUserInfo('google', 'mock-access-token')).rejects.toThrow('Failed to retrieve user information from google'); // }); - }) + }); describe('Handle OAuth2 callback', () => { it('should handle OAuth2 callback successfully', async () => { jest.spyOn(service, 'exchangeCodeForToken').mockResolvedValue({ access_token: 'valid-token', - }) + }); jest.spyOn(service, 'getUserInfo').mockResolvedValue({ id: 'user-id', - }) + }); const result = await service.handleOAuth2Callback( 'mock-state', 'mock-state', 'valid-code', AUTH_PROVIDERS.GOOGLE - ) + ); expect(result).toEqual( 'http://localhost:3000/callback?jwt=mock-jwt' - ) + ); expect(mockCryptoService.validateState).toHaveBeenCalledWith( 'mock-state', 'mock-state' - ) + ); expect(mockAuthService.generateJwt).toHaveBeenCalledWith( 'user-id', AUTH_PROVIDERS.GOOGLE - ) - }) + ); + }); // it('should handle errors during OAuth2 callback processing', async () => { // jest.spyOn(service, 'exchangeCodeForToken').mockRejectedValue(new Error('Token exchange failed')); // await expect(service.handleOAuth2Callback('google', 'invalid-code')).rejects.toThrow('Failed to handle google OAuth2 callback'); // }); - }) -}) + }); +}); diff --git a/src/auth/oAuth.service.ts b/src/auth/oAuth.service.ts index f7cd14f..0b8fd25 100644 --- a/src/auth/oAuth.service.ts +++ b/src/auth/oAuth.service.ts @@ -1,39 +1,41 @@ +import { InjectPinoLogger, PinoLogger } from 'nestjs-pino'; +import * as querystring from 'querystring'; +import { lastValueFrom } from 'rxjs'; + +import { HttpService } from '@nestjs/axios'; import { - Injectable, BadRequestException, ForbiddenException, -} from '@nestjs/common' -import { PinoLogger, InjectPinoLogger } from 'nestjs-pino' -import { HttpService } from '@nestjs/axios' -import { lastValueFrom } from 'rxjs' -import { ConfigService } from '@nestjs/config' -import * as querystring from 'querystring' -import { OAUTH_URLS } from './constants/oAuth.constants' -import { AuthService } from '../auth/auth.service' -import { CryptoUtilsService } from '../utils/crypto-utils.service' -import { AuthProvider } from './types/auth-provider.type' + Injectable, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +import { JwtService } from '../jwt/jwt.service'; +import { CryptoUtilsService } from '../utils/crypto-utils.service'; +import { OAUTH_URLS } from './constants/oAuth.constants'; +import { AuthProvider } from './types/auth-provider.type'; @Injectable() export class OAuthService { constructor( private readonly httpService: HttpService, private readonly configService: ConfigService, - private readonly authService: AuthService, + private readonly jwtService: JwtService, private readonly cryptoService: CryptoUtilsService, @InjectPinoLogger(OAuthService.name) private readonly logger: PinoLogger ) {} getTokenUrl(provider: AuthProvider): string { - return OAUTH_URLS[provider].tokenUrl + return OAUTH_URLS[provider].tokenUrl; } getUserInfoUrl(provider: AuthProvider): string { - return OAUTH_URLS[provider].userInfoUrl + return OAUTH_URLS[provider].userInfoUrl; } getOAuthBaseURL(provider: AuthProvider): string { - return OAUTH_URLS[provider].authUrl + return OAUTH_URLS[provider].authUrl; } generateRedirectUrl(provider: AuthProvider, state: string): string { @@ -45,23 +47,23 @@ export class OAuthService { response_type: 'code', scope: this.configService.get(`${provider}.scopes`), state, - } - const baseUrl = this.getOAuthBaseURL(provider) - const queryParams = new URLSearchParams(params).toString() - return `${baseUrl}?${queryParams}` + }; + const baseUrl = this.getOAuthBaseURL(provider); + const queryParams = new URLSearchParams(params).toString(); + return `${baseUrl}?${queryParams}`; } async exchangeCodeForToken( provider: AuthProvider, code: string ): Promise<{ access_token: string }> { - const clientId = this.configService.get(`${provider}.clientId`) + const clientId = this.configService.get(`${provider}.clientId`); const clientSecret = this.configService.get( `${provider}.clientSecret` - ) + ); const redirectUri = this.configService.get( `${provider}.redirectUri` - ) + ); const params = new URLSearchParams({ client_id: clientId, @@ -69,9 +71,9 @@ export class OAuthService { grant_type: 'authorization_code', code, redirect_uri: redirectUri, - }).toString() + }).toString(); - const tokenUrl = this.getTokenUrl(provider) + const tokenUrl = this.getTokenUrl(provider); try { const response = await lastValueFrom( this.httpService.post(tokenUrl, params, { @@ -79,16 +81,16 @@ export class OAuthService { 'Content-Type': 'application/x-www-form-urlencoded', }, }) - ) - return response.data + ); + return response.data; } catch (error) { this.logger.error( error, `Failed to exchange ${provider} code for token` - ) + ); throw new BadRequestException( `Failed to exchange ${provider} code for token` - ) + ); } } @@ -96,23 +98,23 @@ export class OAuthService { provider: AuthProvider, accessToken: string ): Promise { - const userInfoUrl = this.getUserInfoUrl(provider) + const userInfoUrl = this.getUserInfoUrl(provider); try { const response = await lastValueFrom( this.httpService.get(userInfoUrl, { headers: { Authorization: `Bearer ${accessToken}` }, }) - ) - return response.data + ); + return response.data; } catch (error) { this.logger.error( error, `Failed to retrieve user information from ${provider}` - ) + ); throw new BadRequestException( `Failed to retrieve user information from ${provider}` - ) + ); } } @@ -123,17 +125,20 @@ export class OAuthService { provider: AuthProvider ) { if (!this.cryptoService.validateState(state, sessionState)) { - throw new ForbiddenException('Invalid state') + throw new ForbiddenException('Invalid state'); } - const tokenData = await this.exchangeCodeForToken(provider, code) + const tokenData = await this.exchangeCodeForToken(provider, code); const userInfo = await this.getUserInfo( provider, tokenData.access_token - ) - const jwt = await this.authService.generateJwt(userInfo.id, provider) - const frontendUrl = this.configService.get('app.frontEndURL') - const params = querystring.stringify({ jwt }) - return `${frontendUrl}/callback?${params}` + ); + const jwt = await this.jwtService.generateAuthJwt( + userInfo.id, + provider + ); + const frontendUrl = this.configService.get('app.frontEndURL'); + const params = querystring.stringify({ jwt }); + return `${frontendUrl}/callback?${params}`; } } diff --git a/src/auth/types/auth-provider.type.ts b/src/auth/types/auth-provider.type.ts index f85c4a8..69d21b5 100644 --- a/src/auth/types/auth-provider.type.ts +++ b/src/auth/types/auth-provider.type.ts @@ -1,2 +1,2 @@ -import { AUTH_PROVIDERS } from '../constants/provider.constants' -export type AuthProvider = (typeof AUTH_PROVIDERS)[keyof typeof AUTH_PROVIDERS] +import { AUTH_PROVIDERS } from '../constants/provider.constants'; +export type AuthProvider = (typeof AUTH_PROVIDERS)[keyof typeof AUTH_PROVIDERS]; diff --git a/src/auth/types/jwt-payload.type.ts b/src/auth/types/jwt-payload.type.ts index 68704dd..b16d104 100644 --- a/src/auth/types/jwt-payload.type.ts +++ b/src/auth/types/jwt-payload.type.ts @@ -1,7 +1,8 @@ export type JwtPayload = { - sub: string // User ID or public key - provider: string // Provider (e.g., google, discord, siwe) - iss: string // Issuer - iat: number // Issued at timestamp - exp: number // Expiration timestamp -} + sub: string; + provider: string; + iss: string; + iat: number; + exp: number; + metadata?: Record; +}; diff --git a/src/config/app.config.ts b/src/config/app.config.ts index 20a76c9..7c7e74c 100644 --- a/src/config/app.config.ts +++ b/src/config/app.config.ts @@ -1,12 +1,12 @@ -import { registerAs } from '@nestjs/config' -import * as Joi from 'joi' +import { registerAs } from '@nestjs/config'; +import * as Joi from 'joi'; export default registerAs('app', () => ({ nodeEnv: process.env.NODE_ENV, port: parseInt(process.env.PORT, 10), sessionSecret: process.env.SESSION_SECRET, frontEndURL: process.env.FRONTEND_URL, -})) +})); export const appConfigSchema = { NODE_ENV: Joi.string() @@ -16,4 +16,4 @@ export const appConfigSchema = { PORT: Joi.number().default(3000).required().description('Application port'), SESSION_SECRET: Joi.string().required().description('Session Secret'), FRONTEND_URL: Joi.string().required().description('Frontend URL'), -} +}; diff --git a/src/config/index.ts b/src/config/index.ts index a387253..fd248a7 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,15 +1,18 @@ -import * as Joi from 'joi' -import googleConfig, { - googleConfigSchema, -} from '../auth-google/config/google.config' +import * as Joi from 'joi'; + import discordConfig, { discordConfigSchema, -} from '../auth-discord/config/auth-discord.config' -import authConfig, { authConfigSchema } from '../auth/config/auth.config' -import appConfig, { appConfigSchema } from './app.config' -import loggerConfig, { loggerConfigSchema } from './logger.config' -import walletConfig, { walletConfigSchema } from './wallet.config' -import litConfig, { litConfigSchema } from '../lit/config/lit.config' +} from '../auth-discord/config/auth-discord.config'; +import googleConfig, { + googleConfigSchema, +} from '../auth-google/config/google.config'; +import authConfig, { authConfigSchema } from '../auth/config/auth.config'; +import jwtConfig, { jwtConfigSchema } from '../jwt/config/jwt.config'; +import litConfig, { litConfigSchema } from '../lit/config/lit.config'; +import appConfig, { appConfigSchema } from './app.config'; +import loggerConfig, { loggerConfigSchema } from './logger.config'; +import walletConfig, { walletConfigSchema } from './wallet.config'; + export const configModules = [ appConfig, googleConfig, @@ -18,7 +21,8 @@ export const configModules = [ authConfig, walletConfig, litConfig, -] + jwtConfig, +]; export const configValidationSchema = Joi.object({ ...appConfigSchema, @@ -30,4 +34,5 @@ export const configValidationSchema = Joi.object({ ...loggerConfigSchema, ...walletConfigSchema, ...litConfigSchema, -}) + ...jwtConfigSchema, +}); diff --git a/src/config/logger.config.ts b/src/config/logger.config.ts index 6f5a4e8..01c9460 100644 --- a/src/config/logger.config.ts +++ b/src/config/logger.config.ts @@ -1,13 +1,13 @@ -import { registerAs } from '@nestjs/config' -import * as Joi from 'joi' +import { registerAs } from '@nestjs/config'; +import * as Joi from 'joi'; export default registerAs('logger', () => ({ level: process.env.LOG_LEVEL || 'info', -})) +})); export const loggerConfigSchema = { LOG_LEVEL: Joi.string() .valid('fatal', 'error', 'warn', 'info', 'debug', 'trace') .default('info') .required(), -} +}; diff --git a/src/config/pino.config.ts b/src/config/pino.config.ts index 4585e1c..b36b0a6 100644 --- a/src/config/pino.config.ts +++ b/src/config/pino.config.ts @@ -1,7 +1,7 @@ -import { ConfigService } from '@nestjs/config' +import { ConfigService } from '@nestjs/config'; export const pinoConfig = (configService: ConfigService) => { - const nodeEnv = configService.get('app.nodeEnv') - const logLevel = configService.get('logger.level') + const nodeEnv = configService.get('app.nodeEnv'); + const logLevel = configService.get('logger.level'); return { pinoHttp: { @@ -17,11 +17,11 @@ export const pinoConfig = (configService: ConfigService) => { : undefined, formatters: { level: (label) => { - return { level: label.toUpperCase() } + return { level: label.toUpperCase() }; }, }, timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`, }, - } -} + }; +}; diff --git a/src/config/wallet.config.ts b/src/config/wallet.config.ts index 264b11e..a14e313 100644 --- a/src/config/wallet.config.ts +++ b/src/config/wallet.config.ts @@ -1,12 +1,12 @@ -import { registerAs } from '@nestjs/config' -import * as Joi from 'joi' +import { registerAs } from '@nestjs/config'; +import * as Joi from 'joi'; export default registerAs('wallet', () => ({ privateKey: process.env.WALLET_PRIVATE_KEY, -})) +})); export const walletConfigSchema = { WALLET_PRIVATE_KEY: Joi.string() .required() .description('Wallet private key'), -} +}; diff --git a/src/doc/index.ts b/src/doc/index.ts index 4cde183..17419a5 100644 --- a/src/doc/index.ts +++ b/src/doc/index.ts @@ -1,11 +1,11 @@ -import { INestApplication } from '@nestjs/common' -import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger' +import { INestApplication } from '@nestjs/common'; +import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { SWAGGER_API_ROOT, SWAGGER_API_NAME, SWAGGER_API_DESCRIPTION, SWAGGER_API_CURRENT_VERSION, -} from './swagger.constants' +} from './swagger.constants'; export const setupSwagger = (app: INestApplication) => { const options = new DocumentBuilder() @@ -13,7 +13,7 @@ export const setupSwagger = (app: INestApplication) => { .setDescription(SWAGGER_API_DESCRIPTION) .setVersion(SWAGGER_API_CURRENT_VERSION) .addBearerAuth() - .build() - const document = SwaggerModule.createDocument(app, options) - SwaggerModule.setup(SWAGGER_API_ROOT, app, document) -} + .build(); + const document = SwaggerModule.createDocument(app, options); + SwaggerModule.setup(SWAGGER_API_ROOT, app, document); +}; diff --git a/src/doc/swagger.constants.ts b/src/doc/swagger.constants.ts index e217d0b..4c36c3c 100644 --- a/src/doc/swagger.constants.ts +++ b/src/doc/swagger.constants.ts @@ -1,4 +1,4 @@ -export const SWAGGER_API_ROOT = 'api/docs' -export const SWAGGER_API_NAME = 'On-Chain ID API' -export const SWAGGER_API_DESCRIPTION = 'On-Chain ID API Description' -export const SWAGGER_API_CURRENT_VERSION = '1.0' +export const SWAGGER_API_ROOT = 'api/docs'; +export const SWAGGER_API_NAME = 'On-Chain ID API'; +export const SWAGGER_API_DESCRIPTION = 'On-Chain ID API Description'; +export const SWAGGER_API_CURRENT_VERSION = '1.0'; diff --git a/src/eas/constants/attestation.constants.ts b/src/eas/constants/attestation.constants.ts index a5f9fe7..5d88a97 100644 --- a/src/eas/constants/attestation.constants.ts +++ b/src/eas/constants/attestation.constants.ts @@ -1,9 +1,9 @@ export const ATTEST_TYPED_SIGNATURE = - 'Attest(address attester,bytes32 schema,address recipient,uint64 expirationTime,bool revocable,bytes32 refUID,bytes data,uint256 value,uint256 nonce,uint64 deadline)' + 'Attest(address attester,bytes32 schema,address recipient,uint64 expirationTime,bool revocable,bytes32 refUID,bytes data,uint256 value,uint256 nonce,uint64 deadline)'; export const REVOKE_TYPED_SIGNATURE = - 'Revoke(address revoker,bytes32 schema,bytes32 uid,uint256 value,uint256 nonce,uint64 deadline)' -export const ATTEST_PRIMARY_TYPE = 'Attest' -export const REVOKE_PRIMARY_TYPE = 'Revoke' + 'Revoke(address revoker,bytes32 schema,bytes32 uid,uint256 value,uint256 nonce,uint64 deadline)'; +export const ATTEST_PRIMARY_TYPE = 'Attest'; +export const REVOKE_PRIMARY_TYPE = 'Revoke'; export const ATTEST_TYPE = [ { name: 'attester', type: 'address' }, { name: 'schema', type: 'bytes32' }, @@ -15,7 +15,7 @@ export const ATTEST_TYPE = [ { name: 'value', type: 'uint256' }, { name: 'nonce', type: 'uint256' }, { name: 'deadline', type: 'uint64' }, -] +]; export const REVOKE_TYPE = [ { name: 'revoker', type: 'address' }, { name: 'schema', type: 'bytes32' }, @@ -23,6 +23,6 @@ export const REVOKE_TYPE = [ { name: 'value', type: 'uint256' }, { name: 'nonce', type: 'uint256' }, { name: 'deadline', type: 'uint64' }, -] +]; export const SCHEMA_TYPES = - 'bytes32 key, string provider, string secret, string metadata' + 'bytes32 key, string provider, string secret, string metadata'; diff --git a/src/eas/constants/sepolia.constants.ts b/src/eas/constants/sepolia.constants.ts index f07851e..28131ec 100644 --- a/src/eas/constants/sepolia.constants.ts +++ b/src/eas/constants/sepolia.constants.ts @@ -1,9 +1,9 @@ -import { Abi } from 'viem' +import { Abi } from 'viem'; export const SCHEMA_UUID = - '0x85e90e3e16d319578888790af3284fea8bca549305071531e7478e3e0b5e7d6d' + '0x85e90e3e16d319578888790af3284fea8bca549305071531e7478e3e0b5e7d6d'; export const EAS_SEPOLIA_CONTRACT_ADDRESS = - '0xC2679fBD37d54388Ce493F1DB75320D236e1815e' + '0xC2679fBD37d54388Ce493F1DB75320D236e1815e'; export const EAS_SEPOLIA_CONTRACT_ABI: Abi = [ { inputs: [ @@ -763,4 +763,4 @@ export const EAS_SEPOLIA_CONTRACT_ABI: Abi = [ stateMutability: 'nonpayable', type: 'function', }, -] +]; diff --git a/src/eas/dto/decrypt-attestation-secret.dto.ts b/src/eas/dto/decrypt-attestation-secret.dto.ts index 716fd6a..f8f3be8 100644 --- a/src/eas/dto/decrypt-attestation-secret.dto.ts +++ b/src/eas/dto/decrypt-attestation-secret.dto.ts @@ -1,7 +1,7 @@ -import { ApiProperty } from '@nestjs/swagger' -import { IsString, IsNotEmpty, IsNumber } from 'class-validator' -import { JwtProvider } from '../../shared/decorators/jwt-provider.decorator' -import { AUTH_PROVIDERS } from '../../auth/constants/provider.constants' +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsNotEmpty, IsNumber } from 'class-validator'; +import { JwtProvider } from '../../shared/decorators/jwt-provider.decorator'; +import { AUTH_PROVIDERS } from '../../auth/constants/provider.constants'; export class DecryptAttestationSecretDto { @ApiProperty({ @@ -13,7 +13,7 @@ export class DecryptAttestationSecretDto { @IsString() @IsNotEmpty() @JwtProvider(AUTH_PROVIDERS.SIWE) - readonly siweJwt: string + readonly siweJwt: string; @ApiProperty({ description: 'Chain Id', example: '11155111', @@ -21,5 +21,5 @@ export class DecryptAttestationSecretDto { }) @IsNumber() @IsNotEmpty() - readonly chainId: number + readonly chainId: number; } diff --git a/src/eas/dto/sign-delegated-attestation.dto.ts b/src/eas/dto/sign-delegated-attestation.dto.ts index e583c4f..8aa3106 100644 --- a/src/eas/dto/sign-delegated-attestation.dto.ts +++ b/src/eas/dto/sign-delegated-attestation.dto.ts @@ -1,7 +1,7 @@ -import { ApiProperty } from '@nestjs/swagger' -import { IsString, IsNotEmpty, IsNumber } from 'class-validator' -import { JwtProvider } from '../../shared/decorators/jwt-provider.decorator' -import { AUTH_PROVIDERS } from '../../auth/constants/provider.constants' +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsNotEmpty, IsNumber } from 'class-validator'; +import { JwtProvider } from '../../shared/decorators/jwt-provider.decorator'; +import { AUTH_PROVIDERS } from '../../auth/constants/provider.constants'; export class SignDelegatedAttestationDto { @ApiProperty({ @@ -13,7 +13,7 @@ export class SignDelegatedAttestationDto { @IsString() @IsNotEmpty() @JwtProvider(AUTH_PROVIDERS.SIWE) - readonly siweJwt: string + readonly siweJwt: string; @ApiProperty({ description: 'The siwe JWT or any provider JWT.', example: @@ -22,7 +22,7 @@ export class SignDelegatedAttestationDto { }) @IsString() @IsNotEmpty() - readonly anyJwt: string + readonly anyJwt: string; @ApiProperty({ description: 'Chain Id', @@ -31,5 +31,5 @@ export class SignDelegatedAttestationDto { }) @IsNumber() @IsNotEmpty() - readonly chainId: number + readonly chainId: number; } diff --git a/src/eas/dto/sign-delegated-revocation.dto.ts b/src/eas/dto/sign-delegated-revocation.dto.ts index 98e26c6..6d98ab3 100644 --- a/src/eas/dto/sign-delegated-revocation.dto.ts +++ b/src/eas/dto/sign-delegated-revocation.dto.ts @@ -1,7 +1,7 @@ -import { ApiProperty } from '@nestjs/swagger' -import { IsString, IsNotEmpty, IsNumber } from 'class-validator' -import { JwtProvider } from '../../shared/decorators/jwt-provider.decorator' -import { AUTH_PROVIDERS } from '../../auth/constants/provider.constants' +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsNotEmpty, IsNumber } from 'class-validator'; +import { JwtProvider } from '../../shared/decorators/jwt-provider.decorator'; +import { AUTH_PROVIDERS } from '../../auth/constants/provider.constants'; export class SignDelegatedRevocationDto { @ApiProperty({ @@ -13,7 +13,7 @@ export class SignDelegatedRevocationDto { @IsString() @IsNotEmpty() @JwtProvider(AUTH_PROVIDERS.SIWE) - readonly siweJwt: string + readonly siweJwt: string; @ApiProperty({ description: 'Chain Id', example: '11155111', @@ -21,5 +21,5 @@ export class SignDelegatedRevocationDto { }) @IsNumber() @IsNotEmpty() - readonly chainId: number + readonly chainId: number; } diff --git a/src/eas/eas.controller.ts b/src/eas/eas.controller.ts index 2c4e59e..94180eb 100644 --- a/src/eas/eas.controller.ts +++ b/src/eas/eas.controller.ts @@ -5,13 +5,13 @@ import { HttpStatus, HttpCode, Param, -} from '@nestjs/common' -import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger' -import { SignDelegatedAttestationDto } from './dto/sign-delegated-attestation.dto' -import { SignDelegatedRevocationDto } from './dto/sign-delegated-revocation.dto' -import { DecryptAttestationSecretDto } from './dto/decrypt-attestation-secret.dto' -import { EasService } from '../eas/eas.service' -import { ApiParam } from '@nestjs/swagger' +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { SignDelegatedAttestationDto } from './dto/sign-delegated-attestation.dto'; +import { SignDelegatedRevocationDto } from './dto/sign-delegated-revocation.dto'; +import { DecryptAttestationSecretDto } from './dto/decrypt-attestation-secret.dto'; +import { EasService } from '../eas/eas.service'; +import { ApiParam } from '@nestjs/swagger'; @ApiTags(`Eas`) @Controller(`eas`) @@ -30,7 +30,7 @@ export class EasController { ) { return await this.easService.getSignedDelegatedAttestation( signDelegatedAttestationDto - ) + ); } @Post(':uid/sign-delegated-revocation') @@ -50,7 +50,7 @@ export class EasController { return await this.easService.getSignedDelegatedRevocation( signDelegatedRevocationDto, uid - ) + ); } @Post(':uid/decrypt-attestation-secret') @@ -70,6 +70,6 @@ export class EasController { return await this.easService.decryptAttestationSecret( decryptAttestationSecretDto, uid - ) + ); } } diff --git a/src/eas/eas.module.ts b/src/eas/eas.module.ts index c054ca2..17fe4f5 100644 --- a/src/eas/eas.module.ts +++ b/src/eas/eas.module.ts @@ -1,11 +1,13 @@ -import { Module } from '@nestjs/common' -import { EasService } from './eas.service' -import { UtilsModule } from '../utils/utils.module' -import { EasController } from './eas.controller' -import { AuthModule } from '../auth/auth.module' -import { LitModule } from '../lit/lit.module' +import { Module } from '@nestjs/common'; + +import { JwtModule } from '../jwt/jwt.module'; +import { LitModule } from '../lit/lit.module'; +import { UtilsModule } from '../utils/utils.module'; +import { EasController } from './eas.controller'; +import { EasService } from './eas.service'; + @Module({ - imports: [AuthModule, UtilsModule, LitModule], + imports: [JwtModule, UtilsModule, LitModule], providers: [EasService], controllers: [EasController], exports: [EasService], diff --git a/src/eas/eas.service.spec.ts b/src/eas/eas.service.spec.ts index bad85f6..0aa7371 100644 --- a/src/eas/eas.service.spec.ts +++ b/src/eas/eas.service.spec.ts @@ -1,25 +1,28 @@ -import { Test, TestingModule } from '@nestjs/testing' -import { EasService } from './eas.service' -import { ConfigService } from '@nestjs/config' -import { generatePrivateKey } from 'viem/accounts' // import { EAS_SEPOLIA_CONTRACT_ADDRESS } from './constants/sepolia' // import { sepolia } from 'viem/chains' -import { PinoLogger, LoggerModule } from 'nestjs-pino' -import { EthersUtilsService } from '../utils/ethers.utils.service' -import { AuthService } from '../auth/auth.service' -import { LitService } from '../lit/lit.service' -import { DataUtilsService } from '../utils/data-utils.service' -const mockPrivateKey = generatePrivateKey() +import { LoggerModule, PinoLogger } from 'nestjs-pino'; +import { generatePrivateKey } from 'viem/accounts'; + +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; + +import { AuthService } from '../auth/jwt.service'; +import { LitService } from '../lit/lit.service'; +import { DataUtilsService } from '../utils/data-utils.service'; +import { EthersUtilsService } from '../utils/ethers.utils.service'; +import { EasService } from './eas.service'; + +const mockPrivateKey = generatePrivateKey(); const mockConfigService = { get: jest.fn((key: string) => { - if (key === 'wallet.privateKey') return mockPrivateKey + if (key === 'wallet.privateKey') return mockPrivateKey; }), -} +}; describe('EasService', () => { - let service: EasService - let loggerMock: PinoLogger + let service: EasService; + let loggerMock: PinoLogger; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -33,14 +36,14 @@ describe('EasService', () => { { provide: ConfigService, useValue: mockConfigService }, { provide: PinoLogger, useValue: loggerMock }, ], - }).compile() + }).compile(); - service = module.get(EasService) - }) + service = module.get(EasService); + }); it('should be defined', () => { - expect(service).toBeDefined() - }) + expect(service).toBeDefined(); + }); // it('should have a eas contract', () => { // expect(service.eas).toBeDefined() @@ -60,4 +63,4 @@ describe('EasService', () => { // expect(await service.getDomain()).toEqual(expected) // }) -}) +}); diff --git a/src/eas/eas.service.ts b/src/eas/eas.service.ts index b6d953c..2b5e8ff 100644 --- a/src/eas/eas.service.ts +++ b/src/eas/eas.service.ts @@ -1,42 +1,45 @@ +import { Signer } from 'ethers'; +import { InjectPinoLogger, PinoLogger } from 'nestjs-pino'; +import { generateHash } from 'oci-js-sdk'; +import { Address } from 'viem'; +import { privateKeyToAddress } from 'viem/accounts'; + +import { + Attestation, + EAS, + NO_EXPIRATION, + SchemaDecodedItem, + SchemaEncoder, + ZERO_BYTES32, +} from '@ethereum-attestation-service/eas-sdk'; import { - Injectable, BadRequestException, ForbiddenException, + Injectable, InternalServerErrorException, NotFoundException, -} from '@nestjs/common' -import { ConfigService } from '@nestjs/config' -import { SCHEMA_TYPES } from './constants/attestation.constants' -import { SUPPORTED_CHAINS, CHAINS } from '../shared/constants/chain.constants' -import { SupportedChainId } from '../shared/types/chain.type' -import { Signer } from 'ethers' -import { EthersUtilsService } from '../utils/ethers.utils.service' -import { - EAS, - SchemaEncoder, - NO_EXPIRATION, - ZERO_BYTES32, - Attestation, - SchemaDecodedItem, -} from '@ethereum-attestation-service/eas-sdk' -import { AuthService } from '../auth/auth.service' -import { LitService } from '../lit/lit.service' -import { DataUtilsService } from '../utils/data-utils.service' -import { Address } from 'viem' -import { PinoLogger, InjectPinoLogger } from 'nestjs-pino' -import { privateKeyToAddress } from 'viem/accounts' -import { SignDelegatedAttestationDto } from './dto/sign-delegated-attestation.dto' -import { SignDelegatedRevocationDto } from './dto/sign-delegated-revocation.dto' -import { DecryptAttestationSecretDto } from './dto/decrypt-attestation-secret.dto' -import { generateHash } from 'oci-js-sdk' +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +import { JwtService } from '../jwt/jwt.service'; +import { AuthJwtPayload } from '../jwt/types/jwt-payload.type'; +import { LitService } from '../lit/lit.service'; +import { CHAINS, SUPPORTED_CHAINS } from '../shared/constants/chain.constants'; +import { SupportedChainId } from '../shared/types/chain.type'; +import { DataUtilsService } from '../utils/data-utils.service'; +import { EthersUtilsService } from '../utils/ethers.utils.service'; +import { SCHEMA_TYPES } from './constants/attestation.constants'; +import { DecryptAttestationSecretDto } from './dto/decrypt-attestation-secret.dto'; +import { SignDelegatedAttestationDto } from './dto/sign-delegated-attestation.dto'; +import { SignDelegatedRevocationDto } from './dto/sign-delegated-revocation.dto'; @Injectable() export class EasService { - private attesters: Map = new Map() - private contracts: Map = new Map() + private attesters: Map = new Map(); + private contracts: Map = new Map(); constructor( - private readonly authService: AuthService, + private readonly jwtService: JwtService, private readonly litService: LitService, private readonly dataUtilsService: DataUtilsService, private readonly ethersUtilsService: EthersUtilsService, @@ -44,14 +47,14 @@ export class EasService { @InjectPinoLogger(EasService.name) private readonly logger: PinoLogger ) { - this.setAttesters() - this.setContracts() + this.setAttesters(); + this.setContracts(); } private setAttesters() { const privateKey = this.configService.get( 'wallet.privateKey' - ) as '0x${string}' + ) as '0x${string}'; for (const chainId of SUPPORTED_CHAINS) { this.attesters.set( chainId, @@ -59,39 +62,39 @@ export class EasService { CHAINS[chainId].rpcURL, privateKey ) - ) + ); } } private setContracts(): void { for (const chainId of SUPPORTED_CHAINS) { - const eas = new EAS(CHAINS[chainId].eas.address) - const attester = this.getAttester(chainId) - eas.connect(attester) - this.contracts.set(chainId, eas) + const eas = new EAS(CHAINS[chainId].eas.address); + const attester = this.getAttester(chainId); + eas.connect(attester); + this.contracts.set(chainId, eas); } } getContract(chainId: SupportedChainId): EAS { - return this.contracts.get(chainId) + return this.contracts.get(chainId); } getAttester(chainId: SupportedChainId): Signer { - return this.attesters.get(chainId) + return this.attesters.get(chainId); } encodeAttestationData(params: any[]): string { - const schemaEncoder = new SchemaEncoder(SCHEMA_TYPES) + const schemaEncoder = new SchemaEncoder(SCHEMA_TYPES); return schemaEncoder.encodeData([ { name: 'key', value: params[0], type: 'bytes32' }, { name: 'provider', value: params[1], type: 'string' }, { name: 'secret', value: params[2], type: 'string' }, { name: 'metadata', value: params[3], type: 'string' }, - ]) + ]); } decodeAttestationData(data: string): SchemaDecodedItem[] { - const schemaEncoder = new SchemaEncoder(SCHEMA_TYPES) - return schemaEncoder.decodeData(data) + const schemaEncoder = new SchemaEncoder(SCHEMA_TYPES); + return schemaEncoder.decodeData(data); } private buildAttestationPayload( @@ -108,40 +111,44 @@ export class EasService { data: encodedData, deadline: 0n, value: 0n, - } + }; } checkAttestar(attestation: Attestation, attester: Address) { if (attestation.attester !== attester) { throw new BadRequestException( `We aren't attester of this attesation` - ) + ); } } checkRecipient(attestation: Attestation, recipient: Address) { if (attestation.recipient !== recipient) { throw new ForbiddenException( `You aren't recipient of this attesation` - ) + ); } } async getAttestaion(chainId: SupportedChainId, uid: string) { try { - const eas = this.getContract(chainId) - return await eas.getAttestation(uid) + const eas = this.getContract(chainId); + return await eas.getAttestation(uid); } catch (error) { - this.logger.error(error, "Attestation didn't find") - throw new NotFoundException("Attestation didn't find") + this.logger.error(error, "Attestation didn't find"); + throw new NotFoundException("Attestation didn't find"); } } async getSignedDelegatedAttestation( signDelegatedAttestationDto: SignDelegatedAttestationDto ) { - const { chainId, anyJwt, siweJwt } = signDelegatedAttestationDto - const siweJwtPayload = await this.authService.validateToken(siweJwt) - const anyJwtPayload = await this.authService.validateToken(anyJwt) - const key = generateHash(anyJwtPayload.sub, anyJwtPayload.provider) + const { chainId, anyJwt, siweJwt } = signDelegatedAttestationDto; + const siweJwtPayload = (await this.jwtService.validateToken( + siweJwt + )) as AuthJwtPayload; + const anyJwtPayload = (await this.jwtService.validateToken( + anyJwt + )) as AuthJwtPayload; + const key = generateHash(anyJwtPayload.sub, anyJwtPayload.provider); const secret = await this.litService.encryptToJson( chainId, { @@ -150,36 +157,36 @@ export class EasService { }, key, siweJwtPayload.sub as '0x${string}' - ) + ); - const eas = this.getContract(chainId) + const eas = this.getContract(chainId); const encodedData = this.encodeAttestationData([ key, anyJwtPayload.provider, secret, JSON.stringify({}, null, 0), - ]) + ]); const attestationPayload = this.buildAttestationPayload( chainId, encodedData, siweJwtPayload.sub as '0x${string}' - ) - const delegated = await eas.getDelegated() - const attester = this.getAttester(chainId) + ); + const delegated = await eas.getDelegated(); + const attester = this.getAttester(chainId); try { const signedDelegatedAttestation = await delegated.signDelegatedAttestation( attestationPayload, attester - ) + ); return this.dataUtilsService.convertBigIntsToStrings( signedDelegatedAttestation - ) + ); } catch (error) { - this.logger.error(error, 'Failed to signed delegated attestation') + this.logger.error(error, 'Failed to signed delegated attestation'); throw new InternalServerErrorException( `Failed to signed delegated attestation` - ) + ); } } @@ -187,9 +194,9 @@ export class EasService { signDelegatedRevocationDto: SignDelegatedRevocationDto, uid: string ) { - const { chainId, siweJwt } = signDelegatedRevocationDto - const siweJwtPayload = await this.authService.validateToken(siweJwt) - const attestation = await this.getAttestaion(chainId, uid) + const { chainId, siweJwt } = signDelegatedRevocationDto; + const siweJwtPayload = await this.jwtService.validateToken(siweJwt); + const attestation = await this.getAttestaion(chainId, uid); await this.checkAttestar( attestation, privateKeyToAddress( @@ -197,14 +204,14 @@ export class EasService { 'wallet.privateKey' ) as '0x${string}' ) - ) + ); await this.checkRecipient( attestation, siweJwtPayload.sub as '0x${string}' - ) - const eas = this.getContract(chainId) - const delegated = await eas.getDelegated() - const attester = this.getAttester(chainId) + ); + const eas = this.getContract(chainId); + const delegated = await eas.getDelegated(); + const attester = this.getAttester(chainId); try { const signedDelegatedRevocation = await delegated.signDelegatedRevocation( @@ -215,15 +222,15 @@ export class EasService { value: 0n, }, attester - ) + ); return this.dataUtilsService.convertBigIntsToStrings( signedDelegatedRevocation - ) + ); } catch (error) { - this.logger.error(error, 'Failed to signed delegated revocation') + this.logger.error(error, 'Failed to signed delegated revocation'); throw new InternalServerErrorException( `Failed to signed delegated revocation` - ) + ); } } @@ -231,9 +238,9 @@ export class EasService { decryptAttestationSecretDto: DecryptAttestationSecretDto, uid: string ) { - const { chainId, siweJwt } = decryptAttestationSecretDto - const siweJwtPayload = await this.authService.validateToken(siweJwt) - const attestation = await this.getAttestaion(chainId, uid) + const { chainId, siweJwt } = decryptAttestationSecretDto; + const siweJwtPayload = await this.jwtService.validateToken(siweJwt); + const attestation = await this.getAttestaion(chainId, uid); await this.checkAttestar( attestation, privateKeyToAddress( @@ -241,13 +248,13 @@ export class EasService { 'wallet.privateKey' ) as '0x${string}' ) - ) + ); await this.checkRecipient( attestation, siweJwtPayload.sub as '0x${string}' - ) - const decodedData = this.decodeAttestationData(attestation.data) - const secret = decodedData[2].value.value - return await this.litService.decryptFromJson(chainId, secret) + ); + const decodedData = this.decodeAttestationData(attestation.data); + const secret = decodedData[2].value.value; + return await this.litService.decryptFromJson(chainId, secret); } } diff --git a/src/eas/interfaces/attestation.interfaces.ts b/src/eas/interfaces/attestation.interfaces.ts index a0a9532..3c955c3 100644 --- a/src/eas/interfaces/attestation.interfaces.ts +++ b/src/eas/interfaces/attestation.interfaces.ts @@ -1,34 +1,34 @@ -import { Address } from 'viem' +import { Address } from 'viem'; export interface IAttestationRequestData { - recipient: Address - data: Address - expirationTime: bigint - revocable: boolean - refUID: Address - value: bigint + recipient: Address; + data: Address; + expirationTime: bigint; + revocable: boolean; + refUID: Address; + value: bigint; } export interface IDelegatedAttestationMessage extends IAttestationRequestData { - schema: Address - attester: Address - deadline: bigint - nonce: bigint + schema: Address; + attester: Address; + deadline: bigint; + nonce: bigint; } export interface IAttestationRequest { - schema: Address - data: IAttestationRequestData + schema: Address; + data: IAttestationRequestData; } export interface IDelegatedAttestationRequest extends IAttestationRequest { - signature: { r: Address; s: Address; v: number } - attester: Address - deadline: bigint + signature: { r: Address; s: Address; v: number }; + attester: Address; + deadline: bigint; } export interface TypedData { - name: string + name: string; type: | 'bool' | 'uint8' @@ -40,5 +40,5 @@ export interface TypedData { | 'address' | 'string' | 'bytes' - | 'bytes32' + | 'bytes32'; } diff --git a/src/lit/config/lit.config.ts b/src/lit/config/lit.config.ts index f791fa3..312449e 100644 --- a/src/lit/config/lit.config.ts +++ b/src/lit/config/lit.config.ts @@ -1,14 +1,14 @@ -import * as Joi from 'joi' -import { registerAs } from '@nestjs/config' -import { LitNetwork } from '@lit-protocol/constants' +import * as Joi from 'joi'; +import { registerAs } from '@nestjs/config'; +import { LitNetwork } from '@lit-protocol/constants'; export default registerAs('lit', () => ({ network: process.env.LIT_NETWORK, -})) +})); export const litConfigSchema = { LIT_NETWORK: Joi.string() .required() .description('Lit network') .valid(LitNetwork.Datil, LitNetwork.DatilTest, LitNetwork.DatilDev), -} +}; diff --git a/src/lit/constants/network.constants.ts b/src/lit/constants/network.constants.ts index ed2dde2..f71310f 100644 --- a/src/lit/constants/network.constants.ts +++ b/src/lit/constants/network.constants.ts @@ -1,4 +1,4 @@ -import { LitNetwork, LIT_RPC } from '@lit-protocol/constants' +import { LitNetwork, LIT_RPC } from '@lit-protocol/constants'; export const networks = { 'datil-dev': { @@ -37,4 +37,4 @@ export const networks = { }, rpc: LIT_RPC.CHRONICLE_YELLOWSTONE, }, -} +}; diff --git a/src/lit/lit.module.ts b/src/lit/lit.module.ts index eef91ce..1e6ec7e 100644 --- a/src/lit/lit.module.ts +++ b/src/lit/lit.module.ts @@ -1,6 +1,6 @@ -import { Module } from '@nestjs/common' -import { LitService } from './lit.service' -import { UtilsModule } from '../utils/utils.module' +import { Module } from '@nestjs/common'; +import { LitService } from './lit.service'; +import { UtilsModule } from '../utils/utils.module'; @Module({ imports: [UtilsModule], diff --git a/src/lit/lit.service.ts b/src/lit/lit.service.ts index 0fa01cc..95b9e28 100644 --- a/src/lit/lit.service.ts +++ b/src/lit/lit.service.ts @@ -3,36 +3,36 @@ import { InternalServerErrorException, UnauthorizedException, HttpStatus, -} from '@nestjs/common' +} from '@nestjs/common'; import { LitNodeClientNodeJs, encryptToJson, decryptFromJson, -} from '@lit-protocol/lit-node-client-nodejs' -import { ConfigService } from '@nestjs/config' -import { networks } from './constants/network.constants' -import { UnifiedAccessControlConditions } from '@lit-protocol/types' +} from '@lit-protocol/lit-node-client-nodejs'; +import { ConfigService } from '@nestjs/config'; +import { networks } from './constants/network.constants'; +import { UnifiedAccessControlConditions } from '@lit-protocol/types'; import { PERMISSION_CONTRACTS, ACCESS_MANAGER_CONTRACTS, -} from '../shared/constants/chain.constants' -import { SupportedChainId, LitChain } from '../shared/types/chain.type' -import { LIT_CHAINS } from '@lit-protocol/constants' -import { PinoLogger, InjectPinoLogger } from 'nestjs-pino' -import { Address } from 'viem' -import { EthersUtilsService } from '../utils/ethers.utils.service' -import { LitNetwork } from '@lit-protocol/constants' +} from '../shared/constants/chain.constants'; +import { SupportedChainId, LitChain } from '../shared/types/chain.type'; +import { LIT_CHAINS } from '@lit-protocol/constants'; +import { PinoLogger, InjectPinoLogger } from 'nestjs-pino'; +import { Address } from 'viem'; +import { EthersUtilsService } from '../utils/ethers.utils.service'; +import { LitNetwork } from '@lit-protocol/constants'; import { LitAbility, LitAccessControlConditionResource, createSiweMessage, generateAuthSig, -} from '@lit-protocol/auth-helpers' +} from '@lit-protocol/auth-helpers'; @Injectable() export class LitService { - private litNodeClient: LitNodeClientNodeJs = null - private networkName: LitNetwork + private litNodeClient: LitNodeClientNodeJs = null; + private networkName: LitNetwork; constructor( private readonly configService: ConfigService, private readonly ethersUtilsService: EthersUtilsService, @@ -44,25 +44,25 @@ export class LitService { async connect() { this.networkName = this.configService.get( 'lit.network' - ) as LitNetwork + ) as LitNetwork; this.litNodeClient = new LitNodeClientNodeJs( networks[this.networkName].clientConfig - ) - await this.litNodeClient.connect() + ); + await this.litNodeClient.connect(); } async disconnect() { - await this.litNodeClient.disconnect() - this.litNodeClient = null + await this.litNodeClient.disconnect(); + this.litNodeClient = null; } chainIdToLitChainName = (chainId: number): LitChain | undefined => { for (const [name, chain] of Object.entries(LIT_CHAINS)) { if (chain.chainId === chainId) { - return name as LitChain + return name as LitChain; } } - } + }; generateunifiedAccessControlConditions( chainId: SupportedChainId, key: Address, @@ -118,31 +118,31 @@ export class LitService { value: 'true', }, }, - ] + ]; } async getSessionSigsViaAuthSig(chainId: SupportedChainId) { const signer = this.ethersUtilsService.getSigner( networks[this.networkName].rpc, this.configService.get('wallet.privateKey') - ) + ); if (!this.litNodeClient) { - await this.connect() + await this.connect(); } const expiration = new Date( Date.now() + 1000 * 60 * 60 * 24 - ).toISOString() + ).toISOString(); const resourceAbilityRequests = [ { resource: new LitAccessControlConditionResource('*'), ability: LitAbility.AccessControlConditionDecryption, }, - ] + ]; const authNeededCallback = async (params: { - uri?: string - expiration?: string - resourceAbilityRequests?: any + uri?: string; + expiration?: string; + resourceAbilityRequests?: any; }) => { const toSign = await createSiweMessage({ uri: params.uri!, @@ -151,39 +151,41 @@ export class LitService { walletAddress: await signer.getAddress(), nonce: await this.litNodeClient!.getLatestBlockhash(), litNodeClient: this.litNodeClient, - }) + }); return await generateAuthSig({ signer: signer, toSign, - }) - } + }); + }; return await this.litNodeClient.getSessionSigs({ chain: this.chainIdToLitChainName(chainId), expiration, resourceAbilityRequests, authNeededCallback, - }) + }); } async decryptFromJson(chainId: SupportedChainId, dataToDecrypt: any) { if (!this.litNodeClient) { - await this.connect() + await this.connect(); } - const sessionSigs = await this.getSessionSigsViaAuthSig(chainId) + const sessionSigs = await this.getSessionSigsViaAuthSig(chainId); try { return await decryptFromJson({ litNodeClient: this.litNodeClient, parsedJsonData: JSON.parse(dataToDecrypt), sessionSigs, - }) + }); } catch (error) { - this.logger.error(error, `Failed to decrypt data`) + this.logger.error(error, `Failed to decrypt data`); if (error.status === HttpStatus.UNAUTHORIZED) { - throw new UnauthorizedException(error.message) + throw new UnauthorizedException(error.message); } else { - throw new InternalServerErrorException('Failed to decrypt data') + throw new InternalServerErrorException( + 'Failed to decrypt data' + ); } } } @@ -195,7 +197,7 @@ export class LitService { userAddress: Address ): Promise { if (!this.litNodeClient) { - await this.connect() + await this.connect(); } try { return await encryptToJson({ @@ -208,10 +210,10 @@ export class LitService { userAddress ), chain: this.chainIdToLitChainName(chainId), - }) + }); } catch (error) { - this.logger.error(error, `Failed to encrypt data`) - throw new InternalServerErrorException(`Failed to encrypt data`) + this.logger.error(error, `Failed to encrypt data`); + throw new InternalServerErrorException(`Failed to encrypt data`); } } } diff --git a/src/main.ts b/src/main.ts index b9ed375..0de98bf 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,26 +1,26 @@ -import { NestFactory } from '@nestjs/core' -import { AppModule } from './app.module' -import { ConfigService } from '@nestjs/config' -import helmet from 'helmet' -import * as compression from 'compression' -import { Logger, LoggerErrorInterceptor } from 'nestjs-pino' -import * as session from 'express-session' -import { VersioningType, ValidationPipe } from '@nestjs/common' -import { setupSwagger } from './doc' -import { HttpExceptionFilter } from './shared/filters/http-exception.filter' +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; +import { ConfigService } from '@nestjs/config'; +import helmet from 'helmet'; +import * as compression from 'compression'; +import { Logger, LoggerErrorInterceptor } from 'nestjs-pino'; +import * as session from 'express-session'; +import { VersioningType, ValidationPipe } from '@nestjs/common'; +import { setupSwagger } from './doc'; +import { HttpExceptionFilter } from './shared/filters/http-exception.filter'; async function bootstrap() { - const app = await NestFactory.create(AppModule, { bufferLogs: true }) - app.useLogger(app.get(Logger)) - app.useGlobalInterceptors(new LoggerErrorInterceptor()) - app.useGlobalFilters(new HttpExceptionFilter()) - app.useGlobalPipes(new ValidationPipe()) - app.use(helmet()) - app.use(compression()) - app.enableCors() + const app = await NestFactory.create(AppModule, { bufferLogs: true }); + app.useLogger(app.get(Logger)); + app.useGlobalInterceptors(new LoggerErrorInterceptor()); + app.useGlobalFilters(new HttpExceptionFilter()); + app.useGlobalPipes(new ValidationPipe()); + app.use(helmet()); + app.use(compression()); + app.enableCors(); - const configService = app.get(ConfigService) - const port = configService.get('app.port') + const configService = app.get(ConfigService); + const port = configService.get('app.port'); app.use( session({ @@ -28,20 +28,20 @@ async function bootstrap() { resave: false, saveUninitialized: false, }) - ) + ); app.enableVersioning({ type: VersioningType.URI, defaultVersion: '1', prefix: 'api/v', - }) + }); - setupSwagger(app) + setupSwagger(app); await app.listen(port, () => { - const logger = app.get(Logger) - logger.log(`Server is running on port ${port}..`, 'NestApplication') - }) + const logger = app.get(Logger); + logger.log(`Server is running on port ${port}..`, 'NestApplication'); + }); } -bootstrap() +bootstrap(); diff --git a/src/shared/constants/chain.constants.ts b/src/shared/constants/chain.constants.ts index 2b92714..70ce7e2 100644 --- a/src/shared/constants/chain.constants.ts +++ b/src/shared/constants/chain.constants.ts @@ -1,4 +1,4 @@ -export const SUPPORTED_CHAINS = [11155111, 11155420, 84532, 42161] // Ethereum Sepolia - Optimism Sepolia - Base Sepolia - Arbitrum One +export const SUPPORTED_CHAINS = [11155111, 11155420, 84532, 42161]; // Ethereum Sepolia - Optimism Sepolia - Base Sepolia - Arbitrum One export const CHAINS = { 11155111: { @@ -29,7 +29,7 @@ export const CHAINS = { schema: '0x6b5b50f2de8b387664838bd3c751e21f6b9aac7cf4bf5b2fb86e760b89a8a22d', }, }, -} +}; export const PERMISSION_CONTRACTS = { 11155111: { @@ -633,7 +633,7 @@ export const PERMISSION_CONTRACTS = { }, }, }, -} +}; export const ACCESS_MANAGER_CONTRACTS = { 11155111: { @@ -5406,4 +5406,4 @@ export const ACCESS_MANAGER_CONTRACTS = { HasRoleRoleId: '2', }, }, -} +}; diff --git a/src/shared/decorators/jwt-provider.decorator.ts b/src/shared/decorators/jwt-provider.decorator.ts index a794010..b82d83a 100644 --- a/src/shared/decorators/jwt-provider.decorator.ts +++ b/src/shared/decorators/jwt-provider.decorator.ts @@ -2,8 +2,8 @@ import { registerDecorator, ValidationOptions, ValidationArguments, -} from 'class-validator' -import { decode } from 'jsonwebtoken' +} from 'class-validator'; +import { decode } from 'jsonwebtoken'; export function JwtProvider( expectedProvider: string, @@ -18,19 +18,19 @@ export function JwtProvider( validator: { validate(value: any) { try { - const decoded = decode(value) + const decoded = decode(value); if (decoded['provider'] === expectedProvider) { - return true + return true; } - return false + return false; } catch (error) { - return false + return false; } }, defaultMessage(args: ValidationArguments) { - return `${args.property} provider must be ${expectedProvider}.` + return `${args.property} provider must be ${expectedProvider}.`; }, }, - }) - } + }); + }; } diff --git a/src/shared/filters/http-exception.filter.ts b/src/shared/filters/http-exception.filter.ts index dbe6a2a..ec01f3b 100644 --- a/src/shared/filters/http-exception.filter.ts +++ b/src/shared/filters/http-exception.filter.ts @@ -5,26 +5,26 @@ import { HttpException, HttpStatus, Logger, -} from '@nestjs/common' -import { Request, Response } from 'express' +} from '@nestjs/common'; +import { Request, Response } from 'express'; @Catch() export class HttpExceptionFilter implements ExceptionFilter { - private readonly logger = new Logger(HttpExceptionFilter.name) + private readonly logger = new Logger(HttpExceptionFilter.name); catch(exception: unknown, host: ArgumentsHost) { - const ctx = host.switchToHttp() - const response = ctx.getResponse() - const request = ctx.getRequest() + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); const status = exception instanceof HttpException ? exception.getStatus() - : HttpStatus.INTERNAL_SERVER_ERROR + : HttpStatus.INTERNAL_SERVER_ERROR; const message = exception instanceof HttpException ? exception.getResponse() - : 'Internal Server Error' + : 'Internal Server Error'; this.logger.error({ type: exception instanceof HttpException ? exception.name : 'Error', @@ -45,13 +45,13 @@ export class HttpExceptionFilter implements ExceptionFilter { remoteAddress: request.ip, remotePort: request.socket.remotePort, }, - }) + }); response.status(status).json({ statusCode: status, timestamp: new Date().toISOString(), path: request.url, message: message, - }) + }); } } diff --git a/src/shared/types/chain.type.ts b/src/shared/types/chain.type.ts index 52792f5..8e3991c 100644 --- a/src/shared/types/chain.type.ts +++ b/src/shared/types/chain.type.ts @@ -1,5 +1,5 @@ -import { SUPPORTED_CHAINS } from '../constants/chain.constants' -import type { AccsDefaultParams } from '@lit-protocol/types' +import { SUPPORTED_CHAINS } from '../constants/chain.constants'; +import type { AccsDefaultParams } from '@lit-protocol/types'; -export type SupportedChainId = (typeof SUPPORTED_CHAINS)[number] -export type LitChain = AccsDefaultParams['chain'] +export type SupportedChainId = (typeof SUPPORTED_CHAINS)[number]; +export type LitChain = AccsDefaultParams['chain']; diff --git a/src/utils/crypto-utils.service.spec.ts b/src/utils/crypto-utils.service.spec.ts index 75cf152..9ff1ce3 100644 --- a/src/utils/crypto-utils.service.spec.ts +++ b/src/utils/crypto-utils.service.spec.ts @@ -1,46 +1,46 @@ // src/utils/crypto-utils.service.spec.ts -import { Test, TestingModule } from '@nestjs/testing' -import { CryptoUtilsService } from './crypto-utils.service' -import { EncodeUtilsService } from './encode-utils.service' +import { Test, TestingModule } from '@nestjs/testing'; +import { CryptoUtilsService } from './crypto-utils.service'; +import { EncodeUtilsService } from './encode-utils.service'; describe('CryptoUtilsService', () => { - let service: CryptoUtilsService + let service: CryptoUtilsService; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [CryptoUtilsService, EncodeUtilsService], - }).compile() + }).compile(); - service = module.get(CryptoUtilsService) - }) + service = module.get(CryptoUtilsService); + }); it('should be defined', () => { - expect(service).toBeDefined() - }) + expect(service).toBeDefined(); + }); it('generateState should return a 32-character hex string', () => { - const state = service.generateState() - expect(state).toHaveLength(32) - expect(typeof state).toBe('string') - }) + const state = service.generateState(); + expect(state).toHaveLength(32); + expect(typeof state).toBe('string'); + }); it('generateCodeVerifier should return a 64-character hex string', () => { - const verifier = service.generateCodeVerifier() - expect(verifier).toHaveLength(64) - expect(typeof verifier).toBe('string') - }) + const verifier = service.generateCodeVerifier(); + expect(verifier).toHaveLength(64); + expect(typeof verifier).toBe('string'); + }); it('generateCodeChallenge should produce a URL-safe base64-encoded SHA256 hash', () => { - const verifier = 'verifier' - const challenge = service.generateCodeChallenge(verifier) - expect(typeof challenge).toBe('string') - expect(challenge).not.toMatch(/[+/=]/) - }) + const verifier = 'verifier'; + const challenge = service.generateCodeChallenge(verifier); + expect(typeof challenge).toBe('string'); + expect(challenge).not.toMatch(/[+/=]/); + }); it('validateState should return true when states match', () => { - expect(service.validateState('state1', 'state1')).toBe(true) - }) + expect(service.validateState('state1', 'state1')).toBe(true); + }); it('validateState should return false when states do not match', () => { - expect(service.validateState('state1', 'state2')).toBe(false) - }) -}) + expect(service.validateState('state1', 'state2')).toBe(false); + }); +}); diff --git a/src/utils/crypto-utils.service.ts b/src/utils/crypto-utils.service.ts index 35f33a1..8b757a5 100644 --- a/src/utils/crypto-utils.service.ts +++ b/src/utils/crypto-utils.service.ts @@ -1,26 +1,26 @@ // src/utils/crypto-utils.service.ts -import { Injectable } from '@nestjs/common' -import * as crypto from 'crypto' -import { EncodeUtilsService } from './encode-utils.service' +import { Injectable } from '@nestjs/common'; +import * as crypto from 'crypto'; +import { EncodeUtilsService } from './encode-utils.service'; @Injectable() export class CryptoUtilsService { constructor(private readonly encodeUtils: EncodeUtilsService) {} generateState(length: number = 16): string { - return crypto.randomBytes(length).toString('hex') + return crypto.randomBytes(length).toString('hex'); } validateState(returnedState: string, expectedState: string): boolean { - return returnedState === expectedState + return returnedState === expectedState; } generateCodeVerifier(length: number = 32): string { - return crypto.randomBytes(length).toString('hex') + return crypto.randomBytes(length).toString('hex'); } generateCodeChallenge(verifier: string): string { - const hash = crypto.createHash('sha256').update(verifier).digest() - return this.encodeUtils.base64UrlEncode(hash) + const hash = crypto.createHash('sha256').update(verifier).digest(); + return this.encodeUtils.base64UrlEncode(hash); } } diff --git a/src/utils/data-utils.service.spec.ts b/src/utils/data-utils.service.spec.ts index a8c5cd7..d80a29d 100644 --- a/src/utils/data-utils.service.spec.ts +++ b/src/utils/data-utils.service.spec.ts @@ -1,27 +1,27 @@ -import { Test, TestingModule } from '@nestjs/testing' -import { DataUtilsService } from './data-utils.service' +import { Test, TestingModule } from '@nestjs/testing'; +import { DataUtilsService } from './data-utils.service'; describe('DataUtilsService', () => { - let service: DataUtilsService + let service: DataUtilsService; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [DataUtilsService], - }).compile() + }).compile(); - service = module.get(DataUtilsService) - }) + service = module.get(DataUtilsService); + }); it('should be defined', () => { - expect(service).toBeDefined() - }) + expect(service).toBeDefined(); + }); describe('convertBigIntsToStrings', () => { it('should convert a bigint to a string', () => { - const input = BigInt(12345678901234567890n) - const result = service.convertBigIntsToStrings(input) - expect(result).toBe('12345678901234567890') - }) + const input = BigInt(12345678901234567890n); + const result = service.convertBigIntsToStrings(input); + expect(result).toBe('12345678901234567890'); + }); it('should convert bigints in an object to strings', () => { const input = { @@ -29,44 +29,44 @@ describe('DataUtilsService', () => { b: BigInt(456), c: 'string value', d: 789, - } + }; const expected = { a: '123', b: '456', c: 'string value', d: 789, - } - const result = service.convertBigIntsToStrings(input) - expect(result).toEqual(expected) - }) + }; + const result = service.convertBigIntsToStrings(input); + expect(result).toEqual(expected); + }); it('should convert bigints in an array to strings', () => { - const input = [BigInt(123), BigInt(456), 'string value', 789] - const expected = ['123', '456', 'string value', 789] - const result = service.convertBigIntsToStrings(input) - expect(result).toEqual(expected) - }) + const input = [BigInt(123), BigInt(456), 'string value', 789]; + const expected = ['123', '456', 'string value', 789]; + const result = service.convertBigIntsToStrings(input); + expect(result).toEqual(expected); + }); it('should handle nested objects and arrays', () => { const input = { a: [BigInt(123), { b: BigInt(456) }], c: { d: [BigInt(789)] }, - } + }; const expected = { a: ['123', { b: '456' }], c: { d: ['789'] }, - } - const result = service.convertBigIntsToStrings(input) - expect(result).toEqual(expected) - }) - }) + }; + const result = service.convertBigIntsToStrings(input); + expect(result).toEqual(expected); + }); + }); describe('convertStringsToBigInts', () => { it('should convert a string to a bigint', () => { - const input = '12345678901234567890' - const result = service.convertStringsToBigInts(input) - expect(result).toBe(BigInt(input)) - }) + const input = '12345678901234567890'; + const result = service.convertStringsToBigInts(input); + expect(result).toBe(BigInt(input)); + }); it('should convert strings in an object to bigints', () => { const input = { @@ -74,50 +74,50 @@ describe('DataUtilsService', () => { b: '456', c: 'string value', d: 789, - } + }; const expected = { a: BigInt(123), b: BigInt(456), c: 'string value', d: 789, - } - const result = service.convertStringsToBigInts(input) - expect(result).toEqual(expected) - }) + }; + const result = service.convertStringsToBigInts(input); + expect(result).toEqual(expected); + }); it('should convert strings in an array to bigints', () => { - const input = ['123', '456', 'string value', 789] - const expected = [BigInt(123), BigInt(456), 'string value', 789] - const result = service.convertStringsToBigInts(input) - expect(result).toEqual(expected) - }) + const input = ['123', '456', 'string value', 789]; + const expected = [BigInt(123), BigInt(456), 'string value', 789]; + const result = service.convertStringsToBigInts(input); + expect(result).toEqual(expected); + }); it('should handle nested objects and arrays', () => { const input = { a: ['123', { b: '456' }], c: { d: ['789'] }, - } + }; const expected = { a: [BigInt(123), { b: BigInt(456) }], c: { d: [BigInt(789)] }, - } - const result = service.convertStringsToBigInts(input) - expect(result).toEqual(expected) - }) + }; + const result = service.convertStringsToBigInts(input); + expect(result).toEqual(expected); + }); it('should ignore non-numeric strings', () => { const input = { a: 'not a number', b: '1234', c: [BigInt(5678), '9000'], - } + }; const expected = { a: 'not a number', b: BigInt(1234), c: [BigInt(5678), BigInt(9000)], - } - const result = service.convertStringsToBigInts(input) - expect(result).toEqual(expected) - }) - }) -}) + }; + const result = service.convertStringsToBigInts(input); + expect(result).toEqual(expected); + }); + }); +}); diff --git a/src/utils/data-utils.service.ts b/src/utils/data-utils.service.ts index 2094623..31def83 100644 --- a/src/utils/data-utils.service.ts +++ b/src/utils/data-utils.service.ts @@ -1,13 +1,13 @@ -import { Injectable } from '@nestjs/common' +import { Injectable } from '@nestjs/common'; @Injectable() export class DataUtilsService { convertBigIntsToStrings = (obj: any): any => { if (typeof obj === 'bigint') { - return obj.toString() + return obj.toString(); } if (Array.isArray(obj)) { - return obj.map(this.convertBigIntsToStrings) + return obj.map(this.convertBigIntsToStrings); } if (typeof obj === 'object' && obj !== null) { return Object.fromEntries( @@ -15,17 +15,17 @@ export class DataUtilsService { k, this.convertBigIntsToStrings(v), ]) - ) + ); } - return obj - } + return obj; + }; convertStringsToBigInts = (obj: any): any => { if (typeof obj === 'string' && /^[0-9]+$/.test(obj)) { - return BigInt(obj) + return BigInt(obj); } if (Array.isArray(obj)) { - return obj.map(this.convertStringsToBigInts) + return obj.map(this.convertStringsToBigInts); } if (typeof obj === 'object' && obj !== null) { return Object.fromEntries( @@ -33,8 +33,8 @@ export class DataUtilsService { k, this.convertStringsToBigInts(v), ]) - ) + ); } - return obj - } + return obj; + }; } diff --git a/src/utils/encode-utils.service.spec.ts b/src/utils/encode-utils.service.spec.ts index 700fc7e..d23c5d4 100644 --- a/src/utils/encode-utils.service.spec.ts +++ b/src/utils/encode-utils.service.spec.ts @@ -1,30 +1,30 @@ // src/utils/encode-utils.service.spec.ts -import { Test, TestingModule } from '@nestjs/testing' -import { EncodeUtilsService } from './encode-utils.service' +import { Test, TestingModule } from '@nestjs/testing'; +import { EncodeUtilsService } from './encode-utils.service'; describe('EncodeUtilsService', () => { - let service: EncodeUtilsService + let service: EncodeUtilsService; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [EncodeUtilsService], - }).compile() + }).compile(); - service = module.get(EncodeUtilsService) - }) + service = module.get(EncodeUtilsService); + }); it('should be defined', () => { - expect(service).toBeDefined() - }) + expect(service).toBeDefined(); + }); it('should encode buffer to URL-safe base64 string', () => { - const testBuffer = Buffer.from('NestJS Testing') + const testBuffer = Buffer.from('NestJS Testing'); const expectedResult = 'TmVzdEpTIFRlc3Rpbmc' .replace(/\+/g, '-') .replace(/\//g, '_') - .replace(/=+$/, '') + .replace(/=+$/, ''); - const result = service.base64UrlEncode(testBuffer) + const result = service.base64UrlEncode(testBuffer); - expect(result).toBe(expectedResult) - }) -}) + expect(result).toBe(expectedResult); + }); +}); diff --git a/src/utils/encode-utils.service.ts b/src/utils/encode-utils.service.ts index 03924f0..54aa4eb 100644 --- a/src/utils/encode-utils.service.ts +++ b/src/utils/encode-utils.service.ts @@ -1,5 +1,5 @@ // src/utils/encode-utils.service.ts -import { Injectable } from '@nestjs/common' +import { Injectable } from '@nestjs/common'; @Injectable() export class EncodeUtilsService { @@ -8,6 +8,6 @@ export class EncodeUtilsService { .toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') - .replace(/=+$/, '') + .replace(/=+$/, ''); } } diff --git a/src/utils/ethers.utils.service.spec.ts b/src/utils/ethers.utils.service.spec.ts index 4d7df4a..1bb82e8 100644 --- a/src/utils/ethers.utils.service.spec.ts +++ b/src/utils/ethers.utils.service.spec.ts @@ -1,37 +1,37 @@ -import { Test, TestingModule } from '@nestjs/testing' -import { EthersUtilsService } from './ethers.utils.service' -import { JsonRpcProvider, Wallet } from 'ethers' +import { Test, TestingModule } from '@nestjs/testing'; +import { EthersUtilsService } from './ethers.utils.service'; +import { JsonRpcProvider, Wallet } from 'ethers'; const mockPrivateKey = - '0x0123456789012345678901234567890123456789012345678901234567890123' -const mockRPCURL = 'https://ethereum-sepolia-rpc.publicnode.com' + '0x0123456789012345678901234567890123456789012345678901234567890123'; +const mockRPCURL = 'https://ethereum-sepolia-rpc.publicnode.com'; describe('EthersUtilsService', () => { - let service: EthersUtilsService + let service: EthersUtilsService; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [EthersUtilsService], - }).compile() + }).compile(); - service = module.get(EthersUtilsService) - }) + service = module.get(EthersUtilsService); + }); it('should be defined', () => { - expect(service).toBeDefined() - }) + expect(service).toBeDefined(); + }); describe('getProvider', () => { it('should return a provider for given rpcURL and privateKey', () => { - const provider = service.getProvider(mockRPCURL) - expect(provider).toBeInstanceOf(JsonRpcProvider) - }) - }) + const provider = service.getProvider(mockRPCURL); + expect(provider).toBeInstanceOf(JsonRpcProvider); + }); + }); describe('getSigner', () => { it('should return a signer for given rpcURL and privateKey', () => { - const signer = service.getSigner(mockRPCURL, mockPrivateKey) - expect(signer).toBeInstanceOf(Wallet) - expect(signer.provider).toBeInstanceOf(JsonRpcProvider) - }) - }) -}) + const signer = service.getSigner(mockRPCURL, mockPrivateKey); + expect(signer).toBeInstanceOf(Wallet); + expect(signer.provider).toBeInstanceOf(JsonRpcProvider); + }); + }); +}); diff --git a/src/utils/ethers.utils.service.ts b/src/utils/ethers.utils.service.ts index e9138b6..855cfa6 100644 --- a/src/utils/ethers.utils.service.ts +++ b/src/utils/ethers.utils.service.ts @@ -1,13 +1,13 @@ -import { Injectable } from '@nestjs/common' -import { JsonRpcProvider, Wallet, Signer } from 'ethers' +import { Injectable } from '@nestjs/common'; +import { JsonRpcProvider, Wallet, Signer } from 'ethers'; @Injectable() export class EthersUtilsService { getProvider(rpcURL: string) { - return new JsonRpcProvider(rpcURL) + return new JsonRpcProvider(rpcURL); } getSigner(rpcURL: string, privateKey: string): Signer { - return new Wallet(privateKey, this.getProvider(rpcURL)) + return new Wallet(privateKey, this.getProvider(rpcURL)); } } diff --git a/src/utils/utils.module.ts b/src/utils/utils.module.ts index deda26d..f097e0c 100644 --- a/src/utils/utils.module.ts +++ b/src/utils/utils.module.ts @@ -1,9 +1,9 @@ -import { Module } from '@nestjs/common' -import { CryptoUtilsService } from './crypto-utils.service' -import { EncodeUtilsService } from './encode-utils.service' -import { ViemUtilsService } from './viem.utils.service' -import { DataUtilsService } from './data-utils.service' -import { EthersUtilsService } from './ethers.utils.service' +import { Module } from '@nestjs/common'; +import { CryptoUtilsService } from './crypto-utils.service'; +import { EncodeUtilsService } from './encode-utils.service'; +import { ViemUtilsService } from './viem.utils.service'; +import { DataUtilsService } from './data-utils.service'; +import { EthersUtilsService } from './ethers.utils.service'; @Module({ providers: [ diff --git a/src/utils/viem.service.spec.ts b/src/utils/viem.service.spec.ts index 98439af..cc13187 100644 --- a/src/utils/viem.service.spec.ts +++ b/src/utils/viem.service.spec.ts @@ -1,52 +1,52 @@ -import { Test, TestingModule } from '@nestjs/testing' -import { ViemUtilsService } from './viem.utils.service' -import { SUPPORTED_CHAINS } from '../shared/constants/chain.constants' -import * as chains from 'viem/chains' +import { Test, TestingModule } from '@nestjs/testing'; +import { ViemUtilsService } from './viem.utils.service'; +import { SUPPORTED_CHAINS } from '../shared/constants/chain.constants'; +import * as chains from 'viem/chains'; describe('ViemService', () => { - let service: ViemUtilsService + let service: ViemUtilsService; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ViemUtilsService], - }).compile() + }).compile(); - service = module.get(ViemUtilsService) - }) + service = module.get(ViemUtilsService); + }); it('should be defined', () => { - expect(service).toBeDefined() - }) + expect(service).toBeDefined(); + }); describe('idToChain', () => { it('should return the correct chain for supported chain IDs', () => { for (const chainId of SUPPORTED_CHAINS) { const expectedChain = Object.values(chains).find( (chain) => chain.id === chainId - ) - const result = service.idToChain(chainId) - expect(result).toBe(expectedChain) + ); + const result = service.idToChain(chainId); + expect(result).toBe(expectedChain); } - }) + }); it('should return undefined for an unknown chain ID', () => { - const chain = service.idToChain(9999) - expect(chain).toBeUndefined() - }) - }) + const chain = service.idToChain(9999); + expect(chain).toBeUndefined(); + }); + }); describe('getPublicClient', () => { it('should return a public client for supported chain IDs', () => { for (const chainId of SUPPORTED_CHAINS) { - const client = service.getPublicClient(chainId) - expect(client).toBeDefined() + const client = service.getPublicClient(chainId); + expect(client).toBeDefined(); } - }) + }); it('should return undefined for unsupported chain ID', () => { - const chainId = 9999 - const client = service.getPublicClient(chainId) - expect(client).toBeUndefined() - }) - }) -}) + const chainId = 9999; + const client = service.getPublicClient(chainId); + expect(client).toBeUndefined(); + }); + }); +}); diff --git a/src/utils/viem.utils.service.ts b/src/utils/viem.utils.service.ts index ca4f1ed..22e31e3 100644 --- a/src/utils/viem.utils.service.ts +++ b/src/utils/viem.utils.service.ts @@ -1,41 +1,41 @@ -import { Injectable } from '@nestjs/common' -import { SUPPORTED_CHAINS } from '../shared/constants/chain.constants' -import { SupportedChainId } from '../shared/types/chain.type' -import { createPublicClient, http, Client } from 'viem' -import * as chains from 'viem/chains' -import { Chain } from 'viem/chains' +import { Injectable } from '@nestjs/common'; +import { SUPPORTED_CHAINS } from '../shared/constants/chain.constants'; +import { SupportedChainId } from '../shared/types/chain.type'; +import { createPublicClient, http, Client } from 'viem'; +import * as chains from 'viem/chains'; +import { Chain } from 'viem/chains'; @Injectable() export class ViemUtilsService { - private publicClients: Map + private publicClients: Map; constructor() { - this.publicClients = new Map>() - this.setPublicClients() + this.publicClients = new Map>(); + this.setPublicClients(); } private setPublicClients() { for (const chainId of SUPPORTED_CHAINS) { - const chain = this.idToChain(chainId) + const chain = this.idToChain(chainId); if (chain) { const client = createPublicClient({ chain, transport: http(), - }) - this.publicClients.set(chainId, client) + }); + this.publicClients.set(chainId, client); } } } getPublicClient(chainId: SupportedChainId) { - return this.publicClients.get(chainId) + return this.publicClients.get(chainId); } idToChain(chainId: SupportedChainId): Chain { for (const chain of Object.values(chains)) { if ('id' in chain) { if (chain.id === chainId) { - return chain + return chain; } } } From 44a32f521399a4507d3c25db5454ad557515c013 Mon Sep 17 00:00:00 2001 From: Behzad-rabiei Date: Wed, 8 Jan 2025 15:40:26 +0100 Subject: [PATCH 3/6] feat: add discourse verification module --- .../discourse-verification.controller.spec.ts | 59 ++++++++++++ .../discourse-verification.controller.ts | 63 +++++++++++++ .../discourse-verification.module.ts | 14 +++ .../discourse-verification.service.spec.ts | 58 ++++++++++++ .../discourse-verification.service.ts | 91 +++++++++++++++++++ ...enerate-verification-token-response.dto.ts | 11 +++ .../dto/generate-verification-token.dto.ts | 19 ++++ .../dto/verify-response.dto.ts | 11 +++ src/discourse-verification/dto/verify.dto.ts | 24 +++++ 9 files changed, 350 insertions(+) create mode 100644 src/discourse-verification/discourse-verification.controller.spec.ts create mode 100644 src/discourse-verification/discourse-verification.controller.ts create mode 100644 src/discourse-verification/discourse-verification.module.ts create mode 100644 src/discourse-verification/discourse-verification.service.spec.ts create mode 100644 src/discourse-verification/discourse-verification.service.ts create mode 100644 src/discourse-verification/dto/generate-verification-token-response.dto.ts create mode 100644 src/discourse-verification/dto/generate-verification-token.dto.ts create mode 100644 src/discourse-verification/dto/verify-response.dto.ts create mode 100644 src/discourse-verification/dto/verify.dto.ts diff --git a/src/discourse-verification/discourse-verification.controller.spec.ts b/src/discourse-verification/discourse-verification.controller.spec.ts new file mode 100644 index 0000000..5987d71 --- /dev/null +++ b/src/discourse-verification/discourse-verification.controller.spec.ts @@ -0,0 +1,59 @@ +import { AxiosResponse } from 'axios'; +import { of } from 'rxjs'; + +import { HttpService } from '@nestjs/axios'; +import { Test, TestingModule } from '@nestjs/testing'; + +import { JwtService } from '../jwt/jwt.service'; +import { CryptoUtilsService } from '../utils/crypto-utils.service'; +import { DiscourseVerificationController } from './discourse-verification.controller'; +import { DiscourseVerificationService } from './discourse-verification.service'; + +describe('DiscourseVerificationController', () => { + let controller: DiscourseVerificationController; + const mockJwtService = { + generateAuthJwt: jest.fn().mockResolvedValue('mock-jwt'), + }; + const mockHttpService = { + post: jest.fn().mockImplementation(() => + of({ + data: { access_token: 'mock-access-token' }, + status: 200, + statusText: 'OK', + headers: {}, + config: {}, + } as AxiosResponse<{ access_token: string }>) + ), + get: jest.fn().mockImplementation(() => + of({ + data: { id: 'mock-id' }, + status: 200, + statusText: 'OK', + headers: {}, + config: {}, + } as AxiosResponse<{ id: string }>) + ), + }; + const mockCryptoService = { + validateState: jest.fn().mockReturnValue(true), + }; + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [DiscourseVerificationController], + providers: [ + { provide: HttpService, useValue: mockHttpService }, + { provide: CryptoUtilsService, useValue: mockCryptoService }, + { provide: JwtService, useValue: mockJwtService }, + DiscourseVerificationService, + ], + }).compile(); + + controller = module.get( + DiscourseVerificationController + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/discourse-verification/discourse-verification.controller.ts b/src/discourse-verification/discourse-verification.controller.ts new file mode 100644 index 0000000..fb547bb --- /dev/null +++ b/src/discourse-verification/discourse-verification.controller.ts @@ -0,0 +1,63 @@ +import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; +import { + ApiBadRequestResponse, + ApiOkResponse, + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; + +import { DiscourseVerificationService } from './discourse-verification.service'; +import { GenerateVerificationTokenResponseDto } from './dto/generate-verification-token-response.dto'; +import { GenerateVerificationTokenDto } from './dto/generate-verification-token.dto'; +import { VerifyResponseDto } from './dto/verify-response.dto'; +import { VerifyDto } from './dto/verify.dto'; + +@ApiTags('Discourse Verification') +@Controller('discourse-verification') +export class DiscourseVerificationController { + constructor( + private readonly discourseVerificationService: DiscourseVerificationService + ) {} + + @Post('token') + @ApiOperation({ + summary: 'Generate discourse verification token using siweJwt', + }) + @ApiOkResponse({ + description: 'Discourse verification token generated successfully.', + type: GenerateVerificationTokenResponseDto, + }) + @ApiBadRequestResponse({ + description: 'Generate discourse verification token failed.', + }) + @HttpCode(HttpStatus.OK) + async GenerateVerificationToken( + @Body() generateVerificationTokenDto: GenerateVerificationTokenDto + ): Promise { + return { + verificationJwt: + await this.discourseVerificationService.generateVerificationToken( + generateVerificationTokenDto + ), + }; + } + + @Post('verify') + @ApiOperation({ + summary: 'Verify discourse user using verificationJwt and siweJwt', + }) + @ApiOkResponse({ + description: 'Discourse user Verified.', + type: VerifyResponseDto, + }) + @ApiBadRequestResponse({ description: 'SIWE verification failed.' }) + @HttpCode(HttpStatus.OK) + async verify(@Body() verifyDto: VerifyDto): Promise { + return { + discourseJwt: + await this.discourseVerificationService.verifyDiscourseTopic( + verifyDto + ), + }; + } +} diff --git a/src/discourse-verification/discourse-verification.module.ts b/src/discourse-verification/discourse-verification.module.ts new file mode 100644 index 0000000..6a16615 --- /dev/null +++ b/src/discourse-verification/discourse-verification.module.ts @@ -0,0 +1,14 @@ +import { HttpModule } from '@nestjs/axios'; +import { Module } from '@nestjs/common'; + +import { JwtModule } from '../jwt/jwt.module'; +import { UtilsModule } from '../utils/utils.module'; +import { DiscourseVerificationController } from './discourse-verification.controller'; +import { DiscourseVerificationService } from './discourse-verification.service'; + +@Module({ + imports: [JwtModule, UtilsModule, HttpModule], + controllers: [DiscourseVerificationController], + providers: [DiscourseVerificationService], +}) +export class DiscourseVerificationModule {} diff --git a/src/discourse-verification/discourse-verification.service.spec.ts b/src/discourse-verification/discourse-verification.service.spec.ts new file mode 100644 index 0000000..26a7473 --- /dev/null +++ b/src/discourse-verification/discourse-verification.service.spec.ts @@ -0,0 +1,58 @@ +import { AxiosResponse } from 'axios'; +import { of } from 'rxjs'; + +import { HttpService } from '@nestjs/axios'; +import { Test, TestingModule } from '@nestjs/testing'; + +import { JwtService } from '../jwt/jwt.service'; +import { CryptoUtilsService } from '../utils/crypto-utils.service'; +import { DiscourseVerificationService } from './discourse-verification.service'; + +describe('DiscourseVerificationService', () => { + let service: DiscourseVerificationService; + const mockJwtService = { + generateAuthJwt: jest.fn().mockResolvedValue('mock-jwt'), + }; + + const mockHttpService = { + post: jest.fn().mockImplementation(() => + of({ + data: { access_token: 'mock-access-token' }, + status: 200, + statusText: 'OK', + headers: {}, + config: {}, + } as AxiosResponse<{ access_token: string }>) + ), + get: jest.fn().mockImplementation(() => + of({ + data: { id: 'mock-id' }, + status: 200, + statusText: 'OK', + headers: {}, + config: {}, + } as AxiosResponse<{ id: string }>) + ), + }; + const mockCryptoService = { + validateState: jest.fn().mockReturnValue(true), + }; + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + DiscourseVerificationService, + { provide: HttpService, useValue: mockHttpService }, + { provide: CryptoUtilsService, useValue: mockCryptoService }, + { provide: JwtService, useValue: mockJwtService }, + ], + }).compile(); + + service = module.get( + DiscourseVerificationService + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/discourse-verification/discourse-verification.service.ts b/src/discourse-verification/discourse-verification.service.ts new file mode 100644 index 0000000..58568e5 --- /dev/null +++ b/src/discourse-verification/discourse-verification.service.ts @@ -0,0 +1,91 @@ +import { lastValueFrom } from 'rxjs'; +import { VerificationJwtPayload } from 'src/jwt/types/jwt-payload.type'; + +import { HttpService } from '@nestjs/axios'; +import { BadRequestException, Injectable } from '@nestjs/common'; + +import { JwtService } from '../jwt/jwt.service'; +import { CryptoUtilsService } from '../utils/crypto-utils.service'; +import { GenerateVerificationTokenDto } from './dto/generate-verification-token.dto'; +import { VerifyDto } from './dto/verify.dto'; + +@Injectable() +export class DiscourseVerificationService { + constructor( + private readonly jwtService: JwtService, + private readonly cryptoUtilsService: CryptoUtilsService, + private readonly httpService: HttpService + ) {} + + async generateVerificationToken( + generateVerificationTokenDto: GenerateVerificationTokenDto + ): Promise { + const { siweJwt } = generateVerificationTokenDto; + await this.jwtService.validateToken(siweJwt); + const verificationCode = + this.cryptoUtilsService.generateVerificationCode(); + return this.jwtService.generateVerificationToken(verificationCode); + } + + async verifyDiscourseTopic(verifyDto: VerifyDto): Promise { + const topicUrl = this.normalizeTopicUrl(verifyDto.topicUrl); + const payload = (await this.jwtService.validateToken( + verifyDto.verificationJwt + )) as VerificationJwtPayload; + + const topicData = await this.fetchTopicData(topicUrl); + this.validateTopicData(topicData, payload); + + const firstPost = topicData?.post_stream?.posts?.[0]; + if (!firstPost?.user_id) { + throw new BadRequestException( + 'Could not determine user ID from the topic' + ); + } + const urlObj = new URL(verifyDto.topicUrl); + const baseURL = urlObj.origin; + + const discourseJwt = await this.jwtService.generateAuthJwt( + firstPost?.user_id, + 'discourse', + { baseURL } + ); + return discourseJwt; + } + + private normalizeTopicUrl(url: string): string { + if (!url.endsWith('.json')) { + return url.endsWith('/') ? `${url}json` : `${url}.json`; + } + return url; + } + + private async fetchTopicData(url: string): Promise { + try { + const response = await lastValueFrom(this.httpService.get(url)); + return response.data; + } catch (error) { + throw new BadRequestException( + 'Failed to retrieve Discourse topic JSON' + ); + } + } + + private validateTopicData( + topicData: any, + jwtPayload: VerificationJwtPayload + ): void { + const posts = topicData?.post_stream?.posts || []; + if (!posts.length) { + throw new BadRequestException('No posts found in the topic'); + } + + const firstPost = posts[0]; + const content = firstPost?.cooked || ''; + if (!content.includes(jwtPayload.code)) { + throw new BadRequestException( + 'Verification code not found in topic content' + ); + } + } +} diff --git a/src/discourse-verification/dto/generate-verification-token-response.dto.ts b/src/discourse-verification/dto/generate-verification-token-response.dto.ts new file mode 100644 index 0000000..d9da13a --- /dev/null +++ b/src/discourse-verification/dto/generate-verification-token-response.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class GenerateVerificationTokenResponseDto { + @ApiProperty({ + description: + 'Generated verification JWT token to embed in a Discourse topic', + example: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2RlIjoiMjMxMjQ... (truncated)', + }) + verificationJwt: string; +} diff --git a/src/discourse-verification/dto/generate-verification-token.dto.ts b/src/discourse-verification/dto/generate-verification-token.dto.ts new file mode 100644 index 0000000..e282978 --- /dev/null +++ b/src/discourse-verification/dto/generate-verification-token.dto.ts @@ -0,0 +1,19 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +import { ApiProperty } from '@nestjs/swagger'; + +import { AUTH_PROVIDERS } from '../../auth/constants/provider.constants'; +import { JwtProvider } from '../../shared/decorators/jwt-provider.decorator'; + +export class GenerateVerificationTokenDto { + @ApiProperty({ + description: 'The siwe JWT', + example: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6', + required: true, + }) + @IsString() + @IsNotEmpty() + @JwtProvider(AUTH_PROVIDERS.SIWE) + readonly siweJwt: string; +} diff --git a/src/discourse-verification/dto/verify-response.dto.ts b/src/discourse-verification/dto/verify-response.dto.ts new file mode 100644 index 0000000..30b9ba5 --- /dev/null +++ b/src/discourse-verification/dto/verify-response.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class VerifyResponseDto { + @ApiProperty({ + description: + 'Discourse JWT that can be used for authenticated actions on the Discourse instance', + example: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjMsImJhc2VVUkwiOiJodHRwczovL2ZvcnVtcy5leGFtcGxlLmNvbSIsImlhdCI6MTY5MTMxMjAwMH0.x4dF-KO9drgBbLj197UQVHev4dgd0DcNEkN8bsuEPnY', + }) + discourseJwt: string; +} diff --git a/src/discourse-verification/dto/verify.dto.ts b/src/discourse-verification/dto/verify.dto.ts new file mode 100644 index 0000000..a12c8ec --- /dev/null +++ b/src/discourse-verification/dto/verify.dto.ts @@ -0,0 +1,24 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +import { ApiProperty } from '@nestjs/swagger'; + +export class VerifyDto { + @ApiProperty({ + description: 'the discourse topic url.', + example: 'https://forum.arbitrum.foundation', + required: true, + }) + @IsString() + @IsNotEmpty() + topicUrl: string; + + @ApiProperty({ + description: 'The verification JWT.', + example: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkxasewrOiIxMjM0NTY3ODkwIiwibmFtZSI6', + required: true, + }) + @IsString() + @IsNotEmpty() + verificationJwt: string; +} From d461aebb9e6094ced15b4a15f09f0cbb78bc6513 Mon Sep 17 00:00:00 2001 From: Behzad-rabiei Date: Wed, 8 Jan 2025 15:40:39 +0100 Subject: [PATCH 4/6] refactor: clean the code --- src/app.module.ts | 2 ++ src/auth-discord/auth-discord.module.ts | 3 ++- src/auth-siwe/auth-siwe.controller.spec.ts | 14 ++++++------ src/auth-siwe/auth-siwe.module.ts | 3 +-- src/auth/auth.module.ts | 8 +++---- src/auth/oAuth.service.spec.ts | 10 ++++----- src/eas/eas.service.spec.ts | 4 ++-- src/eas/eas.service.ts | 7 +++++- src/jwt/config/jwt.config.ts | 4 ++-- src/jwt/jwt.module.ts | 2 -- src/jwt/jwt.service.spec.ts | 18 ++++++++++++++- src/jwt/jwt.service.ts | 2 -- src/jwt/types/jwt-payload.type.ts | 2 +- src/utils/config/crypto-utils.config.ts | 4 ++++ src/utils/crypto-utils.service.ts | 26 ++++++++++++++++++++-- 15 files changed, 77 insertions(+), 32 deletions(-) create mode 100644 src/utils/config/crypto-utils.config.ts diff --git a/src/app.module.ts b/src/app.module.ts index c93a067..96e15c9 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -10,6 +10,7 @@ import { pinoConfig } from './config/pino.config'; import { LitModule } from './lit/lit.module'; import { EasModule } from './eas/eas.module'; import { JwtModule } from './jwt/jwt.module'; +import { DiscourseVerificationModule } from './discourse-verification/discourse-verification.module'; @Module({ imports: [ @@ -30,6 +31,7 @@ import { JwtModule } from './jwt/jwt.module'; LitModule, EasModule, JwtModule, + DiscourseVerificationModule, ], controllers: [], providers: [], diff --git a/src/auth-discord/auth-discord.module.ts b/src/auth-discord/auth-discord.module.ts index 6bb7cf5..afa86e9 100644 --- a/src/auth-discord/auth-discord.module.ts +++ b/src/auth-discord/auth-discord.module.ts @@ -1,11 +1,12 @@ import { Module } from '@nestjs/common'; import { AuthModule } from '../auth/auth.module'; +import { JwtModule } from '../jwt/jwt.module'; import { UtilsModule } from '../utils/utils.module'; import { AuthDiscordController } from './auth-discord.controller'; @Module({ - imports: [AuthModule, UtilsModule], + imports: [AuthModule, UtilsModule, JwtModule], providers: [], controllers: [AuthDiscordController], }) diff --git a/src/auth-siwe/auth-siwe.controller.spec.ts b/src/auth-siwe/auth-siwe.controller.spec.ts index a1f012b..e2cc982 100644 --- a/src/auth-siwe/auth-siwe.controller.spec.ts +++ b/src/auth-siwe/auth-siwe.controller.spec.ts @@ -3,7 +3,7 @@ import { parseSiweMessage } from 'viem/siwe'; import { Test, TestingModule } from '@nestjs/testing'; import { AUTH_PROVIDERS } from '../auth/constants/provider.constants'; -import { AuthService } from '../auth/jwt.service'; +import { JwtService } from '../jwt/jwt.service'; import { AuthSiweController } from './auth-siwe.controller'; import { VerifySiweDto } from './dto/verify-siwe.dto'; import { SiweService } from './siwe.service'; @@ -15,7 +15,7 @@ jest.mock('viem/siwe', () => ({ describe('AuthSiweController', () => { let controller: AuthSiweController; let siweService: SiweService; - let authService: AuthService; + let jwtService: JwtService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -29,9 +29,9 @@ describe('AuthSiweController', () => { }, }, { - provide: AuthService, + provide: JwtService, useValue: { - generateJwt: jest.fn(), + generateAuthJwt: jest.fn(), }, }, ], @@ -39,7 +39,7 @@ describe('AuthSiweController', () => { controller = module.get(AuthSiweController); siweService = module.get(SiweService); - authService = module.get(AuthService); + jwtService = module.get(JwtService); }); it('should be defined', () => { @@ -68,7 +68,7 @@ describe('AuthSiweController', () => { jest.spyOn(siweService, 'verifySiweMessage').mockResolvedValue( undefined ); - jest.spyOn(authService, 'generateJwt').mockResolvedValue(jwt); + jest.spyOn(jwtService, 'generateAuthJwt').mockResolvedValue(jwt); (parseSiweMessage as jest.Mock).mockReturnValue({ address }); const result = await controller.verifySiwe(verifySiweDto); @@ -79,7 +79,7 @@ describe('AuthSiweController', () => { verifySiweDto.signature, verifySiweDto.chainId ); - expect(authService.generateJwt).toHaveBeenCalledWith( + expect(jwtService.generateAuthJwt).toHaveBeenCalledWith( address, AUTH_PROVIDERS.SIWE ); diff --git a/src/auth-siwe/auth-siwe.module.ts b/src/auth-siwe/auth-siwe.module.ts index 39e47b7..dbf22a7 100644 --- a/src/auth-siwe/auth-siwe.module.ts +++ b/src/auth-siwe/auth-siwe.module.ts @@ -1,13 +1,12 @@ import { Module } from '@nestjs/common'; -import { AuthModule } from '../auth/auth.module'; import { JwtModule } from '../jwt/jwt.module'; import { UtilsModule } from '../utils/utils.module'; import { AuthSiweController } from './auth-siwe.controller'; import { SiweService } from './siwe.service'; @Module({ - imports: [AuthModule, UtilsModule, JwtModule], + imports: [UtilsModule, JwtModule], controllers: [AuthSiweController], providers: [SiweService], }) diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index ee9e343..71d304e 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -3,14 +3,14 @@ import { UtilsModule } from 'src/utils/utils.module'; import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { JwtModule } from '@nestjs/jwt'; +import { JwtModule as NestJwtModule } from '@nestjs/jwt'; -import { JwtModule as customJwtModule } from '../jwt/jwt.module'; +import { JwtModule } from '../jwt/jwt.module'; import { OAuthService } from './oAuth.service'; @Module({ imports: [ - JwtModule.registerAsync({ + NestJwtModule.registerAsync({ useFactory: async (configService: ConfigService) => ({ secret: configService.get('jwt.secret'), }), @@ -18,7 +18,7 @@ import { OAuthService } from './oAuth.service'; }), HttpModule, UtilsModule, - customJwtModule, + JwtModule, ], providers: [OAuthService], exports: [OAuthService], diff --git a/src/auth/oAuth.service.spec.ts b/src/auth/oAuth.service.spec.ts index a805d8e..09c0a66 100644 --- a/src/auth/oAuth.service.spec.ts +++ b/src/auth/oAuth.service.spec.ts @@ -6,9 +6,9 @@ import { HttpService } from '@nestjs/axios'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; +import { JwtService } from '../jwt/jwt.service'; import { CryptoUtilsService } from '../utils/crypto-utils.service'; import { AUTH_PROVIDERS } from './constants/provider.constants'; -import { AuthService } from './jwt.service'; import { OAuthService } from './oAuth.service'; describe('OAuthService', () => { @@ -47,8 +47,8 @@ describe('OAuthService', () => { }), }; - const mockAuthService = { - generateJwt: jest.fn().mockResolvedValue('mock-jwt'), + const mockJwtService = { + generateAuthJwt: jest.fn().mockResolvedValue('mock-jwt'), }; const mockCryptoService = { @@ -60,7 +60,7 @@ describe('OAuthService', () => { imports: [LoggerModule.forRoot()], providers: [ OAuthService, - { provide: AuthService, useValue: mockAuthService }, + { provide: JwtService, useValue: mockJwtService }, { provide: HttpService, useValue: mockHttpService }, { provide: ConfigService, useValue: mockConfigService }, { provide: CryptoUtilsService, useValue: mockCryptoService }, @@ -124,7 +124,7 @@ describe('OAuthService', () => { 'mock-state', 'mock-state' ); - expect(mockAuthService.generateJwt).toHaveBeenCalledWith( + expect(mockJwtService.generateAuthJwt).toHaveBeenCalledWith( 'user-id', AUTH_PROVIDERS.GOOGLE ); diff --git a/src/eas/eas.service.spec.ts b/src/eas/eas.service.spec.ts index 0aa7371..17da3cc 100644 --- a/src/eas/eas.service.spec.ts +++ b/src/eas/eas.service.spec.ts @@ -6,7 +6,7 @@ import { generatePrivateKey } from 'viem/accounts'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { AuthService } from '../auth/jwt.service'; +import { JwtService } from '../jwt/jwt.service'; import { LitService } from '../lit/lit.service'; import { DataUtilsService } from '../utils/data-utils.service'; import { EthersUtilsService } from '../utils/ethers.utils.service'; @@ -30,7 +30,7 @@ describe('EasService', () => { providers: [ EasService, EthersUtilsService, - AuthService, + JwtService, LitService, DataUtilsService, { provide: ConfigService, useValue: mockConfigService }, diff --git a/src/eas/eas.service.ts b/src/eas/eas.service.ts index 2b5e8ff..ea349dc 100644 --- a/src/eas/eas.service.ts +++ b/src/eas/eas.service.ts @@ -148,12 +148,17 @@ export class EasService { const anyJwtPayload = (await this.jwtService.validateToken( anyJwt )) as AuthJwtPayload; - const key = generateHash(anyJwtPayload.sub, anyJwtPayload.provider); + const key = generateHash( + anyJwtPayload.sub, + anyJwtPayload.provider, + anyJwtPayload.metadata || {} + ); const secret = await this.litService.encryptToJson( chainId, { id: anyJwtPayload.sub, provider: anyJwtPayload.provider, + metadata: anyJwtPayload.metadata || {}, }, key, siweJwtPayload.sub as '0x${string}' diff --git a/src/jwt/config/jwt.config.ts b/src/jwt/config/jwt.config.ts index 93d3338..0978039 100644 --- a/src/jwt/config/jwt.config.ts +++ b/src/jwt/config/jwt.config.ts @@ -12,9 +12,9 @@ export default registerAs('jwt', () => ({ export const jwtConfigSchema = { JWT_SECRET: Joi.string().required().description('JWT secret'), JWT_AUTH_EXPIRATION_MINUTES: Joi.number() - .default(60) + .default(6000) .description('JWT expiration time in minutes for the auth'), JWT_VERIFICATION_MINUTES: Joi.number() - .default(10) + .default(1000) .description('JWT expiration time in minutes for the verification'), }; diff --git a/src/jwt/jwt.module.ts b/src/jwt/jwt.module.ts index 2234cae..3dd86f1 100644 --- a/src/jwt/jwt.module.ts +++ b/src/jwt/jwt.module.ts @@ -1,5 +1,3 @@ -// src/jwt/jwt.module.ts - import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; diff --git a/src/jwt/jwt.service.spec.ts b/src/jwt/jwt.service.spec.ts index 96d2719..040e87a 100644 --- a/src/jwt/jwt.service.spec.ts +++ b/src/jwt/jwt.service.spec.ts @@ -1,13 +1,29 @@ +import { LoggerModule, PinoLogger } from 'nestjs-pino'; + +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { JwtService } from './jwt.service'; describe('JwtService', () => { let service: JwtService; + let loggerMock: PinoLogger; + const mockConfigService = { + get: jest.fn((key: string) => { + if (key === 'jwt.secret') return 'test-value'; + return null; + }), + }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [JwtService], + imports: [LoggerModule.forRoot()], + + providers: [ + JwtService, + { provide: ConfigService, useValue: mockConfigService }, + { provide: PinoLogger, useValue: loggerMock }, + ], }).compile(); service = module.get(JwtService); diff --git a/src/jwt/jwt.service.ts b/src/jwt/jwt.service.ts index 0104391..24f20e3 100644 --- a/src/jwt/jwt.service.ts +++ b/src/jwt/jwt.service.ts @@ -1,5 +1,3 @@ -// src/jwt/jwt.service.ts - import * as jwt from 'jsonwebtoken'; import * as moment from 'moment'; import { InjectPinoLogger, PinoLogger } from 'nestjs-pino'; diff --git a/src/jwt/types/jwt-payload.type.ts b/src/jwt/types/jwt-payload.type.ts index 7e6b9fb..b2d6023 100644 --- a/src/jwt/types/jwt-payload.type.ts +++ b/src/jwt/types/jwt-payload.type.ts @@ -10,7 +10,7 @@ export interface VerificationJwtPayload extends BaseJwtPayload { } export interface AuthJwtPayload extends BaseJwtPayload { - provider?: string; + provider: string; metadata?: Record; } diff --git a/src/utils/config/crypto-utils.config.ts b/src/utils/config/crypto-utils.config.ts new file mode 100644 index 0000000..24a06ff --- /dev/null +++ b/src/utils/config/crypto-utils.config.ts @@ -0,0 +1,4 @@ +export const verificationConfig = { + length: 6, + allowedCharacters: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', +}; diff --git a/src/utils/crypto-utils.service.ts b/src/utils/crypto-utils.service.ts index 8b757a5..ef1ecf5 100644 --- a/src/utils/crypto-utils.service.ts +++ b/src/utils/crypto-utils.service.ts @@ -1,6 +1,8 @@ -// src/utils/crypto-utils.service.ts -import { Injectable } from '@nestjs/common'; import * as crypto from 'crypto'; + +import { Injectable } from '@nestjs/common'; + +import { verificationConfig } from './config/crypto-utils.config'; import { EncodeUtilsService } from './encode-utils.service'; @Injectable() @@ -23,4 +25,24 @@ export class CryptoUtilsService { const hash = crypto.createHash('sha256').update(verifier).digest(); return this.encodeUtils.base64UrlEncode(hash); } + + private generateRandomCode( + length: number, + allowedCharacters: string + ): string { + const charsLength = allowedCharacters.length; + const randomBytes = crypto.randomBytes(length); + let code = ''; + + for (let i = 0; i < length; i++) { + const randomIndex = randomBytes[i] % charsLength; + code += allowedCharacters.charAt(randomIndex); + } + return code; + } + + public generateVerificationCode(): string { + const { length, allowedCharacters } = verificationConfig; + return this.generateRandomCode(length, allowedCharacters); + } } From 76f6ea33e94f9d99e212004cf9e94e3aae0a4465 Mon Sep 17 00:00:00 2001 From: Behzad-rabiei Date: Wed, 8 Jan 2025 15:57:43 +0100 Subject: [PATCH 5/6] fix: fix the git leak issue --- .../dto/generate-verification-token-response.dto.ts | 2 +- src/discourse-verification/dto/verify-response.dto.ts | 2 +- src/discourse-verification/dto/verify.dto.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/discourse-verification/dto/generate-verification-token-response.dto.ts b/src/discourse-verification/dto/generate-verification-token-response.dto.ts index d9da13a..b94862c 100644 --- a/src/discourse-verification/dto/generate-verification-token-response.dto.ts +++ b/src/discourse-verification/dto/generate-verification-token-response.dto.ts @@ -5,7 +5,7 @@ export class GenerateVerificationTokenResponseDto { description: 'Generated verification JWT token to embed in a Discourse topic', example: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2RlIjoiMjMxMjQ... (truncated)', + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6', }) verificationJwt: string; } diff --git a/src/discourse-verification/dto/verify-response.dto.ts b/src/discourse-verification/dto/verify-response.dto.ts index 30b9ba5..0c22ed9 100644 --- a/src/discourse-verification/dto/verify-response.dto.ts +++ b/src/discourse-verification/dto/verify-response.dto.ts @@ -5,7 +5,7 @@ export class VerifyResponseDto { description: 'Discourse JWT that can be used for authenticated actions on the Discourse instance', example: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjMsImJhc2VVUkwiOiJodHRwczovL2ZvcnVtcy5leGFtcGxlLmNvbSIsImlhdCI6MTY5MTMxMjAwMH0.x4dF-KO9drgBbLj197UQVHev4dgd0DcNEkN8bsuEPnY', + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6', }) discourseJwt: string; } diff --git a/src/discourse-verification/dto/verify.dto.ts b/src/discourse-verification/dto/verify.dto.ts index a12c8ec..bfcd38c 100644 --- a/src/discourse-verification/dto/verify.dto.ts +++ b/src/discourse-verification/dto/verify.dto.ts @@ -15,7 +15,7 @@ export class VerifyDto { @ApiProperty({ description: 'The verification JWT.', example: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkxasewrOiIxMjM0NTY3ODkwIiwibmFtZSI6', + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6', required: true, }) @IsString() From 1920c7a09488d29ae609ed47736049cd73b8b143 Mon Sep 17 00:00:00 2001 From: Behzad-rabiei Date: Wed, 8 Jan 2025 16:01:12 +0100 Subject: [PATCH 6/6] chore: update err msg --- src/jwt/jwt.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jwt/jwt.service.ts b/src/jwt/jwt.service.ts index 24f20e3..bf886f6 100644 --- a/src/jwt/jwt.service.ts +++ b/src/jwt/jwt.service.ts @@ -36,7 +36,7 @@ export class JwtService { ) as JwtPayload; } catch (error) { this.logger.error(error, 'Failed to validate token'); - throw new UnauthorizedException(error.message); + throw new UnauthorizedException('Invalid token'); } }