diff --git a/apps/api-gateway/src/authz/authz.controller.ts b/apps/api-gateway/src/authz/authz.controller.ts index c31c8a1f1..dbc60f931 100644 --- a/apps/api-gateway/src/authz/authz.controller.ts +++ b/apps/api-gateway/src/authz/authz.controller.ts @@ -1,10 +1,14 @@ import { + BadRequestException, Body, Controller, + Delete, + ForbiddenException, Get, HttpStatus, Logger, Param, + ParseUUIDPipe, Post, Query, Req, @@ -15,10 +19,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 +49,12 @@ 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'; +import * as useragent from 'express-useragent'; + @Controller('auth') @ApiTags('auth') @UseFilters(CustomExceptionFilter) @@ -158,9 +177,19 @@ 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 expressUa = useragent.parse(ua); + const device = { + 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); const finalResponse: IResponseType = { statusCode: HttpStatus.OK, @@ -331,4 +360,82 @@ 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 sessions 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( + @User() reqUser: user, + @Res() res: Response, + @Param( + 'userId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(`Invalid format for User Id`); + } + }) + ) + 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 = { + 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/api-gateway/src/authz/authz.service.ts b/apps/api-gateway/src/authz/authz.service.ts index f9dc909f6..1fb50dc03 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'; @@ -17,8 +17,9 @@ 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 { ISessionDetails } from 'apps/user/interfaces/user.interface'; +import { IRestrictedUserSession, ISessionDetails } from 'apps/user/interfaces/user.interface'; import { UserLogoutDto } from './dtos/user-logout.dto'; +import type { Prisma } from '@prisma/client'; @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: 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); } @@ -60,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); } @@ -76,6 +81,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/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/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/apps/user/dtos/login-user.dto.ts b/apps/user/dtos/login-user.dto.ts index 2a4aa64db..0c344161b 100644 --- a/apps/user/dtos/login-user.dto.ts +++ b/apps/user/dtos/login-user.dto.ts @@ -1,23 +1,29 @@ -import { trim } from '@credebl/common/cast.helper'; +import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator'; + import { ApiProperty } from '@nestjs/swagger'; +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' }) - @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: '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() - @IsBoolean({ message: 'isPasskey should be boolean' }) - isPasskey?: boolean; + @ApiProperty({ example: 'false' }) + @IsOptional() + clientInfo?: Prisma.JsonValue; } diff --git a/apps/user/interfaces/user.interface.ts b/apps/user/interfaces/user.interface.ts index a0c3ba997..d080d596a 100644 --- a/apps/user/interfaces/user.interface.ts +++ b/apps/user/interfaces/user.interface.ts @@ -179,6 +179,7 @@ export interface IUserSignIn { email: string; password: string; isPasskey?: boolean; + clientInfo: Prisma.JsonValue; } export interface ISession { @@ -192,6 +193,7 @@ export interface ISession { accountId?: string; sessionType?: string; expiresAt?: Date; + clientInfo?: Prisma.JsonValue | null; } export interface IUpdateAccountDetails { @@ -290,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 c20942086..38f8ef0a8 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, @@ -31,6 +32,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 @@ -137,7 +139,7 @@ export class UserRepository { */ async getSession(sessionId: string): Promise { try { - return this.prisma.session.findUnique({ + return await this.prisma.session.findUnique({ where: { id: sessionId } @@ -687,7 +689,8 @@ export class UserRepository { refreshToken, accountId, sessionType, - expiresAt + expiresAt, + ...(tokenDetails.clientInfo ? { clientInfo: tokenDetails.clientInfo } : { clientInfo: { clientToken: true } }) } }); return sessionResponse; @@ -697,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; @@ -974,6 +985,22 @@ export class UserRepository { } } + async deleteSessionBySessionId(sessionId: string, userId: string): Promise<{ message: string }> { + try { + await this.prisma.session.delete({ + where: { id: sessionId, 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; + } + } + 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..5731998f1 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, @@ -84,11 +85,26 @@ 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); } + @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..474dc6752 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, @@ -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'; @@ -435,7 +436,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 +477,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); @@ -523,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 { @@ -578,6 +590,24 @@ export class UserService { } } + async userSessions(userId: string): Promise { + try { + 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); + } + } + + async deleteSession(sessionId: string, userId: string): Promise<{ message: string }> { + try { + return await 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', 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; 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..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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c325bcf7..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 @@ -3000,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'} @@ -4838,12 +4845,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 +5355,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==} @@ -9178,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