diff --git a/apps/backend/package.json b/apps/backend/package.json index 9ae902c..f23a312 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -23,11 +23,13 @@ "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/jwt": "^11.0.1", "@nestjs/platform-express": "^11.0.1", "@nestjs/typeorm": "^11.0.0", "bcrypt": "^6.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", + "passport-jwt": "^4.0.1", "pg": "^8.16.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", @@ -43,6 +45,7 @@ "@types/express": "^5.0.0", "@types/jest": "^30.0.0", "@types/node": "^22.10.7", + "@types/passport-jwt": "^4.0.1", "@types/supertest": "^6.0.2", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", diff --git a/apps/backend/src/auth/auth.controller.spec.ts b/apps/backend/src/auth/auth.controller.spec.ts index 2e29a81..45ff2d5 100644 --- a/apps/backend/src/auth/auth.controller.spec.ts +++ b/apps/backend/src/auth/auth.controller.spec.ts @@ -3,9 +3,13 @@ import { AuthController } from '@/auth/auth.controller'; import { AuthService } from '@/auth/auth.service'; import { RegisterUserDto } from '@/auth/dto/register-user.dto'; import { UserResponseDto } from '@/users/user-response.dto'; +import { LoginUserDto } from '@/auth/dto/login-user.dto'; +import { AccessTokenDto } from '@/auth/dto/access-token.dto'; +import { ConflictException, UnauthorizedException } from '@nestjs/common'; const mockAuthService = { register: jest.fn(), + login: jest.fn(), }; describe('AuthController', () => { @@ -58,5 +62,84 @@ describe('AuthController', () => { expect(authService.register).toHaveBeenCalledWith(mockRegisterUserDto); expect(actualUserResponseDto).toEqual(mockUserResponseDto); }); + + it('should propagate ConflictException from the Auth Service', async () => { + // Arrange + const mockRegisterUserDto: RegisterUserDto = { + name: 'Test User', + email: 'test.user@test.com', + password: 'StrongPassword@123', + }; + + (authService.register as jest.Mock).mockRejectedValue( + new ConflictException('Unable to complete registration at this time.'), + ); + + // Act & Assert + await expect( + controller.register(mockRegisterUserDto), + ).rejects.toMatchObject({ + name: 'ConflictException', + message: 'Unable to complete registration at this time.', + }); + }); + + it('should propagate any general error from the Auth Service', async () => { + // Arrange + const mockRegisterUserDto: RegisterUserDto = { + name: 'Test User', + email: 'test.user@test.com', + password: 'StrongPassword@123', + }; + + (authService.register as jest.Mock).mockRejectedValue( + new Error('Something went wrong.'), + ); + + // Act & Assert + await expect(controller.register(mockRegisterUserDto)).rejects.toThrow( + 'Something went wrong.', + ); + }); + }); + + describe('login', () => { + it('should call the login method of the Auth Service', async () => { + // Arrange + const mockLoginUserDto: LoginUserDto = { + email: 'test.user@test.com', + password: 'PlainTextPassword', + }; + const expectedResponse: AccessTokenDto = { + access_token: 'SomeReallyLongAccessTokenText', + }; + + (authService.login as jest.Mock).mockResolvedValue(expectedResponse); + + // Act + const actualResponse = await controller.login(mockLoginUserDto); + + // Assert + expect(authService.login).toHaveBeenCalledWith(mockLoginUserDto); + expect(actualResponse).toEqual(expectedResponse); + }); + + it('should propagate UnauthorizedException from the Auth Service', async () => { + // Arrange + const mockLoginUserDto: LoginUserDto = { + email: 'test.user@test.com', + password: 'PlainTextPassword', + }; + + (authService.login as jest.Mock).mockRejectedValue( + new UnauthorizedException('Invalid credentials.'), + ); + + // Act & Assert + await expect(controller.login(mockLoginUserDto)).rejects.toMatchObject({ + name: 'UnauthorizedException', + message: 'Invalid credentials.', + }); + }); }); }); diff --git a/apps/backend/src/auth/auth.controller.ts b/apps/backend/src/auth/auth.controller.ts index 46bd950..7726eda 100644 --- a/apps/backend/src/auth/auth.controller.ts +++ b/apps/backend/src/auth/auth.controller.ts @@ -2,13 +2,22 @@ import { Body, Controller, Post } from '@nestjs/common'; import { RegisterUserDto } from '@/auth/dto/register-user.dto'; import { AuthService } from '@/auth/auth.service'; import { UserResponseDto } from '@/users/user-response.dto'; +import { LoginUserDto } from '@/auth/dto/login-user.dto'; +import { AccessTokenDto } from '@/auth/dto/access-token.dto'; @Controller('auth') export class AuthController { constructor(private authService: AuthService) {} @Post('register') - register(@Body() registerUserDto: RegisterUserDto): Promise { + async register( + @Body() registerUserDto: RegisterUserDto, + ): Promise { return this.authService.register(registerUserDto); } + + @Post('login') + async login(@Body() loginUserDto: LoginUserDto): Promise { + return this.authService.login(loginUserDto); + } } diff --git a/apps/backend/src/auth/auth.module.ts b/apps/backend/src/auth/auth.module.ts index 908e053..01788fd 100644 --- a/apps/backend/src/auth/auth.module.ts +++ b/apps/backend/src/auth/auth.module.ts @@ -1,11 +1,25 @@ import { Module } from '@nestjs/common'; -import { AuthController } from './auth.controller'; -import { AuthService } from './auth.service'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { User } from '@/users/user.entity'; +import { AuthController } from '@/auth/auth.controller'; +import { AuthService } from '@/auth/auth.service'; +import { UsersModule } from '@/users/users.module'; +import { JwtModule } from '@nestjs/jwt'; +import { ConfigModule, ConfigService } from '@nestjs/config'; @Module({ - imports: [TypeOrmModule.forFeature([User])], + imports: [ + UsersModule, + JwtModule.registerAsync({ + imports: [ConfigModule], + useFactory: (configService: ConfigService) => ({ + global: true, + secret: configService.getOrThrow('JWT_SECRET'), + signOptions: { + expiresIn: '60m', + }, + }), + inject: [ConfigService], + }), + ], controllers: [AuthController], providers: [AuthService], }) diff --git a/apps/backend/src/auth/auth.service.spec.ts b/apps/backend/src/auth/auth.service.spec.ts index d792295..8f88677 100644 --- a/apps/backend/src/auth/auth.service.spec.ts +++ b/apps/backend/src/auth/auth.service.spec.ts @@ -4,11 +4,16 @@ import { DataSource } from 'typeorm'; import { RegisterUserDto } from '@/auth/dto/register-user.dto'; import { User } from '@/users/user.entity'; import * as bcrypt from 'bcrypt'; -import { ConflictException } from '@nestjs/common'; +import { ConflictException, UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { UsersService } from '@/users/users.service'; +import { JwtService } from '@nestjs/jwt'; +import { LoginUserDto } from '@/auth/dto/login-user.dto'; +import { AccessTokenDto } from '@/auth/dto/access-token.dto'; jest.mock('bcrypt', () => ({ hash: jest.fn(), + compare: jest.fn(), })); const mockQueryRunner = { @@ -30,10 +35,20 @@ const mockConfigService = { get: jest.fn(), }; +const mockUsersService = { + findOneByEmail: jest.fn(), +}; + +const mockJwtService = { + signAsync: jest.fn(), +}; + describe('AuthService', () => { let service: AuthService; let dataSource: DataSource; let configService: ConfigService; + let usersService: UsersService; + let jwtService: JwtService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -47,12 +62,22 @@ describe('AuthService', () => { provide: ConfigService, useValue: mockConfigService, }, + { + provide: UsersService, + useValue: mockUsersService, + }, + { + provide: JwtService, + useValue: mockJwtService, + }, ], }).compile(); service = module.get(AuthService); dataSource = module.get(DataSource); configService = module.get(ConfigService); + usersService = module.get(UsersService); + jwtService = module.get(JwtService); jest.clearAllMocks(); }); @@ -257,4 +282,95 @@ describe('AuthService', () => { ); }); }); + + describe('login', () => { + it('should return the access token when credentials are valid', async () => { + // Arrange + const mockLoginUserDto: LoginUserDto = { + email: 'test.user@test.com', + password: 'PlainTextPassword', + }; + + const testUser = new User(); + testUser.id = 1; + testUser.email = 'test.user@test.com'; + testUser.password = 'SomeRandomHashedPassword'; + + const expectedPayload = { + sub: testUser.id, + email: testUser.email, + }; + const expectedAccessToken = 'SomeReallyLongAccessTokenText'; + const expectedResponse: AccessTokenDto = { + access_token: expectedAccessToken, + }; + + (usersService.findOneByEmail as jest.Mock).mockResolvedValue(testUser); + (bcrypt.compare as jest.Mock).mockResolvedValue(true); + (jwtService.signAsync as jest.Mock).mockReturnValue(expectedAccessToken); + + // Act + const response = await service.login(mockLoginUserDto); + + // Assert + expect(usersService.findOneByEmail).toHaveBeenCalledWith( + mockLoginUserDto.email, + ); + expect(bcrypt.compare).toHaveBeenCalledWith( + mockLoginUserDto.password, + testUser.password, + ); + expect(jwtService.signAsync).toHaveBeenCalledWith(expectedPayload); + expect(response).toEqual(expectedResponse); + }); + + it('should throw Unauthorized exception when user does not exists', async () => { + // Arrange + const mockLoginUserDto: LoginUserDto = { + email: 'nouser@test.com', + password: 'PlainTextPassword', + }; + + (usersService.findOneByEmail as jest.Mock).mockResolvedValue(null); + + // Act & Assert + await expect(service.login(mockLoginUserDto)).rejects.toBeInstanceOf( + UnauthorizedException, + ); + expect(usersService.findOneByEmail).toHaveBeenCalledWith( + mockLoginUserDto.email, + ); + expect(bcrypt.compare).not.toHaveBeenCalled(); + expect(jwtService.signAsync).not.toHaveBeenCalled(); + }); + + it('should throw Unauthorized exception when passwords do not match', async () => { + // Arrange + const mockLoginUserDto: LoginUserDto = { + email: 'test.user@test.com', + password: 'PlainTextPassword', + }; + + const testUser = new User(); + testUser.id = 1; + testUser.email = 'test.user@test.com'; + testUser.password = 'SomeRandomHashedPassword'; + + (usersService.findOneByEmail as jest.Mock).mockResolvedValue(testUser); + (bcrypt.compare as jest.Mock).mockResolvedValue(false); + + // Act & Assert + await expect(service.login(mockLoginUserDto)).rejects.toBeInstanceOf( + UnauthorizedException, + ); + expect(usersService.findOneByEmail).toHaveBeenCalledWith( + mockLoginUserDto.email, + ); + expect(bcrypt.compare).toHaveBeenCalledWith( + mockLoginUserDto.password, + testUser.password, + ); + expect(jwtService.signAsync).not.toHaveBeenCalled(); + }); + }); }); diff --git a/apps/backend/src/auth/auth.service.ts b/apps/backend/src/auth/auth.service.ts index ea0d851..888e11d 100644 --- a/apps/backend/src/auth/auth.service.ts +++ b/apps/backend/src/auth/auth.service.ts @@ -1,16 +1,26 @@ -import { ConflictException, Injectable } from '@nestjs/common'; +import { + ConflictException, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; import { RegisterUserDto } from '@/auth/dto/register-user.dto'; import * as bcrypt from 'bcrypt'; import { DataSource } from 'typeorm'; import { User } from '@/users/user.entity'; import { UserResponseDto } from '@/users/user-response.dto'; import { ConfigService } from '@nestjs/config'; +import { UsersService } from '@/users/users.service'; +import { JwtService } from '@nestjs/jwt'; +import { LoginUserDto } from '@/auth/dto/login-user.dto'; +import { AccessTokenDto } from '@/auth/dto/access-token.dto'; @Injectable() export class AuthService { constructor( private dataSource: DataSource, private configService: ConfigService, + private usersService: UsersService, + private jwtService: JwtService, ) {} async register(registerUserDto: RegisterUserDto): Promise { const { name, email, password } = registerUserDto; @@ -62,4 +72,37 @@ export class AuthService { await queryRunner.release(); } } + + async login(loginUserDto: LoginUserDto): Promise { + const lowerCaseEmail = loginUserDto.email.toLowerCase().trim(); + const user = await this.usersService.findOneByEmail(lowerCaseEmail); + + if (!user) { + throw new UnauthorizedException({ + message: 'Invalid credentials.', + }); + } + + const isPasswordCorrect = await bcrypt.compare( + loginUserDto.password, + user.password, + ); + + if (!isPasswordCorrect) { + throw new UnauthorizedException({ + message: 'Invalid credentials.', + }); + } + + const payload = { + sub: user.id, + email: user.email, + }; + + const accessToken = await this.jwtService.signAsync(payload); + + return { + access_token: accessToken, + }; + } } diff --git a/apps/backend/src/auth/dto/access-token.dto.ts b/apps/backend/src/auth/dto/access-token.dto.ts new file mode 100644 index 0000000..3668c60 --- /dev/null +++ b/apps/backend/src/auth/dto/access-token.dto.ts @@ -0,0 +1,3 @@ +export class AccessTokenDto { + readonly access_token: string; +} diff --git a/apps/backend/src/auth/dto/login-user.dto.ts b/apps/backend/src/auth/dto/login-user.dto.ts new file mode 100644 index 0000000..690dcb4 --- /dev/null +++ b/apps/backend/src/auth/dto/login-user.dto.ts @@ -0,0 +1,13 @@ +import { IsEmail, IsNotEmpty, IsString, MaxLength } from 'class-validator'; + +export class LoginUserDto { + @IsNotEmpty() + @IsEmail() + @MaxLength(255) + email: string; + + @IsNotEmpty() + @IsString() + @MaxLength(100) + password: string; +} diff --git a/apps/backend/src/users/users.module.ts b/apps/backend/src/users/users.module.ts index c75728c..7b13af6 100644 --- a/apps/backend/src/users/users.module.ts +++ b/apps/backend/src/users/users.module.ts @@ -1,7 +1,10 @@ import { Module } from '@nestjs/common'; import { UsersService } from '@/users/users.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { User } from '@/users/user.entity'; @Module({ + imports: [TypeOrmModule.forFeature([User])], providers: [UsersService], exports: [UsersService], }) diff --git a/apps/backend/src/users/users.service.spec.ts b/apps/backend/src/users/users.service.spec.ts index 74933c6..df9d9b5 100644 --- a/apps/backend/src/users/users.service.spec.ts +++ b/apps/backend/src/users/users.service.spec.ts @@ -1,18 +1,66 @@ import { Test, TestingModule } from '@nestjs/testing'; import { UsersService } from '@/users/users.service'; +import { Repository } from 'typeorm'; +import { User } from '@/users/user.entity'; +import { getRepositoryToken } from '@nestjs/typeorm'; + +const mockQueryBuilder = { + where: jest.fn().mockReturnThis(), + addSelect: jest.fn().mockReturnThis(), + getOne: jest.fn(), +}; + +const mockUserRepository = { + createQueryBuilder: jest.fn().mockReturnValue(mockQueryBuilder), +}; describe('UsersService', () => { let service: UsersService; + let userRepository: Repository; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [UsersService], + providers: [ + UsersService, + { + provide: getRepositoryToken(User), + useValue: mockUserRepository, + }, + ], }).compile(); service = module.get(UsersService); + userRepository = module.get>(getRepositoryToken(User)); + + jest.clearAllMocks(); }); it('should be defined', () => { expect(service).toBeDefined(); }); + + describe('findOneByEmail', () => { + it('should call the query builder to find a user by email', async () => { + // Arrange + const mockEmail = 'test.user@test.com'; + const expectedUser = new User(); + + (userRepository.createQueryBuilder().getOne as jest.Mock).mockReturnValue( + expectedUser, + ); + + // Act + const user = await service.findOneByEmail(mockEmail); + + // Assert + expect(userRepository.createQueryBuilder).toHaveBeenCalledWith('user'); + expect(mockQueryBuilder.where).toHaveBeenCalledWith( + 'user.email = :email', + { email: mockEmail }, + ); + expect(mockQueryBuilder.addSelect).toHaveBeenCalledWith('user.password'); + expect(mockQueryBuilder.getOne).toHaveBeenCalled(); + expect(user).toEqual(expectedUser); + }); + }); }); diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts index c105258..2435615 100644 --- a/apps/backend/src/users/users.service.ts +++ b/apps/backend/src/users/users.service.ts @@ -1,9 +1,20 @@ import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { User } from '@/users/user.entity'; +import { Repository } from 'typeorm'; -/** - * @Injectable() - * Service responsible for all user-related business logic and data access. - * This service will be used for operations like finding users, updating profiles, etc. - */ @Injectable() -export class UsersService {} +export class UsersService { + constructor( + @InjectRepository(User) + private userRepository: Repository, + ) {} + + async findOneByEmail(email: string): Promise { + return this.userRepository + .createQueryBuilder('user') + .where('user.email = :email', { email }) + .addSelect('user.password') + .getOne(); + } +} diff --git a/package-lock.json b/package-lock.json index b7077b9..79089f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,11 +21,13 @@ "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/jwt": "^11.0.1", "@nestjs/platform-express": "^11.0.1", "@nestjs/typeorm": "^11.0.0", "bcrypt": "^6.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", + "passport-jwt": "^4.0.1", "pg": "^8.16.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", @@ -41,6 +43,7 @@ "@types/express": "^5.0.0", "@types/jest": "^30.0.0", "@types/node": "^22.10.7", + "@types/passport-jwt": "^4.0.1", "@types/supertest": "^6.0.2", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", @@ -5105,6 +5108,19 @@ } } }, + "node_modules/@nestjs/jwt": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.1.tgz", + "integrity": "sha512-HXSsc7SAnCnjA98TsZqrE7trGtHDnYXWp4Ffy6LwSmck1QvbGYdMzBquXofX5l6tIRpeY4Qidl2Ti2CVG77Pdw==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "9.0.10", + "jsonwebtoken": "9.0.2" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" + } + }, "node_modules/@nestjs/platform-express": { "version": "11.1.6", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.6.tgz", @@ -7075,6 +7091,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -7089,16 +7115,53 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.18.8", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.8.tgz", "integrity": "sha512-pAZSHMiagDR7cARo/cch1f3rXy0AEXwsVsVH09FcyeJVAzCnGgmYis7P3JidtTUjyadhTeSo8TgRPswstghDaw==", - "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, "node_modules/@types/pegjs": { "version": "0.10.6", "resolved": "https://registry.npmjs.org/@types/pegjs/-/pegjs-0.10.6.tgz", @@ -9076,6 +9139,12 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -10353,6 +10422,15 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -14103,6 +14181,28 @@ ], "license": "MIT" }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/jsox": { "version": "1.2.123", "resolved": "https://registry.npmjs.org/jsox/-/jsox-1.2.123.tgz", @@ -14113,6 +14213,27 @@ "jsox": "lib/cli.js" } }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/karma": { "version": "6.4.4", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", @@ -14799,6 +14920,42 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -14813,6 +14970,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -16517,6 +16680,24 @@ "node": ">= 0.8" } }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "license": "MIT", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -17647,7 +17828,6 @@ "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -19849,7 +20029,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "devOptional": true, "license": "MIT" }, "node_modules/unique-filename": {