From c27680980978f8bd6c15331c8f44dd3339e37af3 Mon Sep 17 00:00:00 2001 From: sujitaw Date: Wed, 10 Sep 2025 11:49:29 +0530 Subject: [PATCH 01/10] feat/api to fetch all sessions and delete session Signed-off-by: sujitaw --- .../api-gateway/src/authz/authz.controller.ts | 95 ++++++++++++++++++- apps/user/repositories/user.repository.ts | 18 ++++ apps/user/src/user.controller.ts | 12 ++- apps/user/src/user.service.ts | 20 +++- libs/common/src/response-messages/index.ts | 4 +- 5 files changed, 144 insertions(+), 5 deletions(-) diff --git a/apps/api-gateway/src/authz/authz.controller.ts b/apps/api-gateway/src/authz/authz.controller.ts index c31c8a1f1..e90311509 100644 --- a/apps/api-gateway/src/authz/authz.controller.ts +++ b/apps/api-gateway/src/authz/authz.controller.ts @@ -1,10 +1,13 @@ import { + BadRequestException, Body, Controller, + Delete, Get, HttpStatus, Logger, Param, + ParseUUIDPipe, Post, Query, Req, @@ -15,10 +18,19 @@ import { } from '@nestjs/common'; import { AuthzService } from './authz.service'; import { CommonService } from '../../../../libs/common/src/common.service'; -import { ApiBearerAuth, ApiBody, ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { + ApiBearerAuth, + ApiBody, + ApiForbiddenResponse, + ApiOperation, + ApiQuery, + ApiResponse, + ApiTags, + ApiUnauthorizedResponse +} from '@nestjs/swagger'; import { ApiResponseDto } from '../dtos/apiResponse.dto'; import { UserEmailVerificationDto } from '../user/dto/create-user.dto'; -import IResponseType from '@credebl/common/interfaces/response.interface'; +import IResponseType, { IResponse } from '@credebl/common/interfaces/response.interface'; import { ResponseMessages } from '@credebl/common/response-messages'; import { Response, Request } from 'express'; import { EmailVerificationDto } from '../user/dto/email-verify.dto'; @@ -36,6 +48,11 @@ import { SessionGuard } from './guards/session.guard'; import { UserLogoutDto } from './dtos/user-logout.dto'; import { AuthGuard } from '@nestjs/passport'; import { ISessionData } from 'apps/user/interfaces/user.interface'; +import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; +import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; +import { User } from './decorators/user.decorator'; +import { user } from '@prisma/client'; + @Controller('auth') @ApiTags('auth') @UseFilters(CustomExceptionFilter) @@ -331,4 +348,78 @@ export class AuthzController { return res.status(HttpStatus.OK).json(finalResponse); } + + /** + * Get all sessions by userId + * @param userId The ID of the user + * @returns All sessions related to the user + */ + @Get('/:userId/sessions') + @ApiOperation({ + summary: 'Get all sesssions by userId', + description: 'Retrieve sessions for the user. Based on userId.' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) + @ApiUnauthorizedResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Unauthorized', type: UnauthorizedErrorDto }) + @ApiForbiddenResponse({ status: HttpStatus.FORBIDDEN, description: 'Forbidden', type: ForbiddenErrorDto }) + @ApiBearerAuth() + @UseGuards(AuthGuard('jwt')) + async userSessions( + @Res() res: Response, + @Param( + 'userId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(`Invalid format for User Id`); + } + }) + ) + userId: string + ): Promise { + const response = await this.authzService.userSessions(userId); + + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.user.success.fetchAllSession, + data: response + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + /** + * Delete session by sessionId + * @param sessionId The ID of the session record to delete + * @returns Acknowledgement on deletion + */ + @Delete('/:sessionId/sessions') + @ApiOperation({ + summary: 'Delete a particular session using its sessionId', + description: 'Delete a particular session using its sessionId' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) + @ApiUnauthorizedResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Unauthorized', type: UnauthorizedErrorDto }) + @ApiForbiddenResponse({ status: HttpStatus.FORBIDDEN, description: 'Forbidden', type: ForbiddenErrorDto }) + @ApiBearerAuth() + @UseGuards(AuthGuard('jwt')) + async deleteSession( + @User() reqUser: user, + @Res() res: Response, + @Param( + 'sessionId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(`Invalid format for session Id`); + } + }) + ) + sessionId: string + ): Promise { + const response = await this.authzService.deleteSession(sessionId, reqUser.id); + + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: response.message + }; + return res.status(HttpStatus.OK).json(finalResponse); + } } diff --git a/apps/user/repositories/user.repository.ts b/apps/user/repositories/user.repository.ts index c20942086..515da2dcd 100644 --- a/apps/user/repositories/user.repository.ts +++ b/apps/user/repositories/user.repository.ts @@ -31,6 +31,7 @@ import { import { ProviderType, UserRole } from '@credebl/enum/enum'; import { PrismaService } from '@credebl/prisma-service'; +import { RpcException } from '@nestjs/microservices'; interface UserQueryOptions { id?: string; // Use the appropriate type based on your data model @@ -974,6 +975,23 @@ export class UserRepository { } } + async deleteSessionBySessionId(sessionId: string, userId: string): Promise<{ message: string }> { + try { + const result = await this.prisma.session.deleteMany({ + where: { id: sessionId, userId } + }); + + if (0 === result.count) { + throw new RpcException(new NotFoundException(`Session not found for userId: ${userId}`)); + } + + return { message: 'Session deleted successfully' }; + } catch (error) { + this.logger.error(`Error in Deleting Session: ${error.message}`); + throw error; + } + } + async fetchSessionByRefreshToken(refreshToken: string): Promise { try { const sessionDetails = await this.prisma.session.findFirst({ diff --git a/apps/user/src/user.controller.ts b/apps/user/src/user.controller.ts index a539d8a99..25a69d0e7 100644 --- a/apps/user/src/user.controller.ts +++ b/apps/user/src/user.controller.ts @@ -23,7 +23,7 @@ import { IVerifyUserEmail } from '@credebl/common/interfaces/user.interface'; // eslint-disable-next-line camelcase -import { client_aliases, user, user_org_roles } from '@prisma/client'; +import { client_aliases, session, user, user_org_roles } from '@prisma/client'; import { AcceptRejectInvitationDto } from '../dtos/accept-reject-invitation.dto'; import { AddPasskeyDetailsDto } from 'apps/api-gateway/src/user/dto/add-user.dto'; @@ -89,6 +89,16 @@ export class UserController { return this.userService.refreshTokenDetails(refreshToken); } + @MessagePattern({ cmd: 'session-details-by-userId' }) + async userSessions(userId: string): Promise { + return this.userService.userSessions(userId); + } + + @MessagePattern({ cmd: 'delete-session-by-sessionId' }) + async deleteSession(payload: { sessionId: string; userId: string }): Promise<{ message: string }> { + return this.userService.deleteSession(payload.sessionId, payload.userId); + } + @MessagePattern({ cmd: 'user-reset-password' }) async resetPassword(payload: IUserResetPassword): Promise { return this.userService.resetPassword(payload); diff --git a/apps/user/src/user.service.ts b/apps/user/src/user.service.ts index c0ddbfc29..6c4078fdd 100644 --- a/apps/user/src/user.service.ts +++ b/apps/user/src/user.service.ts @@ -26,7 +26,7 @@ import { UserRepository } from '../repositories/user.repository'; import { VerifyEmailTokenDto } from '../dtos/verify-email.dto'; import { sendEmail } from '@credebl/common/send-grid-helper-file'; // eslint-disable-next-line camelcase -import { client_aliases, RecordType, user, user_org_roles } from '@prisma/client'; +import { client_aliases, RecordType, session, user, user_org_roles } from '@prisma/client'; import { ICheckUserDetails, OrgInvitations, @@ -578,6 +578,24 @@ export class UserService { } } + async userSessions(userId: string): Promise { + try { + return this.userRepository.fetchUserSessions(userId); + } catch (error) { + this.logger.error(`get user sessions: ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); + } + } + + async deleteSession(sessionId: string, userId: string): Promise<{ message: string }> { + try { + return this.userRepository.deleteSessionBySessionId(sessionId, userId); + } catch (error) { + this.logger.error(`delete session by session id: ${JSON.stringify(error)}`); + throw error; + } + } + async updateFidoVerifiedUser(email: string, isFidoVerified: boolean, password: string): Promise { if (isFidoVerified) { await this.userRepository.addUserPassword(email.toLowerCase(), password); diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index 6174734e8..49308396c 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -26,7 +26,9 @@ export const ResponseMessages = { countriesVerificationCode: 'All countries has been fetched successfully', stateVerificationCode: 'All states has been fetched successfully', cityVerificationCode: 'All cities has been fetched successfully', - logout: 'User logout successfully' + logout: 'User logout successfully', + fetchAllSession: 'User Sessions fetched Successfully', + sessionDelete: 'Session deleted Successfully' }, error: { exists: 'User already exists', From ba4ff61a7b04dd50831cdcb1d3b64bcfca3c8cb5 Mon Sep 17 00:00:00 2001 From: sujitaw Date: Fri, 12 Sep 2025 17:55:37 +0530 Subject: [PATCH 02/10] feat/update signin api to store session details Signed-off-by: sujitaw --- .../api-gateway/src/authz/authz.controller.ts | 15 +++++-- apps/api-gateway/src/authz/authz.service.ts | 27 ++++++++++-- apps/user/dtos/login-user.dto.ts | 35 ++++++++------- apps/user/interfaces/user.interface.ts | 3 ++ apps/user/repositories/user.repository.ts | 3 +- apps/user/src/user.service.ts | 5 ++- libs/prisma-service/prisma/schema.prisma | 1 + package.json | 1 + pnpm-lock.yaml | 44 +++++++++++++++++-- 9 files changed, 105 insertions(+), 29 deletions(-) diff --git a/apps/api-gateway/src/authz/authz.controller.ts b/apps/api-gateway/src/authz/authz.controller.ts index e90311509..36176b96b 100644 --- a/apps/api-gateway/src/authz/authz.controller.ts +++ b/apps/api-gateway/src/authz/authz.controller.ts @@ -52,7 +52,7 @@ import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; import { User } from './decorators/user.decorator'; import { user } from '@prisma/client'; - +import * as UAParser from 'ua-parser-js'; @Controller('auth') @ApiTags('auth') @UseFilters(CustomExceptionFilter) @@ -175,9 +175,18 @@ export class AuthzController { }) @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: AuthTokenResponse }) @ApiBody({ type: LoginUserDto }) - async login(@Body() loginUserDto: LoginUserDto, @Res() res: Response): Promise { + async login(@Req() req: Request, @Body() loginUserDto: LoginUserDto, @Res() res: Response): Promise { if (loginUserDto.email) { - const userData = await this.authzService.login(loginUserDto.email, loginUserDto.password); + const ip = (req.headers['x-forwarded-for'] as string)?.split(',')[0] || req.socket.remoteAddress; + const ua = req.headers['user-agent']; + const parser = new UAParser.UAParser(ua); + const device = { + os: `${parser?.getOS()?.name} ${parser?.getOS()?.version ?? ''}`.trim(), + browser: `${parser?.getBrowser()?.name} ${parser?.getBrowser()?.version ?? ''}`.trim(), + deviceType: parser?.getDevice()?.type || 'desktop' + }; + const clientInfo = JSON.stringify({ ...device, rawDetail: ua, ip }); + const userData = await this.authzService.login(clientInfo, loginUserDto.email, loginUserDto.password); const finalResponse: IResponseType = { statusCode: HttpStatus.OK, diff --git a/apps/api-gateway/src/authz/authz.service.ts b/apps/api-gateway/src/authz/authz.service.ts index f9dc909f6..4a9402a24 100644 --- a/apps/api-gateway/src/authz/authz.service.ts +++ b/apps/api-gateway/src/authz/authz.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Inject } from '@nestjs/common'; +import { Injectable, Inject, HttpException } from '@nestjs/common'; import { ClientProxy } from '@nestjs/microservices'; import { BaseService } from '../../../../libs/service/base.service'; import { WebSocketGateway, WebSocketServer } from '@nestjs/websockets'; @@ -16,9 +16,10 @@ import { ResetPasswordDto } from './dtos/reset-password.dto'; import { ForgotPasswordDto } from './dtos/forgot-password.dto'; import { ResetTokenPasswordDto } from './dtos/reset-token-password'; import { NATSClient } from '@credebl/common/NATSClient'; -import { user } from '@prisma/client'; +import { session, user } from '@prisma/client'; import { ISessionDetails } from 'apps/user/interfaces/user.interface'; import { UserLogoutDto } from './dtos/user-logout.dto'; +import { JsonValue } from 'aws-sdk/clients/glue'; @Injectable() @WebSocketGateway() export class AuthzService extends BaseService { @@ -50,8 +51,8 @@ export class AuthzService extends BaseService { return this.natsClient.sendNatsMessage(this.authServiceProxy, 'user-email-verification', payload); } - async login(email: string, password?: string, isPasskey = false): Promise { - const payload = { email, password, isPasskey }; + async login(clientInfo: JsonValue, email: string, password?: string, isPasskey = false): Promise { + const payload = { email, password, isPasskey, clientInfo }; return this.natsClient.sendNatsMessage(this.authServiceProxy, 'user-holder-login', payload); } @@ -76,6 +77,24 @@ export class AuthzService extends BaseService { return this.natsClient.sendNatsMessage(this.authServiceProxy, 'refresh-token-details', refreshToken); } + async userSessions(userId: string): Promise { + return this.natsClient.sendNatsMessage(this.authServiceProxy, 'session-details-by-userId', userId); + } + + async deleteSession(sessionId: string, userId: string): Promise<{ message: string }> { + try { + return await this.natsClient.sendNatsMessage(this.authServiceProxy, 'delete-session-by-sessionId', { + sessionId, + userId + }); + } catch (error) { + if (error?.response && error?.status) { + throw new HttpException(error.response, error.status); + } + throw error; + } + } + async addUserDetails(userInfo: AddUserDetailsDto): Promise { const payload = { userInfo }; return this.natsClient.sendNatsMessage(this.authServiceProxy, 'add-user', payload); diff --git a/apps/user/dtos/login-user.dto.ts b/apps/user/dtos/login-user.dto.ts index 2a4aa64db..d56af3861 100644 --- a/apps/user/dtos/login-user.dto.ts +++ b/apps/user/dtos/login-user.dto.ts @@ -1,23 +1,28 @@ import { trim } from '@credebl/common/cast.helper'; import { ApiProperty } from '@nestjs/swagger'; +import { JsonValue } from '@prisma/client/runtime/library'; import { Transform } from 'class-transformer'; import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator'; export class LoginUserDto { - @ApiProperty({ example: 'awqx@yopmail.com' }) - @IsEmail({}, { message: 'Please provide a valid email' }) - @IsNotEmpty({ message: 'Email is required' }) - @IsString({ message: 'Email should be a string' }) - @Transform(({ value }) => trim(value)) - email: string; - - @ApiProperty({ example: 'Password@1' }) - @IsOptional() - @IsString({ message: 'password should be string' }) - password?: string; + @ApiProperty({ example: 'awqx@yopmail.com' }) + @IsEmail({}, { message: 'Please provide a valid email' }) + @IsNotEmpty({ message: 'Email is required' }) + @IsString({ message: 'Email should be a string' }) + @Transform(({ value }) => trim(value)) + email: string; - @ApiProperty({ example: 'false' }) - @IsOptional() - @IsBoolean({ message: 'isPasskey should be boolean' }) - isPasskey?: boolean; + @ApiProperty({ example: 'Password@1' }) + @IsOptional() + @IsString({ message: 'password should be string' }) + password?: string; + + @ApiProperty({ example: 'false' }) + @IsOptional() + @IsBoolean({ message: 'isPasskey should be boolean' }) + isPasskey?: boolean; + + @ApiProperty({ example: 'false' }) + @IsOptional() + clientInfo?: JsonValue; } diff --git a/apps/user/interfaces/user.interface.ts b/apps/user/interfaces/user.interface.ts index a0c3ba997..568f8fb48 100644 --- a/apps/user/interfaces/user.interface.ts +++ b/apps/user/interfaces/user.interface.ts @@ -1,4 +1,5 @@ import { $Enums, Prisma, RecordType } from '@prisma/client'; +import { JsonValue } from 'aws-sdk/clients/glue'; export interface IUsersProfile { id: string; @@ -179,6 +180,7 @@ export interface IUserSignIn { email: string; password: string; isPasskey?: boolean; + clientInfo: JsonValue; } export interface ISession { @@ -192,6 +194,7 @@ export interface ISession { accountId?: string; sessionType?: string; expiresAt?: Date; + clientInfo?: Prisma.JsonValue | null; } export interface IUpdateAccountDetails { diff --git a/apps/user/repositories/user.repository.ts b/apps/user/repositories/user.repository.ts index 515da2dcd..fa6074ebf 100644 --- a/apps/user/repositories/user.repository.ts +++ b/apps/user/repositories/user.repository.ts @@ -688,7 +688,8 @@ export class UserRepository { refreshToken, accountId, sessionType, - expiresAt + expiresAt, + ...(tokenDetails.clientInfo ? { clientInfo: tokenDetails.clientInfo } : { clientInfo: { clientToken: true } }) } }); return sessionResponse; diff --git a/apps/user/src/user.service.ts b/apps/user/src/user.service.ts index 6c4078fdd..3e17dcded 100644 --- a/apps/user/src/user.service.ts +++ b/apps/user/src/user.service.ts @@ -435,7 +435,7 @@ export class UserService { * @returns User access token details */ async login(loginUserDto: LoginUserDto): Promise { - const { email, password, isPasskey } = loginUserDto; + const { email, password, isPasskey, clientInfo } = loginUserDto; try { this.validateEmail(email.toLowerCase()); @@ -476,7 +476,8 @@ export class UserService { expires: tokenDetails?.expires_in, refreshToken: tokenDetails?.refresh_token, sessionType: SessionType.USER_SESSION, - expiresAt + expiresAt, + clientInfo }; const fetchAccountDetails = await this.userRepository.checkAccountDetails(userData?.id); diff --git a/libs/prisma-service/prisma/schema.prisma b/libs/prisma-service/prisma/schema.prisma index 337c953a2..108ff614e 100644 --- a/libs/prisma-service/prisma/schema.prisma +++ b/libs/prisma-service/prisma/schema.prisma @@ -67,6 +67,7 @@ model session { sessionType String? account account? @relation(fields: [accountId], references:[id]) expiresAt DateTime? @db.Timestamp(6) + clientInfo Json? } model token { diff --git a/package.json b/package.json index 214141f89..f140475cb 100644 --- a/package.json +++ b/package.json @@ -114,6 +114,7 @@ "socket.io-client": "^4.7.1", "swagger-ui-express": "^5.0.0", "typeorm": "^0.3.10", + "ua-parser-js": "^2.0.5", "unzipper": "^0.10.14", "uuid": "^9.0.0", "validator": "^13.11.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c325bcf7..0214a21bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -272,6 +272,9 @@ importers: typeorm: specifier: ^0.3.10 version: 0.3.25(ioredis@5.6.1)(pg@8.16.2)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.1)(typescript@5.8.3)) + ua-parser-js: + specifier: ^2.0.5 + version: 2.0.5 unzipper: specifier: ^0.10.14 version: 0.10.14 @@ -2615,6 +2618,9 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-europe-js@0.1.2: + resolution: {integrity: sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==} + detect-libc@2.0.4: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} @@ -3617,6 +3623,9 @@ packages: resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} engines: {node: '>= 0.4'} + is-standalone-pwa@0.1.1: + resolution: {integrity: sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g==} + is-stream@1.1.0: resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} engines: {node: '>=0.10.0'} @@ -4838,12 +4847,12 @@ packages: puppeteer@21.0.1: resolution: {integrity: sha512-KTjmSdPZ6bMkq3EbAzAUhcB3gMDXvdwd6912rxG9hNtjwRJzHSA568vh6vIbO2WQeNmozRdt1LtiUMLSWfeMrg==} engines: {node: '>=16.3.0'} - deprecated: < 22.8.2 is no longer supported + deprecated: < 24.10.2 is no longer supported puppeteer@21.11.0: resolution: {integrity: sha512-9jTHuYe22TD3sNxy0nEIzC7ZrlRnDgeX3xPkbS7PnbdwYjl2o/z/YuCrRBwezdKpbTDTJ4VqIggzNyeRcKq3cg==} engines: {node: '>=16.13.2'} - deprecated: < 22.8.2 is no longer supported + deprecated: < 24.10.2 is no longer supported hasBin: true pure-rand@6.1.0: @@ -5348,16 +5357,17 @@ packages: superagent@7.1.6: resolution: {integrity: sha512-gZkVCQR1gy/oUXr+kxJMLDjla434KmSOKbx5iGD30Ql+AkJQ/YlPKECJy2nhqOsHLjGHzoDTXNSjhnvWhzKk7g==} engines: {node: '>=6.4.0 <13 || >=14'} - deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net + deprecated: Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net superagent@8.1.2: resolution: {integrity: sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==} engines: {node: '>=6.4.0 <13 || >=14'} - deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net + deprecated: Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net supertest@6.3.4: resolution: {integrity: sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==} engines: {node: '>=6.4.0'} + deprecated: Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} @@ -5693,6 +5703,13 @@ packages: engines: {node: '>=14.17'} hasBin: true + ua-is-frozen@0.1.2: + resolution: {integrity: sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==} + + ua-parser-js@2.0.5: + resolution: {integrity: sha512-sZErtx3rhpvZQanWW5umau4o/snfoLqRcQwQIZ54377WtRzIecnIKvjpkd5JwPcSUMglGnbIgcsQBGAbdi3S9Q==} + hasBin: true + uglify-js@3.19.3: resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} engines: {node: '>=0.8.0'} @@ -5716,6 +5733,10 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici@7.16.0: + resolution: {integrity: sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==} + engines: {node: '>=20.18.1'} + unfetch@4.2.0: resolution: {integrity: sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==} @@ -8651,6 +8672,8 @@ snapshots: destroy@1.2.0: {} + detect-europe-js@0.1.2: {} + detect-libc@2.0.4: {} detect-newline@3.1.0: {} @@ -9956,6 +9979,8 @@ snapshots: dependencies: call-bound: 1.0.4 + is-standalone-pwa@0.1.1: {} + is-stream@1.1.0: optional: true @@ -12375,6 +12400,15 @@ snapshots: typescript@5.8.3: {} + ua-is-frozen@0.1.2: {} + + ua-parser-js@2.0.5: + dependencies: + detect-europe-js: 0.1.2 + is-standalone-pwa: 0.1.1 + ua-is-frozen: 0.1.2 + undici: 7.16.0 + uglify-js@3.19.3: optional: true @@ -12398,6 +12432,8 @@ snapshots: undici-types@6.21.0: {} + undici@7.16.0: {} + unfetch@4.2.0: {} unicode-byte-truncate@1.0.0: From 3822565679245667ee6cadc9a58d586eb9eb8596 Mon Sep 17 00:00:00 2001 From: sujitaw Date: Fri, 12 Sep 2025 18:39:23 +0530 Subject: [PATCH 03/10] feat/added prisma migration file for clientInfo column Signed-off-by: sujitaw --- .../migration.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 libs/prisma-service/prisma/migrations/20250911122855_add_client_info_column_for_sessions_table/migration.sql diff --git a/libs/prisma-service/prisma/migrations/20250911122855_add_client_info_column_for_sessions_table/migration.sql b/libs/prisma-service/prisma/migrations/20250911122855_add_client_info_column_for_sessions_table/migration.sql new file mode 100644 index 000000000..bcca871a0 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20250911122855_add_client_info_column_for_sessions_table/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "session" ADD COLUMN "clientInfo" JSONB; From baf9bae8f9666800deeb6164ba70166cc955ef19 Mon Sep 17 00:00:00 2001 From: sujitaw Date: Sat, 13 Sep 2025 18:53:29 +0530 Subject: [PATCH 04/10] fix/sonarcube comment Signed-off-by: sujitaw --- apps/user/src/user.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/user/src/user.service.ts b/apps/user/src/user.service.ts index 3e17dcded..0ecbe1fe7 100644 --- a/apps/user/src/user.service.ts +++ b/apps/user/src/user.service.ts @@ -581,7 +581,7 @@ export class UserService { async userSessions(userId: string): Promise { try { - return this.userRepository.fetchUserSessions(userId); + return await this.userRepository.fetchUserSessions(userId); } catch (error) { this.logger.error(`get user sessions: ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); @@ -590,7 +590,7 @@ export class UserService { async deleteSession(sessionId: string, userId: string): Promise<{ message: string }> { try { - return this.userRepository.deleteSessionBySessionId(sessionId, userId); + return await this.userRepository.deleteSessionBySessionId(sessionId, userId); } catch (error) { this.logger.error(`delete session by session id: ${JSON.stringify(error)}`); throw error; From 63e13490a78ec9d2eea7ce45d8f14e1d835c5ed1 Mon Sep 17 00:00:00 2001 From: sujitaw Date: Sat, 13 Sep 2025 20:25:53 +0530 Subject: [PATCH 05/10] fix/code rabbit comments Signed-off-by: sujitaw --- apps/api-gateway/src/authz/authz.controller.ts | 12 +++++++++--- apps/api-gateway/src/authz/authz.service.ts | 8 ++++---- apps/user/dtos/login-user.dto.ts | 9 +++++---- apps/user/interfaces/user.interface.ts | 12 ++++++++++-- apps/user/repositories/user.repository.ts | 11 ++++++++++- apps/user/src/user.controller.ts | 5 +++-- apps/user/src/user.service.ts | 5 +++-- 7 files changed, 44 insertions(+), 18 deletions(-) diff --git a/apps/api-gateway/src/authz/authz.controller.ts b/apps/api-gateway/src/authz/authz.controller.ts index 36176b96b..127d239a3 100644 --- a/apps/api-gateway/src/authz/authz.controller.ts +++ b/apps/api-gateway/src/authz/authz.controller.ts @@ -3,6 +3,7 @@ import { Body, Controller, Delete, + ForbiddenException, Get, HttpStatus, Logger, @@ -52,7 +53,8 @@ import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; import { User } from './decorators/user.decorator'; import { user } from '@prisma/client'; -import * as UAParser from 'ua-parser-js'; +// import * as UAParser from 'ua-parser-js'; +import { UAParser } from 'ua-parser-js'; @Controller('auth') @ApiTags('auth') @UseFilters(CustomExceptionFilter) @@ -179,7 +181,7 @@ export class AuthzController { if (loginUserDto.email) { const ip = (req.headers['x-forwarded-for'] as string)?.split(',')[0] || req.socket.remoteAddress; const ua = req.headers['user-agent']; - const parser = new UAParser.UAParser(ua); + const parser = new UAParser(ua); const device = { os: `${parser?.getOS()?.name} ${parser?.getOS()?.version ?? ''}`.trim(), browser: `${parser?.getBrowser()?.name} ${parser?.getBrowser()?.version ?? ''}`.trim(), @@ -365,7 +367,7 @@ export class AuthzController { */ @Get('/:userId/sessions') @ApiOperation({ - summary: 'Get all sesssions by userId', + summary: 'Get all sessions by userId', description: 'Retrieve sessions for the user. Based on userId.' }) @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) @@ -374,6 +376,7 @@ export class AuthzController { @ApiBearerAuth() @UseGuards(AuthGuard('jwt')) async userSessions( + @User() reqUser: user, @Res() res: Response, @Param( 'userId', @@ -385,6 +388,9 @@ export class AuthzController { ) userId: string ): Promise { + if (reqUser.id !== userId) { + throw new ForbiddenException('You are not allowed to access sessions of another user'); + } const response = await this.authzService.userSessions(userId); const finalResponse: IResponse = { diff --git a/apps/api-gateway/src/authz/authz.service.ts b/apps/api-gateway/src/authz/authz.service.ts index 4a9402a24..b3c16237a 100644 --- a/apps/api-gateway/src/authz/authz.service.ts +++ b/apps/api-gateway/src/authz/authz.service.ts @@ -17,9 +17,9 @@ import { ForgotPasswordDto } from './dtos/forgot-password.dto'; import { ResetTokenPasswordDto } from './dtos/reset-token-password'; import { NATSClient } from '@credebl/common/NATSClient'; import { session, user } from '@prisma/client'; -import { ISessionDetails } from 'apps/user/interfaces/user.interface'; +import { IRestrictedUserSession } from 'apps/user/interfaces/user.interface'; import { UserLogoutDto } from './dtos/user-logout.dto'; -import { JsonValue } from 'aws-sdk/clients/glue'; +import type { Prisma } from '@prisma/client'; @Injectable() @WebSocketGateway() export class AuthzService extends BaseService { @@ -51,12 +51,12 @@ export class AuthzService extends BaseService { return this.natsClient.sendNatsMessage(this.authServiceProxy, 'user-email-verification', payload); } - async login(clientInfo: JsonValue, email: string, password?: string, isPasskey = false): Promise { + async login(clientInfo: Prisma.JsonValue, email: string, password?: string, isPasskey = false): Promise { const payload = { email, password, isPasskey, clientInfo }; return this.natsClient.sendNatsMessage(this.authServiceProxy, 'user-holder-login', payload); } - async getSession(sessionId): Promise { + async getSession(sessionId): Promise { const payload = { ...sessionId }; return this.natsClient.sendNatsMessage(this.authServiceProxy, 'fetch-session-details', payload); } diff --git a/apps/user/dtos/login-user.dto.ts b/apps/user/dtos/login-user.dto.ts index d56af3861..0c344161b 100644 --- a/apps/user/dtos/login-user.dto.ts +++ b/apps/user/dtos/login-user.dto.ts @@ -1,8 +1,9 @@ -import { trim } from '@credebl/common/cast.helper'; +import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator'; + import { ApiProperty } from '@nestjs/swagger'; -import { JsonValue } from '@prisma/client/runtime/library'; +import type { Prisma } from '@prisma/client'; import { Transform } from 'class-transformer'; -import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { trim } from '@credebl/common/cast.helper'; export class LoginUserDto { @ApiProperty({ example: 'awqx@yopmail.com' }) @@ -24,5 +25,5 @@ export class LoginUserDto { @ApiProperty({ example: 'false' }) @IsOptional() - clientInfo?: JsonValue; + clientInfo?: Prisma.JsonValue; } diff --git a/apps/user/interfaces/user.interface.ts b/apps/user/interfaces/user.interface.ts index 568f8fb48..d080d596a 100644 --- a/apps/user/interfaces/user.interface.ts +++ b/apps/user/interfaces/user.interface.ts @@ -1,5 +1,4 @@ import { $Enums, Prisma, RecordType } from '@prisma/client'; -import { JsonValue } from 'aws-sdk/clients/glue'; export interface IUsersProfile { id: string; @@ -180,7 +179,7 @@ export interface IUserSignIn { email: string; password: string; isPasskey?: boolean; - clientInfo: JsonValue; + clientInfo: Prisma.JsonValue; } export interface ISession { @@ -293,3 +292,12 @@ export interface IAccountDetails { export interface ISessionData { sessionId: string; } + +export interface IRestrictedUserSession { + id: string; + userId: string; + expiresAt: Date; + createdAt: Date; + clientInfo: Prisma.JsonValue | null; + sessionType: string; +} diff --git a/apps/user/repositories/user.repository.ts b/apps/user/repositories/user.repository.ts index fa6074ebf..ee8673e4a 100644 --- a/apps/user/repositories/user.repository.ts +++ b/apps/user/repositories/user.repository.ts @@ -3,6 +3,7 @@ import { IOrgUsers, + IRestrictedUserSession, ISendVerificationEmail, ISession, IShareUserCertificate, @@ -699,11 +700,19 @@ export class UserRepository { } } - async fetchUserSessions(userId: string): Promise { + async fetchUserSessions(userId: string): Promise { try { const userSessionCount = await this.prisma.session.findMany({ where: { userId + }, + select: { + id: true, + userId: true, + expiresAt: true, + createdAt: true, + clientInfo: true, + sessionType: true } }); return userSessionCount; diff --git a/apps/user/src/user.controller.ts b/apps/user/src/user.controller.ts index 25a69d0e7..c81bc78d2 100644 --- a/apps/user/src/user.controller.ts +++ b/apps/user/src/user.controller.ts @@ -1,6 +1,7 @@ import { ICheckUserDetails, IOrgUsers, + IRestrictedUserSession, ISessionDetails, ISessions, IUserDeletedActivity, @@ -23,7 +24,7 @@ import { IVerifyUserEmail } from '@credebl/common/interfaces/user.interface'; // eslint-disable-next-line camelcase -import { client_aliases, session, user, user_org_roles } from '@prisma/client'; +import { client_aliases, user, user_org_roles } from '@prisma/client'; import { AcceptRejectInvitationDto } from '../dtos/accept-reject-invitation.dto'; import { AddPasskeyDetailsDto } from 'apps/api-gateway/src/user/dto/add-user.dto'; @@ -90,7 +91,7 @@ export class UserController { } @MessagePattern({ cmd: 'session-details-by-userId' }) - async userSessions(userId: string): Promise { + async userSessions(userId: string): Promise { return this.userService.userSessions(userId); } diff --git a/apps/user/src/user.service.ts b/apps/user/src/user.service.ts index 0ecbe1fe7..ea93b16ab 100644 --- a/apps/user/src/user.service.ts +++ b/apps/user/src/user.service.ts @@ -42,7 +42,8 @@ import { IUserForgotPassword, ISessionDetails, ISessions, - IUpdateAccountDetails + IUpdateAccountDetails, + IRestrictedUserSession } from '../interfaces/user.interface'; import { AcceptRejectInvitationDto } from '../dtos/accept-reject-invitation.dto'; import { UserActivityService } from '@credebl/user-activity'; @@ -579,7 +580,7 @@ export class UserService { } } - async userSessions(userId: string): Promise { + async userSessions(userId: string): Promise { try { return await this.userRepository.fetchUserSessions(userId); } catch (error) { From 71c65868e1db14ec68f644bbf4debd6197f791ff Mon Sep 17 00:00:00 2001 From: sujitaw Date: Mon, 15 Sep 2025 11:47:22 +0530 Subject: [PATCH 06/10] fix/rabbit comments Signed-off-by: sujitaw --- apps/api-gateway/src/authz/authz.service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/api-gateway/src/authz/authz.service.ts b/apps/api-gateway/src/authz/authz.service.ts index b3c16237a..34a3941ba 100644 --- a/apps/api-gateway/src/authz/authz.service.ts +++ b/apps/api-gateway/src/authz/authz.service.ts @@ -16,8 +16,8 @@ import { ResetPasswordDto } from './dtos/reset-password.dto'; import { ForgotPasswordDto } from './dtos/forgot-password.dto'; import { ResetTokenPasswordDto } from './dtos/reset-token-password'; import { NATSClient } from '@credebl/common/NATSClient'; -import { session, user } from '@prisma/client'; -import { IRestrictedUserSession } from 'apps/user/interfaces/user.interface'; +import { user } from '@prisma/client'; +import { IRestrictedUserSession, ISessionDetails } from 'apps/user/interfaces/user.interface'; import { UserLogoutDto } from './dtos/user-logout.dto'; import type { Prisma } from '@prisma/client'; @Injectable() @@ -56,7 +56,7 @@ export class AuthzService extends BaseService { return this.natsClient.sendNatsMessage(this.authServiceProxy, 'user-holder-login', payload); } - async getSession(sessionId): Promise { + async getSession(sessionId): Promise { const payload = { ...sessionId }; return this.natsClient.sendNatsMessage(this.authServiceProxy, 'fetch-session-details', payload); } @@ -77,7 +77,7 @@ export class AuthzService extends BaseService { return this.natsClient.sendNatsMessage(this.authServiceProxy, 'refresh-token-details', refreshToken); } - async userSessions(userId: string): Promise { + async userSessions(userId: string): Promise { return this.natsClient.sendNatsMessage(this.authServiceProxy, 'session-details-by-userId', userId); } From de0948cd86fd8555f090ecf15a48f5603ad9a05d Mon Sep 17 00:00:00 2001 From: sujitaw Date: Mon, 15 Sep 2025 12:32:11 +0530 Subject: [PATCH 07/10] fix/pr comments Signed-off-by: sujitaw --- apps/api-gateway/src/authz/authz.controller.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/api-gateway/src/authz/authz.controller.ts b/apps/api-gateway/src/authz/authz.controller.ts index 127d239a3..67f920cce 100644 --- a/apps/api-gateway/src/authz/authz.controller.ts +++ b/apps/api-gateway/src/authz/authz.controller.ts @@ -53,7 +53,6 @@ import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; import { User } from './decorators/user.decorator'; import { user } from '@prisma/client'; -// import * as UAParser from 'ua-parser-js'; import { UAParser } from 'ua-parser-js'; @Controller('auth') @ApiTags('auth') From 50d718c84290fb3ff7790b44d21596ade81f1302 Mon Sep 17 00:00:00 2001 From: sujitaw Date: Mon, 15 Sep 2025 16:25:58 +0530 Subject: [PATCH 08/10] fix/changed the package from uaparser to express-parser Signed-off-by: sujitaw --- .../api-gateway/src/authz/authz.controller.ts | 12 ++--- apps/api-gateway/src/main.ts | 3 ++ package.json | 2 +- pnpm-lock.yaml | 44 ++++--------------- 4 files changed, 20 insertions(+), 41 deletions(-) diff --git a/apps/api-gateway/src/authz/authz.controller.ts b/apps/api-gateway/src/authz/authz.controller.ts index 67f920cce..dbc60f931 100644 --- a/apps/api-gateway/src/authz/authz.controller.ts +++ b/apps/api-gateway/src/authz/authz.controller.ts @@ -53,7 +53,8 @@ import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; import { User } from './decorators/user.decorator'; import { user } from '@prisma/client'; -import { UAParser } from 'ua-parser-js'; +import * as useragent from 'express-useragent'; + @Controller('auth') @ApiTags('auth') @UseFilters(CustomExceptionFilter) @@ -180,12 +181,13 @@ export class AuthzController { if (loginUserDto.email) { const ip = (req.headers['x-forwarded-for'] as string)?.split(',')[0] || req.socket.remoteAddress; const ua = req.headers['user-agent']; - const parser = new UAParser(ua); + const expressUa = useragent.parse(ua); const device = { - os: `${parser?.getOS()?.name} ${parser?.getOS()?.version ?? ''}`.trim(), - browser: `${parser?.getBrowser()?.name} ${parser?.getBrowser()?.version ?? ''}`.trim(), - deviceType: parser?.getDevice()?.type || 'desktop' + browser: `${expressUa.browser} ${expressUa.version ?? ''}`.trim(), + os: expressUa.platform, + deviceType: expressUa.isDesktop ? 'desktop' : 'mobile' }; + const clientInfo = JSON.stringify({ ...device, rawDetail: ua, ip }); const userData = await this.authzService.login(clientInfo, loginUserDto.email, loginUserDto.password); diff --git a/apps/api-gateway/src/main.ts b/apps/api-gateway/src/main.ts index aabe4b3a5..0b08970c5 100644 --- a/apps/api-gateway/src/main.ts +++ b/apps/api-gateway/src/main.ts @@ -16,6 +16,8 @@ import { CommonConstants } from '@credebl/common/common.constant'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; import { NatsInterceptor } from '@credebl/common'; import { UpdatableValidationPipe } from '@credebl/common/custom-overrideable-validation-pipe'; +import * as useragent from 'express-useragent'; + dotenv.config(); async function bootstrap(): Promise { @@ -46,6 +48,7 @@ async function bootstrap(): Promise { app.use(express.json({ limit: '100mb' })); app.use(express.urlencoded({ limit: '100mb', extended: true })); app.use(cookieParser()); + app.use(useragent.express()); app.use((req, res, next) => { let err = null; diff --git a/package.json b/package.json index f140475cb..3affb2fec 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "crypto-random-string": "^5.0.0", "dotenv": "^16.0.3", "express": "^4.18.2", + "express-useragent": "^1.0.15", "fs": "0.0.1-security", "generate-password": "^1.7.0", "helmet": "^7.0.0", @@ -114,7 +115,6 @@ "socket.io-client": "^4.7.1", "swagger-ui-express": "^5.0.0", "typeorm": "^0.3.10", - "ua-parser-js": "^2.0.5", "unzipper": "^0.10.14", "uuid": "^9.0.0", "validator": "^13.11.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0214a21bc..65c373c35 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -158,6 +158,9 @@ importers: express: specifier: ^4.18.2 version: 4.21.2 + express-useragent: + specifier: ^1.0.15 + version: 1.0.15 fs: specifier: 0.0.1-security version: 0.0.1-security @@ -272,9 +275,6 @@ importers: typeorm: specifier: ^0.3.10 version: 0.3.25(ioredis@5.6.1)(pg@8.16.2)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.1)(typescript@5.8.3)) - ua-parser-js: - specifier: ^2.0.5 - version: 2.0.5 unzipper: specifier: ^0.10.14 version: 0.10.14 @@ -2618,9 +2618,6 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - detect-europe-js@0.1.2: - resolution: {integrity: sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==} - detect-libc@2.0.4: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} @@ -3006,6 +3003,10 @@ packages: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + express-useragent@1.0.15: + resolution: {integrity: sha512-eq5xMiYCYwFPoekffMjvEIk+NWdlQY9Y38OsTyl13IvA728vKT+q/CSERYWzcw93HGBJcIqMIsZC5CZGARPVdg==} + engines: {node: '>=4.5'} + express@4.21.2: resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} @@ -3623,9 +3624,6 @@ packages: resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} engines: {node: '>= 0.4'} - is-standalone-pwa@0.1.1: - resolution: {integrity: sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g==} - is-stream@1.1.0: resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} engines: {node: '>=0.10.0'} @@ -5703,13 +5701,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - ua-is-frozen@0.1.2: - resolution: {integrity: sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==} - - ua-parser-js@2.0.5: - resolution: {integrity: sha512-sZErtx3rhpvZQanWW5umau4o/snfoLqRcQwQIZ54377WtRzIecnIKvjpkd5JwPcSUMglGnbIgcsQBGAbdi3S9Q==} - hasBin: true - uglify-js@3.19.3: resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} engines: {node: '>=0.8.0'} @@ -5733,10 +5724,6 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici@7.16.0: - resolution: {integrity: sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==} - engines: {node: '>=20.18.1'} - unfetch@4.2.0: resolution: {integrity: sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==} @@ -8672,8 +8659,6 @@ snapshots: destroy@1.2.0: {} - detect-europe-js@0.1.2: {} - detect-libc@2.0.4: {} detect-newline@3.1.0: {} @@ -9201,6 +9186,8 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 + express-useragent@1.0.15: {} + express@4.21.2: dependencies: accepts: 1.3.8 @@ -9979,8 +9966,6 @@ snapshots: dependencies: call-bound: 1.0.4 - is-standalone-pwa@0.1.1: {} - is-stream@1.1.0: optional: true @@ -12400,15 +12385,6 @@ snapshots: typescript@5.8.3: {} - ua-is-frozen@0.1.2: {} - - ua-parser-js@2.0.5: - dependencies: - detect-europe-js: 0.1.2 - is-standalone-pwa: 0.1.1 - ua-is-frozen: 0.1.2 - undici: 7.16.0 - uglify-js@3.19.3: optional: true @@ -12432,8 +12408,6 @@ snapshots: undici-types@6.21.0: {} - undici@7.16.0: {} - unfetch@4.2.0: {} unicode-byte-truncate@1.0.0: From 69e94cca1f4c2e3534accbb626c7257e53709c3d Mon Sep 17 00:00:00 2001 From: sujitaw Date: Tue, 16 Sep 2025 17:05:02 +0530 Subject: [PATCH 09/10] feat/update jwt logic to throw 401 if session is deleted Signed-off-by: sujitaw --- apps/api-gateway/src/authz/authz.service.ts | 4 ++++ .../src/authz/jwt-payload.interface.ts | 24 +++++++++---------- apps/api-gateway/src/authz/jwt.strategy.ts | 13 ++++++++++ apps/user/repositories/user.repository.ts | 3 ++- apps/user/src/user.controller.ts | 5 ++++ apps/user/src/user.service.ts | 10 ++++++++ 6 files changed, 46 insertions(+), 13 deletions(-) diff --git a/apps/api-gateway/src/authz/authz.service.ts b/apps/api-gateway/src/authz/authz.service.ts index 34a3941ba..1fb50dc03 100644 --- a/apps/api-gateway/src/authz/authz.service.ts +++ b/apps/api-gateway/src/authz/authz.service.ts @@ -61,6 +61,10 @@ export class AuthzService extends BaseService { return this.natsClient.sendNatsMessage(this.authServiceProxy, 'fetch-session-details', payload); } + async checkSession(sessionId): Promise { + return this.natsClient.sendNatsMessage(this.authServiceProxy, 'check-session-details', sessionId); + } + async resetPassword(resetPasswordDto: ResetPasswordDto): Promise { return this.natsClient.sendNatsMessage(this.authServiceProxy, 'user-reset-password', resetPasswordDto); } diff --git a/apps/api-gateway/src/authz/jwt-payload.interface.ts b/apps/api-gateway/src/authz/jwt-payload.interface.ts index c4dcec845..da946d04f 100644 --- a/apps/api-gateway/src/authz/jwt-payload.interface.ts +++ b/apps/api-gateway/src/authz/jwt-payload.interface.ts @@ -1,13 +1,13 @@ export interface JwtPayload { - iss: string; - sub: string; - aud: string[]; - iat?: number; - exp?: number; - azp: string; - scope: string; - gty?: string; - permissions: string[]; - email?: string - } - \ No newline at end of file + iss: string; + sub: string; + aud: string[]; + iat?: number; + exp?: number; + azp: string; + scope: string; + gty?: string; + permissions: string[]; + email?: string; + sid: string; +} diff --git a/apps/api-gateway/src/authz/jwt.strategy.ts b/apps/api-gateway/src/authz/jwt.strategy.ts index a5dc0a8b6..e5f9860f1 100644 --- a/apps/api-gateway/src/authz/jwt.strategy.ts +++ b/apps/api-gateway/src/authz/jwt.strategy.ts @@ -62,6 +62,19 @@ export class JwtStrategy extends PassportStrategy(Strategy) { let userDetails = null; let userInfo; + const sessionId = payload?.sid; + let sessionDetails = null; + if (sessionId) { + try { + sessionDetails = await this.authzService.checkSession(sessionId); + } catch (error) { + this.logger.log('Error in JWT Stratergy while fetching session details', JSON.stringify(error, null, 2)); + } + if (!sessionDetails) { + throw new UnauthorizedException(ResponseMessages.user.error.invalidAccessToken); + } + } + if (payload?.email) { userInfo = await this.usersService.getUserByUserIdInKeycloak(payload?.email); } diff --git a/apps/user/repositories/user.repository.ts b/apps/user/repositories/user.repository.ts index ee8673e4a..3c3ac28b0 100644 --- a/apps/user/repositories/user.repository.ts +++ b/apps/user/repositories/user.repository.ts @@ -139,11 +139,12 @@ export class UserRepository { */ async getSession(sessionId: string): Promise { try { - return this.prisma.session.findUnique({ + const data = await this.prisma.session.findUnique({ where: { id: sessionId } }); + return data; } catch (error) { this.logger.error(`Not Found: ${JSON.stringify(error)}`); throw new NotFoundException(error); diff --git a/apps/user/src/user.controller.ts b/apps/user/src/user.controller.ts index c81bc78d2..5731998f1 100644 --- a/apps/user/src/user.controller.ts +++ b/apps/user/src/user.controller.ts @@ -85,6 +85,11 @@ export class UserController { return this.userService.getSession(payload?.sessionId); } + @MessagePattern({ cmd: 'check-session-details' }) + async checkSession(sessionId: string): Promise { + return this.userService.checkSession(sessionId); + } + @MessagePattern({ cmd: 'refresh-token-details' }) async refreshTokenDetails(refreshToken: string): Promise { return this.userService.refreshTokenDetails(refreshToken); diff --git a/apps/user/src/user.service.ts b/apps/user/src/user.service.ts index ea93b16ab..474dc6752 100644 --- a/apps/user/src/user.service.ts +++ b/apps/user/src/user.service.ts @@ -525,6 +525,16 @@ export class UserService { } } + async checkSession(sessionId: string): Promise { + try { + const sessionDetails = await this.userRepository.getSession(sessionId); + return sessionDetails; + } catch (error) { + this.logger.error(`In fetching session details : ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); + } + } + async refreshTokenDetails(refreshToken: string): Promise { try { try { From c3399518b9d69251fad5a1dee9f16c983c0b255e Mon Sep 17 00:00:00 2001 From: sujitaw Date: Mon, 22 Sep 2025 18:26:28 +0530 Subject: [PATCH 10/10] fix/resolve pr comments Signed-off-by: sujitaw --- apps/user/repositories/user.repository.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/apps/user/repositories/user.repository.ts b/apps/user/repositories/user.repository.ts index 3c3ac28b0..38f8ef0a8 100644 --- a/apps/user/repositories/user.repository.ts +++ b/apps/user/repositories/user.repository.ts @@ -139,12 +139,11 @@ export class UserRepository { */ async getSession(sessionId: string): Promise { try { - const data = await this.prisma.session.findUnique({ + return await this.prisma.session.findUnique({ where: { id: sessionId } }); - return data; } catch (error) { this.logger.error(`Not Found: ${JSON.stringify(error)}`); throw new NotFoundException(error); @@ -988,16 +987,15 @@ export class UserRepository { async deleteSessionBySessionId(sessionId: string, userId: string): Promise<{ message: string }> { try { - const result = await this.prisma.session.deleteMany({ + await this.prisma.session.delete({ where: { id: sessionId, userId } }); - if (0 === result.count) { - throw new RpcException(new NotFoundException(`Session not found for userId: ${userId}`)); - } - return { message: 'Session deleted successfully' }; } catch (error) { + if ('P2025' === error.code) { + throw new RpcException(new NotFoundException(`Session not found for userId: ${userId}`)); + } this.logger.error(`Error in Deleting Session: ${error.message}`); throw error; }