diff --git a/package.json b/package.json index 07529c6..f5be7bc 100644 --- a/package.json +++ b/package.json @@ -23,12 +23,18 @@ "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/jwt": "^11.0.0", "@nestjs/mapped-types": "*", + "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", "@nestjs/swagger": "^11.2.0", "@prisma/client": "^6.9.0", + "argon2": "^0.43.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", + "cookie-parser": "^1.4.7", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", "prisma": "^6.9.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1" @@ -41,9 +47,12 @@ "@nestjs/testing": "^11.0.1", "@swc/cli": "^0.6.0", "@swc/core": "^1.10.7", + "@types/cookie-parser": "^1.4.9", "@types/express": "^5.0.0", "@types/jest": "^29.5.14", "@types/node": "^22.10.7", + "@types/passport": "^1.0.17", + "@types/passport-jwt": "^4.0.1", "@types/supertest": "^6.0.2", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f2f4c3a..fea785b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,77 +8,16 @@ datasource db { url = env("POSTGRES_URI") } -model Movie { +model User { id String @id @default(uuid()) - title String - description String? + email String @unique + password String - releaseYear Int @map("release_year") - rating Float @default(0.0) + name String - isAvailabel Boolean @default(false) @map("is_available") - - genre Genre @default(DRAMMA) - - poster MoviePoster @relation(fields: [posterId], references: [id]) - posterId String @unique @map("poster_id") - - reviews Review[] @relation("movie_reviews") - actors Actor[] @relation("movie_actors") - - createdAt DateTime @default(now()) @map("created_at") + createdAt DateTime @default(now()) @map("create_at") updatedAt DateTime @updatedAt @map("updated_at") - @@index([releaseYear, title]) - @@map("movies") -} - -model MoviePoster { - id String @id @default(uuid()) - - url String - - movie Movie? @relation() - - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - - @@map("movie_posters") -} - -model Review { - id String @id @default(uuid()) - - text String - rating Decimal @default(0.0) - - movie Movie @relation("movie_reviews", fields: [movieId], references: [id], onDelete: Cascade) - movieId String @map("movie_id") - - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - - @@map("reviews") -} - -model Actor { - id String @id @default(uuid()) - - name String - movies Movie[] @relation("movie_actors") - - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - - @@map("actors") -} - -enum Genre { - ACTION - COMEDY - DRAMMA - HORROR - - @@map("enum_genres") + @@map("users") } diff --git a/src/actor/actor.controller.ts b/src/actor/actor.controller.ts deleted file mode 100644 index d8bb443..0000000 --- a/src/actor/actor.controller.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Body, Controller, Post } from '@nestjs/common'; -import { ActorService } from './actor.service'; -import { CreateActorDto } from './dto/create-actor.dto'; - -@Controller('actors') -export class ActorController { - constructor(private readonly actorService: ActorService) {} - - @Post() - create(@Body() dto: CreateActorDto) { - return this.actorService.create(dto); - } -} diff --git a/src/actor/actor.module.ts b/src/actor/actor.module.ts deleted file mode 100644 index f637ec2..0000000 --- a/src/actor/actor.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ActorService } from './actor.service'; -import { ActorController } from './actor.controller'; - -@Module({ - controllers: [ActorController], - providers: [ActorService], -}) -export class ActorModule {} diff --git a/src/actor/actor.service.ts b/src/actor/actor.service.ts deleted file mode 100644 index dc32019..0000000 --- a/src/actor/actor.service.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { Actor } from 'generated/prisma'; -import { PrismaService } from '../prisma/prisma.service'; -import { CreateActorDto } from './dto/create-actor.dto'; - -@Injectable() -export class ActorService { - constructor(private prismaService: PrismaService) {} - - async create(dto: CreateActorDto): Promise { - const { name } = dto; - return this.prismaService.actor.create({ - data: { - name, - }, - }); - } -} diff --git a/src/actor/dto/create-actor.dto.ts b/src/actor/dto/create-actor.dto.ts deleted file mode 100644 index cbccb7a..0000000 --- a/src/actor/dto/create-actor.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { IsNotEmpty, IsString, MaxLength } from 'class-validator'; - -export class CreateActorDto { - @IsNotEmpty() - @IsString() - @MaxLength(64) - name: string; -} diff --git a/src/app.controller.ts b/src/app.controller.ts index 010034b..24fa193 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,26 +1,4 @@ -import { - Body, - Controller, - Get, - Post, - UseGuards, - UsePipes, -} from '@nestjs/common'; -import { UserAgentDecorator } from './common/decorators/user-agent.decorator'; -import { AuthGuard } from './common/guards/auth.guard'; -import { StringToLowercasePipe } from './common/pipes/string-to-lowercase.pipe'; +import { Controller } from '@nestjs/common'; @Controller() -export class AppController { - @UsePipes(StringToLowercasePipe) - @Post() - create(@Body('title') title: string) { - return `Movie ${title}`; - } - - @UseGuards(AuthGuard) - @Get() - getProfile(@UserAgentDecorator() userAgent: string) { - return { name: 'FirstName', age: 22, userAgent }; - } -} +export class AppController {} diff --git a/src/app.module.ts b/src/app.module.ts index 3752d2d..c5c5e01 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,11 +1,8 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AppController } from './app.controller'; -import { TaskModule } from './task/task.module'; -import { MovieModule } from './movie/movie.module'; -import { ReviewModule } from './review/review.module'; -import { ActorModule } from './actor/actor.module'; import { PrismaModule } from './prisma/prisma.module'; +import { AuthModule } from './auth/auth.module'; @Module({ imports: [ @@ -13,10 +10,7 @@ import { PrismaModule } from './prisma/prisma.module'; isGlobal: true, }), PrismaModule, - TaskModule, - MovieModule, - ReviewModule, - ActorModule, + AuthModule, ], controllers: [AppController], providers: [], diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts new file mode 100644 index 0000000..46f1a9f --- /dev/null +++ b/src/auth/auth.controller.ts @@ -0,0 +1,92 @@ +import { + Body, + Controller, + Get, + HttpCode, + HttpStatus, + Post, + Req, + Res, +} from '@nestjs/common'; +import { + ApiBadRequestResponse, + ApiConflictResponse, + ApiNotFoundResponse, + ApiOkResponse, + ApiOperation, + ApiUnauthorizedResponse, +} from '@nestjs/swagger'; +import { AuthService } from './auth.service'; +import { Authorization } from './decorators/authorixation.decorator'; +import { Authorized } from './decorators/authorized.decorator'; +import { AuthResponse } from './dto/auth.dto'; +import { LoginRequest } from './dto/login.dto'; +import { RegisterRequest } from './dto/register.dto'; +import type { Request, Response } from 'express'; +import { User } from 'generated/prisma'; + +@Controller('auth') +export class AuthController { + constructor(private readonly authService: AuthService) {} + + @ApiOperation({ + summary: 'Register user', + description: 'Register user by email and password', + }) + @ApiBadRequestResponse({ description: 'Validation error' }) + @ApiOkResponse({ type: AuthResponse }) + @ApiConflictResponse({ + example: `User with email john-doe@example.com already exist`, + }) + @Post('register') + @HttpCode(HttpStatus.CREATED) + async register( + @Res({ passthrough: true }) response: Response, + @Body() dto: RegisterRequest, + ) { + return this.authService.register(response, dto); + } + + @ApiBadRequestResponse({ description: 'Validation error' }) + @ApiOkResponse({ type: AuthResponse }) + @ApiNotFoundResponse({ description: 'User not found' }) + @ApiOperation({ + summary: 'Login user', + description: 'Login via email and password', + }) + @Post('login') + async login( + @Res({ passthrough: true }) response: Response, + @Body() dto: LoginRequest, + ) { + return this.authService.login(response, dto); + } + + @ApiOperation({ + summary: 'Logout user', + }) + @Post('logout') + logout(@Res({ passthrough: true }) response: Response) { + return this.authService.logout(response); + } + + @ApiOperation({ + summary: 'Update user refresh token', + }) + @ApiOkResponse({ type: AuthResponse }) + @ApiUnauthorizedResponse({ description: 'User should auth' }) + @Post('refresh') + async refresh( + @Req() request: Request, + @Res({ passthrough: true }) response: Response, + ) { + return this.authService.refresh(request, response); + } + + @Authorization() + @Get('me') + @HttpCode(HttpStatus.OK) + me(@Authorized('id') user: User['id']) { + return { id: user }; + } +} diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts new file mode 100644 index 0000000..feb6715 --- /dev/null +++ b/src/auth/auth.module.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { JwtModule } from '@nestjs/jwt'; +import { PassportModule } from '@nestjs/passport'; +import { getJwtConfig } from '../config/jwt.config'; +import { AuthService } from './auth.service'; +import { AuthController } from './auth.controller'; +import { JwtStrategy } from './strategies/jwt.strategy'; + +@Module({ + imports: [ + PassportModule, + JwtModule.registerAsync({ + imports: [ConfigModule], + useFactory: getJwtConfig, + inject: [ConfigService], + }), + ], + controllers: [AuthController], + providers: [AuthService, JwtStrategy], +}) +export class AuthModule {} diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts new file mode 100644 index 0000000..8cf1605 --- /dev/null +++ b/src/auth/auth.service.ts @@ -0,0 +1,165 @@ +import { + ConflictException, + Injectable, + NotFoundException, + UnauthorizedException, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { JwtService } from '@nestjs/jwt'; +import { hash, verify } from 'argon2'; +import type { Request, Response } from 'express'; +import { PrismaService } from '../prisma/prisma.service'; +import { isDev } from '../utils/is-dev'; +import { LoginRequest } from './dto/login.dto'; +import { RegisterRequest } from './dto/register.dto'; +import { User } from 'generated/prisma'; +import { JwtPayload } from './interfaces/jwt.interface'; + +@Injectable() +export class AuthService { + private readonly COOKIE_DOMAIN: string; + private readonly JWT_ACCESS_TOKEN_TTL: string; + private readonly JWT_REFRESH_TOKEN_TTL: string; + + constructor( + private readonly prismaService: PrismaService, + private readonly configService: ConfigService, + private readonly jwtService: JwtService, + ) { + this.COOKIE_DOMAIN = this.configService.getOrThrow('COOKIE_DOMAIN'); + this.JWT_ACCESS_TOKEN_TTL = this.configService.getOrThrow( + 'JWT_ACCESS_TOKEN_TTL', + ); + this.JWT_REFRESH_TOKEN_TTL = this.configService.getOrThrow( + 'JWT_REFRESH_TOKEN_TTL', + ); + } + + async register(res: Response, dto: RegisterRequest) { + const { email, name, password } = dto; + + const existUser = await this.prismaService.user.findUnique({ + where: { + email, + }, + }); + + if (existUser) { + throw new ConflictException(`User with email ${email} already exist`); + } + + const user = await this.prismaService.user.create({ + data: { + email, + name, + password: await hash(password), + }, + }); + + return this.auth(res, user.id); + } + + async login(res: Response, dto: LoginRequest) { + const { email, password } = dto; + + const user = await this.prismaService.user.findUnique({ + where: { + email, + }, + select: { + id: true, + password: true, + }, + }); + + if (!user) { + throw new ConflictException('Wrong email or password'); + } + + const isValidPassword = await verify(user.password, password); + if (!isValidPassword) { + throw new ConflictException('Wrong email or password'); + } + + return this.auth(res, user.id); + } + + logout(res: Response) { + this.setCookie(res, 'refreshToken', new Date(0)); + return true; + } + + auth(res: Response, id: User['id']) { + const { accessToken, refreshToken } = this.generateTokens(id); + + this.setCookie(res, refreshToken, new Date(Date.now() + 60 * 60 * 24 * 7)); + + return { accessToken }; + } + + async validate(id: string) { + const user = await this.prismaService.user.findUnique({ + where: { + id, + }, + }); + + if (!user) { + throw new NotFoundException(); + } + + return user; + } + + public async refresh(req: Request, res: Response) { + const refreshToken = req.cookies['refreshToken'] as string; + + if (!refreshToken) { + throw new UnauthorizedException(); + } + + const payload: JwtPayload = await this.jwtService.verifyAsync(refreshToken); + if (payload) { + const user = await this.prismaService.user.findUnique({ + where: { + id: payload.id, + }, + select: { + id: true, + }, + }); + + if (!user) { + throw new NotFoundException('User not found'); + } + + this.auth(res, user.id); + } + } + + private generateTokens(id: User['id']) { + const payload: JwtPayload = { id }; + + const accessToken = this.jwtService.sign(payload, { + expiresIn: this.JWT_ACCESS_TOKEN_TTL, + }); + const refreshToken = this.jwtService.sign(payload, { + expiresIn: this.JWT_REFRESH_TOKEN_TTL, + }); + + return { + accessToken, + refreshToken, + }; + } + + private setCookie(res: Response, value: string, expires: Date) { + res.cookie('refreshToken', value, { + httpOnly: true, + domain: this.COOKIE_DOMAIN, + expires, + secure: !isDev(this.configService), + sameSite: isDev(this.configService) ? 'none' : 'lax', + }); + } +} diff --git a/src/auth/decorators/authorixation.decorator.ts b/src/auth/decorators/authorixation.decorator.ts new file mode 100644 index 0000000..06da0bb --- /dev/null +++ b/src/auth/decorators/authorixation.decorator.ts @@ -0,0 +1,6 @@ +import { applyDecorators, UseGuards } from '@nestjs/common'; +import { JwtGuard } from '../guards/auth.guard'; + +export function Authorization() { + return applyDecorators(UseGuards(JwtGuard)); +} diff --git a/src/auth/decorators/authorized.decorator.ts b/src/auth/decorators/authorized.decorator.ts new file mode 100644 index 0000000..4df6714 --- /dev/null +++ b/src/auth/decorators/authorized.decorator.ts @@ -0,0 +1,13 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { Request } from 'express'; +import { User } from 'generated/prisma'; + +export const Authorized = createParamDecorator( + (data: keyof User, ctx: ExecutionContext) => { + const req: Request = ctx.switchToHttp().getRequest(); + + const user = req.user; + + return data ? user![data] : user; + }, +); diff --git a/src/auth/dto/auth.dto.ts b/src/auth/dto/auth.dto.ts new file mode 100644 index 0000000..2bfd9ad --- /dev/null +++ b/src/auth/dto/auth.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class AuthResponse { + @ApiProperty({ + title: 'JWT Refresh Token', + example: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' + + 'eyJpZCI6Ijk4NGFlODk3LWY2ZjMtNGZhNC1iMjYxLWExOWU5NDY3OWVmMCIsImlhdCI6MTc0OTg5NTc0MywiZXhwIjoxNzQ5OTAyOTQzfQ.' + + 'SkW6SW9wcTy2Vc9NPVbg5Q6RExscH4Mgth5HerBRGrs', + }) + accessToken: string; +} diff --git a/src/auth/dto/login.dto.ts b/src/auth/dto/login.dto.ts new file mode 100644 index 0000000..e16336c --- /dev/null +++ b/src/auth/dto/login.dto.ts @@ -0,0 +1,33 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsEmail, + IsNotEmpty, + IsString, + MaxLength, + MinLength, +} from 'class-validator'; + +export class LoginRequest { + @ApiProperty({ + description: 'User email', + example: 'john-doe@example.com', + }) + @IsString({ message: 'Email should be a string' }) + @IsNotEmpty({ message: 'Email is required' }) + @IsEmail({}, { message: 'Not valid email' }) + email: string; + + @ApiProperty({ + description: 'User password', + example: '1q2w3e4r', + minLength: 6, + maxLength: 128, + }) + @IsString({ message: 'Password should be a string' }) + @IsNotEmpty({ message: 'Password is required' }) + @MinLength(6, { message: 'Password should contain more than 6 characters' }) + @MaxLength(128, { + message: 'Password should contain less than 128 characters', + }) + password: string; +} diff --git a/src/auth/dto/register.dto.ts b/src/auth/dto/register.dto.ts new file mode 100644 index 0000000..97e2e5a --- /dev/null +++ b/src/auth/dto/register.dto.ts @@ -0,0 +1,43 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsEmail, + IsNotEmpty, + IsString, + MaxLength, + MinLength, +} from 'class-validator'; + +export class RegisterRequest { + @ApiProperty({ + description: 'User name', + example: 'John Doe', + maxLength: 50, + }) + @IsString({ message: 'Name should be a string' }) + @IsNotEmpty({ message: 'Name is required' }) + @MaxLength(50, { message: "Name's length should be less 50 chars" }) + name: string; + + @ApiProperty({ + description: 'User email', + example: 'john-doe@example.com', + }) + @IsString({ message: 'Email should be a string' }) + @IsNotEmpty({ message: 'Email is required' }) + @IsEmail({}, { message: 'Not valid email' }) + email: string; + + @ApiProperty({ + description: 'User password', + example: '1q2w3e4r', + minLength: 6, + maxLength: 128, + }) + @IsString({ message: 'Password should be a string' }) + @IsNotEmpty({ message: 'Password is required' }) + @MinLength(6, { message: 'Password should contain more than 6 characters' }) + @MaxLength(128, { + message: 'Password should contain less than 128 characters', + }) + password: string; +} diff --git a/src/auth/guards/auth.guard.ts b/src/auth/guards/auth.guard.ts new file mode 100644 index 0000000..d594762 --- /dev/null +++ b/src/auth/guards/auth.guard.ts @@ -0,0 +1,3 @@ +import { AuthGuard } from '@nestjs/passport'; + +export class JwtGuard extends AuthGuard('jwt') {} diff --git a/src/auth/interfaces/jwt.interface.ts b/src/auth/interfaces/jwt.interface.ts new file mode 100644 index 0000000..8f65b1c --- /dev/null +++ b/src/auth/interfaces/jwt.interface.ts @@ -0,0 +1,5 @@ +import { User } from 'generated/prisma'; + +export interface JwtPayload { + id: User['id']; +} diff --git a/src/auth/strategies/jwt.strategy.ts b/src/auth/strategies/jwt.strategy.ts new file mode 100644 index 0000000..8b2037a --- /dev/null +++ b/src/auth/strategies/jwt.strategy.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { AuthService } from '../auth.service'; +import { JwtPayload } from '../interfaces/jwt.interface'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor( + private readonly authService: AuthService, + private readonly configService: ConfigService, + ) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: configService.getOrThrow('JWT_SECRET'), + algorithms: ['HS256'], + }); + } + + async validate(payload: JwtPayload) { + return await this.authService.validate(payload.id); + } +} diff --git a/src/common/decorators/user-agent.decorator.ts b/src/common/decorators/user-agent.decorator.ts deleted file mode 100644 index dcfea24..0000000 --- a/src/common/decorators/user-agent.decorator.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createParamDecorator, ExecutionContext } from '@nestjs/common'; -import { Request } from 'express'; - -export const UserAgentDecorator = createParamDecorator( - (_data: unknown, context: ExecutionContext) => { - const request: Request = context.switchToHttp().getRequest(); - return request.headers['user-agent']; - }, -); diff --git a/src/common/filters/all-exceptions.filter.ts b/src/common/filters/all-exceptions.filter.ts deleted file mode 100644 index 4e57242..0000000 --- a/src/common/filters/all-exceptions.filter.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { - ArgumentsHost, - Catch, - ExceptionFilter, - HttpException, - Logger, -} from '@nestjs/common'; -import { Request, Response } from 'express'; - -@Catch() -export class AllExceptionsFilter implements ExceptionFilter { - private readonly logger = new Logger(AllExceptionsFilter.name); - catch(exception: any, host: ArgumentsHost): any { - const context = host.switchToHttp(); - const response: Response = context.getResponse(); - const request: Request = context.getRequest(); - - const status = - exception instanceof HttpException ? exception.getStatus() : 500; - const message = - exception instanceof HttpException - ? exception.message - : 'Internal server error'; - - this.logger.error(message, exception); - - response.status(status).json({ - status, - message, - timestamp: new Date().toISOString(), - path: request.baseUrl, - }); - } -} diff --git a/src/common/guards/auth.guard.ts b/src/common/guards/auth.guard.ts deleted file mode 100644 index 29fabfa..0000000 --- a/src/common/guards/auth.guard.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { - CanActivate, - ExecutionContext, - Injectable, - UnauthorizedException, -} from '@nestjs/common'; -import { Request } from 'express'; -import { Observable } from 'rxjs'; - -@Injectable() -export class AuthGuard implements CanActivate { - canActivate( - context: ExecutionContext, - ): boolean | Promise | Observable { - const request: Request = context.switchToHttp().getRequest(); - const token = request.headers.authorization; - - if (!token || !token.startsWith('Bearer')) { - throw new UnauthorizedException('You are not authorized'); - } - - return true; - } -} diff --git a/src/common/interceptors/response.interceptor.ts b/src/common/interceptors/response.interceptor.ts deleted file mode 100644 index e709e00..0000000 --- a/src/common/interceptors/response.interceptor.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { - CallHandler, - ExecutionContext, - Injectable, - NestInterceptor, -} from '@nestjs/common'; -import { map, Observable } from 'rxjs'; - -@Injectable() -export class ResponseInterceptor implements NestInterceptor { - intercept( - context: ExecutionContext, - next: CallHandler, - ): Observable { - return next.handle().pipe( - map((data) => ({ - status: 'OK', - data, - })), - ); - } -} diff --git a/src/common/middlewares/logger/logger.middleware.ts b/src/common/middlewares/logger/logger.middleware.ts deleted file mode 100644 index ee993d3..0000000 --- a/src/common/middlewares/logger/logger.middleware.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Injectable, NestMiddleware } from '@nestjs/common'; -import type { NextFunction, Request, Response } from 'express'; - -@Injectable() -export class LoggerMiddleware implements NestMiddleware { - use(req: Request, res: Response, next: NextFunction) { - console.log(`Request ${req.method} ${req.baseUrl}`); - next(); - } -} - -export function loggerMiddlewareFunc( - req: Request, - res: Response, - next: NextFunction, -) { - console.log(`[${req.method}]: ${req.originalUrl}`); - next(); -} diff --git a/src/common/pipes/string-to-lowercase.pipe.ts b/src/common/pipes/string-to-lowercase.pipe.ts deleted file mode 100644 index 3b59f14..0000000 --- a/src/common/pipes/string-to-lowercase.pipe.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common'; - -@Injectable() -export class StringToLowercasePipe implements PipeTransform { - transform(value: any, metadata: ArgumentMetadata): any { - if (typeof value === 'string') { - return value.toLocaleLowerCase(); - } - - return value; - } -} diff --git a/src/config/jwt.config.ts b/src/config/jwt.config.ts new file mode 100644 index 0000000..e4f557b --- /dev/null +++ b/src/config/jwt.config.ts @@ -0,0 +1,17 @@ +import { ConfigService } from '@nestjs/config'; +import { JwtModuleOptions } from '@nestjs/jwt'; + +export async function getJwtConfig( + configService: ConfigService, +): Promise { + return { + secret: configService.getOrThrow('JWT_SECRET'), + signOptions: { + algorithm: 'HS256', + }, + verifyOptions: { + algorithms: ['HS256'], + ignoreExpiration: false, + }, + }; +} diff --git a/src/config/swagger.config.ts b/src/config/swagger.config.ts new file mode 100644 index 0000000..1b450c5 --- /dev/null +++ b/src/config/swagger.config.ts @@ -0,0 +1,11 @@ +import { DocumentBuilder } from '@nestjs/swagger'; + +export function getSwaggerConfig() { + return new DocumentBuilder() + .setTitle('Nest.js Course') + .setDescription('Documentation for nest.js') + .setVersion('0.0.1') + .setContact('Krylov Sergey', 'https://ksv741.tech', 'ksv741@mail.ru') + .addBearerAuth() + .build(); +} diff --git a/src/main.ts b/src/main.ts index ca26050..81d3e4d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,30 +1,16 @@ -import { NestFactory } from '@nestjs/core'; -import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; -import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; -import { AllExceptionsFilter } from './common/filters/all-exceptions.filter'; -import { ResponseInterceptor } from './common/interceptors/response.interceptor'; -import { loggerMiddlewareFunc } from './common/middlewares/logger/logger.middleware'; +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; +import * as cookieParser from 'cookie-parser'; +import { setupSwagger } from './utils/swagger'; async function bootstrap() { - const config = new DocumentBuilder() - .setTitle('Nest.js Course') - .setDescription('Documentation for nest.js') - .setVersion('0.0.1') - .setContact('Krylov Sergey', 'https://ksv741.tech', 'ksv741@mail.ru') - .setLicense('MIT', 'https://github.com') - .build(); const app = await NestFactory.create(AppModule); - const document = SwaggerModule.createDocument(app, config); - SwaggerModule.setup('/docs', app, document, { - jsonDocumentUrl: 'docs/swagger.json', - yamlDocumentUrl: 'docs/swagger.yaml', - customSiteTitle: 'Nest.js base course', - }); + + setupSwagger(app); + app.use(cookieParser()); app.useGlobalPipes(new ValidationPipe()); - app.use(loggerMiddlewareFunc); - app.useGlobalInterceptors(new ResponseInterceptor()); - app.useGlobalFilters(new AllExceptionsFilter()); + await app.listen(process.env.PORT ?? 3000); } bootstrap(); diff --git a/src/movie/dto/movie.dto.ts b/src/movie/dto/movie.dto.ts deleted file mode 100644 index 71eb9c0..0000000 --- a/src/movie/dto/movie.dto.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { - IsArray, - IsInt, - IsNotEmpty, - IsString, - IsUrl, - IsUUID, - Max, - Min, -} from 'class-validator'; -import { Actor } from 'generated/prisma'; - -export class MovieDto { - @ApiProperty({ - description: 'Название фильма', - example: 'Fight Club', - type: String, - }) - @IsNotEmpty() - @IsString() - title: string; - - @ApiProperty({ - description: 'Год выпуска', - example: 1995, - type: Number, - }) - @IsNotEmpty() - @IsInt() - @Min(1888) - @Max(new Date().getFullYear()) - releaseYear: number; - - @ApiProperty({ - description: 'Список актеров', - example: ['1234', '5678'], - type: [String], - }) - @IsArray() - @IsUUID('4', { each: true }) - actorIds: Actor['id'][]; - - @ApiProperty({ - description: 'URL постера', - example: 'https://storage.example.com/posters/12345.jpg', - }) - @IsUrl() - imageUrl: string; -} diff --git a/src/movie/dto/response.dto.ts b/src/movie/dto/response.dto.ts deleted file mode 100644 index f23202b..0000000 --- a/src/movie/dto/response.dto.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class MovieResponse { - @ApiProperty({ - description: 'Id фильма', - example: '123456', - type: String, - }) - id: string; - - @ApiProperty({ - description: 'Название фильма', - example: 'Fight Club', - type: String, - }) - title: string; - - @ApiProperty({ - description: 'Год выпуска', - example: 1995, - type: Number, - }) - releaseYear: number; -} diff --git a/src/movie/movie.controller.ts b/src/movie/movie.controller.ts deleted file mode 100644 index 3f50897..0000000 --- a/src/movie/movie.controller.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { - Body, - Controller, - Delete, - Get, - HttpStatus, - Param, - Post, - Put, -} from '@nestjs/common'; -import { - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiResponse, - ApiTags, -} from '@nestjs/swagger'; -import { MovieDto } from './dto/movie.dto'; -import { MovieResponse } from './dto/response.dto'; -import { MovieService } from './movie.service'; -import { Movie } from 'generated/prisma'; - -@ApiTags('Movies') -@Controller('movies') -export class MovieController { - constructor(private readonly movieService: MovieService) {} - - @ApiOperation({ - summary: 'Список фильмов', - description: 'Получить список всех фильмов', - }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'Фильмы найдены', - }) - @Get() - public findAll() { - return this.movieService.findAll(); - } - - @ApiOperation({ - summary: 'Поллучить фильм', - description: 'Получить фильм по id', - }) - @ApiParam({ name: 'id', type: 'string', description: 'ID фильма' }) - @ApiOkResponse({ description: 'Фильм найден', type: MovieResponse }) - @ApiNotFoundResponse({ - description: 'Филь не найден', - example: { - status: 404, - message: 'Not found movie with id 22', - timestamp: '2025-06-12T10:46:30.694Z', - path: '', - }, - }) - @Get(':id') - public findById(@Param('id') id: Movie['id']) { - return this.movieService.findById(id); - } - - @ApiOperation({ - summary: 'Добавить фильм', - description: 'Добавить фильм в коллекцию', - }) - @Post() - public create(@Body() dto: MovieDto) { - return this.movieService.create(dto); - } - - @Put(':id') - public update(@Body() dto: MovieDto, @Param('id') id: Movie['id']) { - return this.movieService.update(id, dto); - } - - @Delete(':id') - public delete(@Param('id') id: Movie['id']) { - return this.movieService.delete(id); - } -} diff --git a/src/movie/movie.module.ts b/src/movie/movie.module.ts deleted file mode 100644 index 4fb044b..0000000 --- a/src/movie/movie.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Module } from '@nestjs/common'; -import { MovieService } from './movie.service'; -import { MovieController } from './movie.controller'; - -@Module({ - controllers: [MovieController], - providers: [MovieService], -}) -export class MovieModule {} diff --git a/src/movie/movie.service.ts b/src/movie/movie.service.ts deleted file mode 100644 index 7cb2af0..0000000 --- a/src/movie/movie.service.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; -import { PrismaService } from '../prisma/prisma.service'; -import { MovieDto } from './dto/movie.dto'; -import { Movie } from 'generated/prisma'; - -@Injectable() -export class MovieService { - constructor(private prismaService: PrismaService) {} - - async findAll() { - return this.prismaService.movie.findMany({ - orderBy: { - id: 'asc', - }, - select: { - id: true, - title: true, - actors: { - select: { - id: true, - name: true, - }, - }, - }, - }); - } - - async findById(id: Movie['id']): Promise { - const movie = await this.prismaService.movie.findUnique({ - where: { id }, - include: { - actors: true, - poster: true, - reviews: true, - }, - }); - - if (!movie) { - throw new NotFoundException(`Not found movie with id ${id}`); - } - - return movie; - } - - async create(dto: MovieDto) { - const { actorIds, releaseYear, title, imageUrl } = dto; - const actors = await this.prismaService.actor.findMany({ - where: { - id: { in: actorIds }, - }, - }); - if (!actors || actors.length === 0) { - throw new NotFoundException('Not found actors'); - } - - return this.prismaService.movie.create({ - data: { - title, - releaseYear, - actors: { - connect: actors.map((actor) => ({ - id: actor.id, - })), - }, - poster: { - create: { - url: imageUrl, - }, - }, - }, - }); - } - - async update(id: Movie['id'], dto: MovieDto): Promise { - const movie = await this.findById(id); - const actors = await this.prismaService.actor.findMany({ - where: { - id: { in: dto.actorIds }, - }, - }); - await this.prismaService.movie.update({ - where: { id: movie.id }, - data: { - title: dto.title, - releaseYear: dto.releaseYear, - poster: { - create: { - url: dto.imageUrl, - }, - }, - actors: { - connect: actors.map((actor) => ({ id: actor.id })), - }, - }, - }); - - return true; - } - - async delete(id: Movie['id']): Promise { - const movie = await this.findById(id); - await this.prismaService.movie.delete({ - where: { - id: movie.id, - }, - }); - - return movie; - } -} diff --git a/src/review/dto/create-review.dto.ts b/src/review/dto/create-review.dto.ts deleted file mode 100644 index 651ea23..0000000 --- a/src/review/dto/create-review.dto.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { IsNotEmpty, IsString, IsUUID, Max, Min } from 'class-validator'; -import { Review } from 'generated/prisma'; - -export class CreateReviewDto { - @IsNotEmpty() - @IsString() - text: Review['text']; - - @IsNotEmpty() - @Min(0) - @Max(10) - rating: Review['rating']; - - @IsNotEmpty() - @IsUUID() - movieId: Review['movieId']; -} diff --git a/src/review/review.controller.ts b/src/review/review.controller.ts deleted file mode 100644 index 93bb7e3..0000000 --- a/src/review/review.controller.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Body, Controller, Get, Post } from '@nestjs/common'; -import { CreateReviewDto } from './dto/create-review.dto'; -import { ReviewService } from './review.service'; - -@Controller('reviews') -export class ReviewController { - constructor(private readonly reviewService: ReviewService) {} - - @Post() - public async create(@Body() dto: CreateReviewDto) { - return this.reviewService.create(dto); - } - - @Get() - public findAll() { - return this.reviewService.findAll(); - } -} diff --git a/src/review/review.module.ts b/src/review/review.module.ts deleted file mode 100644 index f292f1d..0000000 --- a/src/review/review.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { MovieService } from '../movie/movie.service'; -import { ReviewService } from './review.service'; -import { ReviewController } from './review.controller'; - -@Module({ - controllers: [ReviewController], - providers: [ReviewService, MovieService], -}) -export class ReviewModule {} diff --git a/src/review/review.service.ts b/src/review/review.service.ts deleted file mode 100644 index 61a5bb9..0000000 --- a/src/review/review.service.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { Review } from 'generated/prisma'; -import { PrismaService } from 'src/prisma/prisma.service'; -import { CreateReviewDto } from './dto/create-review.dto'; - -@Injectable() -export class ReviewService { - constructor(private readonly prismaService: PrismaService) {} - - async findAll(): Promise { - return this.prismaService.review.findMany(); - } - - async create(dto: CreateReviewDto): Promise { - const { movieId, rating, text } = dto; - - return this.prismaService.review.create({ - data: { - text, - rating, - movie: { - connect: { - id: movieId, - }, - }, - }, - }); - } -} diff --git a/src/task/docorators/start-with.decorator.ts b/src/task/docorators/start-with.decorator.ts deleted file mode 100644 index 7876aba..0000000 --- a/src/task/docorators/start-with.decorator.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { registerDecorator, ValidatorOptions } from 'class-validator'; - -export function StartsWiths( - prefix: string, - validatorOption?: ValidatorOptions, -) { - return (obj: object, propertyName: string) => { - registerDecorator({ - name: 'startsWith', - target: obj.constructor, - propertyName, - options: validatorOption, - validator: { - validate(value: any): Promise | boolean { - console.log('value', value); - return typeof value === 'string' && value.startsWith(prefix); - }, - defaultMessage(): string { - return `Should start with "${prefix}"`; - }, - }, - }); - }; -} diff --git a/src/task/dto/create-task.dto.ts b/src/task/dto/create-task.dto.ts deleted file mode 100644 index ade5638..0000000 --- a/src/task/dto/create-task.dto.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { - IsArray, - IsEnum, - IsInt, - IsNotEmpty, - IsOptional, - IsPositive, - IsString, - Length, -} from 'class-validator'; -import { StartsWiths } from '../docorators/start-with.decorator'; - -export enum TaskTag { - WORK = 'work', - STUDY = 'study', - HOME = 'home', -} - -export class CreateTaskDto { - @IsString() - @IsNotEmpty() - @StartsWiths('task') - @Length(2, 10) - title: string; - - @IsString() - @IsOptional() - description: string; - - @IsInt({}) - @IsOptional() - @IsPositive() - priority: number; - - @IsArray() - @IsEnum(TaskTag, { each: true }) - @IsOptional() - tags: TaskTag[]; -} diff --git a/src/task/dto/update-task.dto.ts b/src/task/dto/update-task.dto.ts deleted file mode 100644 index f7af821..0000000 --- a/src/task/dto/update-task.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { IsBoolean, IsNotEmpty, IsString, Length } from 'class-validator'; - -export class UpdateTaskDto { - @IsString({ message: 'Должно быть строкой' }) - @IsNotEmpty({ message: 'Не должно быть пустым' }) - @Length(2, 40, { message: 'Должно быть от 2 до 40 символов' }) - title: string; - - @IsBoolean({ message: 'Должно быть булевым выражением' }) - isCompleted: boolean; -} diff --git a/src/task/task.controller.ts b/src/task/task.controller.ts deleted file mode 100644 index 8bdb09e..0000000 --- a/src/task/task.controller.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { - Body, - Controller, - Delete, - Get, - Param, - Patch, - Post, - Put, -} from '@nestjs/common'; -import { CreateTaskDto } from './dto/create-task.dto'; -import { UpdateTaskDto } from './dto/update-task.dto'; -import { TaskService } from './task.service'; - -@Controller('task') -export class TaskController { - constructor(private readonly taskService: TaskService) {} - - @Get('all') - findAll() { - return this.taskService.findAll(); - } - - @Get('by-id/:id') - findById( - @Param('id') - id: string, - ) { - return this.taskService.findById(Number(id)); - } - - @Post() - create(@Body() dto: CreateTaskDto) { - return this.taskService.create(dto); - } - - @Put(':id') - update(@Param('id') id: string, @Body() dto: UpdateTaskDto) { - return this.taskService.update(Number(id), dto); - } - - @Patch(':id') - patchUpdate(@Param('id') id: string, @Body() dto: Partial) { - return this.taskService.patchTask(Number(id), dto); - } - - @Delete(':id') - delete(@Param('id') id: string) { - return this.taskService.delete(Number(id)); - } -} diff --git a/src/task/task.module.ts b/src/task/task.module.ts deleted file mode 100644 index e7c4c1d..0000000 --- a/src/task/task.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TaskService } from './task.service'; -import { TaskController } from './task.controller'; - -@Module({ - controllers: [TaskController], - providers: [TaskService], -}) -export class TaskModule {} diff --git a/src/task/task.service.ts b/src/task/task.service.ts deleted file mode 100644 index dbee2e0..0000000 --- a/src/task/task.service.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; -import { CreateTaskDto } from './dto/create-task.dto'; -import { UpdateTaskDto } from './dto/update-task.dto'; - -@Injectable() -export class TaskService { - private tasks = [ - { - id: 1, - title: 'Learn Nestjs', - isCompleted: false, - }, - { - id: 2, - title: 'Build API', - isCompleted: true, - }, - ]; - - findAll() { - return this.tasks; - } - - findById(id: number) { - const task = this.tasks.find((task) => task.id === id); - - if (!task) { - throw new NotFoundException(`Not found task with id ${id}`); - } - - return task; - } - - create(dto: CreateTaskDto) { - const { title, description, priority, tags } = dto; - const newTask = { - id: this.tasks.length + 1, - title, - isCompleted: false, - description, - priority, - tags, - }; - - this.tasks.push(newTask); - - return newTask; - } - - update(id: number, dto: UpdateTaskDto) { - const task = this.findById(id); - const { isCompleted, title } = dto; - - task.isCompleted = isCompleted; - task.title = title; - - return task; - } - - patchTask(id: number, dto: Partial) { - const task = this.findById(id); - - Object.assign(task, dto); - - return task; - } - - delete(id: number) { - const task = this.findById(id); - this.tasks = this.tasks.filter((t) => t !== task); - return task; - } -} diff --git a/src/utils/is-dev.ts b/src/utils/is-dev.ts new file mode 100644 index 0000000..f7fffe1 --- /dev/null +++ b/src/utils/is-dev.ts @@ -0,0 +1,5 @@ +import { ConfigService } from '@nestjs/config'; + +export const isDev = (configService: ConfigService) => { + return configService.getOrThrow('NODE_ENV') === 'development'; +}; diff --git a/src/utils/swagger.ts b/src/utils/swagger.ts new file mode 100644 index 0000000..32f9354 --- /dev/null +++ b/src/utils/swagger.ts @@ -0,0 +1,14 @@ +import { INestApplication } from '@nestjs/common'; +import { SwaggerModule } from '@nestjs/swagger'; +import { getSwaggerConfig } from '../config/swagger.config'; + +export function setupSwagger(app: INestApplication) { + const config = getSwaggerConfig(); + + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('/docs', app, document, { + jsonDocumentUrl: 'docs/swagger.json', + yamlDocumentUrl: 'docs/swagger.yaml', + customSiteTitle: 'Nest.js base course', + }); +} diff --git a/yarn.lock b/yarn.lock index 8d56466..1c8d531 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1022,11 +1022,24 @@ path-to-regexp "8.2.0" tslib "2.8.1" +"@nestjs/jwt@^11.0.0": + version "11.0.0" + resolved "https://registry.yarnpkg.com/@nestjs/jwt/-/jwt-11.0.0.tgz#aef1590e70830c70fba0f59e9b17314dc4d36822" + integrity sha512-v7YRsW3Xi8HNTsO+jeHSEEqelX37TVWgwt+BcxtkG/OfXJEOs6GZdbdza200d6KqId1pJQZ6UPj1F0M6E+mxaA== + dependencies: + "@types/jsonwebtoken" "9.0.7" + jsonwebtoken "9.0.2" + "@nestjs/mapped-types@*", "@nestjs/mapped-types@2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz#b9b536b7c3571567aa1d0223db8baa1a51505a19" integrity sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw== +"@nestjs/passport@^11.0.5": + version "11.0.5" + resolved "https://registry.yarnpkg.com/@nestjs/passport/-/passport-11.0.5.tgz#dd3e506c2fb7ddc80fd1321c01cc1a0ca6d6b609" + integrity sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ== + "@nestjs/platform-express@^11.0.1": version "11.1.2" resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-11.1.2.tgz#e4b2671c74876ff399f50b7a7f36cb4b4c9e35ec" @@ -1108,6 +1121,11 @@ dependencies: "@noble/hashes" "^1.1.5" +"@phc/format@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@phc/format/-/format-1.0.0.tgz#b5627003b3216dc4362125b13f48a4daa76680e4" + integrity sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ== + "@pkgr/core@^0.2.4": version "0.2.7" resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.7.tgz#eb5014dfd0b03e7f3ba2eeeff506eed89b028058" @@ -1380,6 +1398,11 @@ dependencies: "@types/node" "*" +"@types/cookie-parser@^1.4.9": + version "1.4.9" + resolved "https://registry.yarnpkg.com/@types/cookie-parser/-/cookie-parser-1.4.9.tgz#f0e79c766a58ee7369a52e7509b3840222f68ed2" + integrity sha512-tGZiZ2Gtc4m3wIdLkZ8mkj1T6CEHb35+VApbL2T14Dew8HA7c+04dmKqsKRNC+8RJPm16JEK0tFSwdZqubfc4g== + "@types/cookiejar@^2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.5.tgz#14a3e83fa641beb169a2dd8422d91c3c345a9a78" @@ -1416,6 +1439,15 @@ "@types/range-parser" "*" "@types/send" "*" +"@types/express@*": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.3.tgz#6c4bc6acddc2e2a587142e1d8be0bce20757e956" + integrity sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^5.0.0" + "@types/serve-static" "*" + "@types/express@^5.0.0": version "5.0.2" resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.2.tgz#7be9e337a5745d6b43ef5b0c352dad94a7f0c256" @@ -1474,6 +1506,21 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== +"@types/jsonwebtoken@*": + version "9.0.9" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz#a4c3a446c0ebaaf467a58398382616f416345fb3" + integrity sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ== + dependencies: + "@types/ms" "*" + "@types/node" "*" + +"@types/jsonwebtoken@9.0.7": + version "9.0.7" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz#e49b96c2b29356ed462e9708fc73b833014727d2" + integrity sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg== + dependencies: + "@types/node" "*" + "@types/methods@^1.1.4": version "1.1.4" resolved "https://registry.yarnpkg.com/@types/methods/-/methods-1.1.4.tgz#d3b7ac30ac47c91054ea951ce9eed07b1051e547" @@ -1484,6 +1531,11 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== +"@types/ms@*": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" + integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== + "@types/node@*", "@types/node@^22.10.7": version "22.15.29" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.29.tgz#c75999124a8224a3f79dd8b6ccfb37d74098f678" @@ -1491,6 +1543,29 @@ dependencies: undici-types "~6.21.0" +"@types/passport-jwt@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/passport-jwt/-/passport-jwt-4.0.1.tgz#080fbe934fb9f6954fb88ec4cdf4bb2cc7c4d435" + integrity sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ== + dependencies: + "@types/jsonwebtoken" "*" + "@types/passport-strategy" "*" + +"@types/passport-strategy@*": + version "0.2.38" + resolved "https://registry.yarnpkg.com/@types/passport-strategy/-/passport-strategy-0.2.38.tgz#482abba0b165cd4553ec8b748f30b022bd6c04d3" + integrity sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA== + dependencies: + "@types/express" "*" + "@types/passport" "*" + +"@types/passport@*", "@types/passport@^1.0.17": + version "1.0.17" + resolved "https://registry.yarnpkg.com/@types/passport/-/passport-1.0.17.tgz#718a8d1f7000ebcf6bbc0853da1bc8c4bc7ea5e6" + integrity sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg== + dependencies: + "@types/express" "*" + "@types/qs@*": version "6.14.0" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.14.0.tgz#d8b60cecf62f2db0fb68e5e006077b9178b85de5" @@ -2022,6 +2097,15 @@ arg@^4.1.0: resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== +argon2@^0.43.0: + version "0.43.0" + resolved "https://registry.yarnpkg.com/argon2/-/argon2-0.43.0.tgz#4a5d8943506923ce7450f4e9069e03e134c7473e" + integrity sha512-u/HKLcbWShVDhkfwI4hWyiUf3qyX8QhTfaIv2cWE18uqhXCmR5hb6Ed7oqYi2KCQegeAnRhiFzbjzm7i5yl1GA== + dependencies: + "@phc/format" "^1.0.0" + node-addon-api "^8.3.1" + node-gyp-build "^4.8.4" + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -2229,6 +2313,11 @@ buffer-crc32@~0.2.3: resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== +buffer-equal-constant-time@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -2511,12 +2600,25 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +cookie-parser@^1.4.7: + version "1.4.7" + resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.7.tgz#e2125635dfd766888ffe90d60c286404fa0e7b26" + integrity sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw== + dependencies: + cookie "0.7.2" + cookie-signature "1.0.6" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + cookie-signature@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== -cookie@^0.7.1: +cookie@0.7.2, cookie@^0.7.1: version "0.7.2" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== @@ -2686,6 +2788,13 @@ eastasianwidth@^0.2.0: resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -4141,6 +4250,39 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +jsonwebtoken@9.0.2, jsonwebtoken@^9.0.0: + version "9.0.2" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" + integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== + 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" + +jwa@^1.4.1: + version "1.4.2" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.2.tgz#16011ac6db48de7b102777e57897901520eec7b9" + integrity sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw== + dependencies: + buffer-equal-constant-time "^1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + keyv@^4.5.3, keyv@^4.5.4: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" @@ -4205,6 +4347,36 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -4215,6 +4387,11 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + lodash@4.17.21, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -4410,7 +4587,7 @@ mkdirp@^0.5.4: dependencies: minimist "^1.2.6" -ms@^2.1.3: +ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -4453,6 +4630,11 @@ node-abort-controller@^3.0.1: resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ== +node-addon-api@^8.3.1: + version "8.4.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-8.4.0.tgz#8cbc68ee1c216368921a8f63038a23a39cd8ba44" + integrity sha512-D9DI/gXHvVmjHS08SVch0Em8G5S1P+QWtU31appcKT/8wFSPRcdHadIFSAntdMMVM5zz+/DL+bL/gz3UDppqtg== + node-emoji@1.11.0: version "1.11.0" resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c" @@ -4460,6 +4642,11 @@ node-emoji@1.11.0: dependencies: lodash "^4.17.21" +node-gyp-build@^4.8.4: + version "4.8.4" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8" + integrity sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ== + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -4615,6 +4802,28 @@ parseurl@^1.3.3: resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== +passport-jwt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/passport-jwt/-/passport-jwt-4.0.1.tgz#c443795eff322c38d173faa0a3c481479646ec3d" + integrity sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ== + dependencies: + jsonwebtoken "^9.0.0" + passport-strategy "^1.0.0" + +passport-strategy@1.x.x, passport-strategy@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" + integrity sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA== + +passport@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/passport/-/passport-0.7.0.tgz#3688415a59a48cf8068417a8a8092d4492ca3a05" + integrity sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ== + dependencies: + passport-strategy "1.x.x" + pause "0.0.1" + utils-merge "^1.0.1" + path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" @@ -4653,6 +4862,11 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +pause@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" + integrity sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg== + peek-readable@^5.3.1: version "5.4.2" resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-5.4.2.tgz#aff1e1ba27a7d6911ddb103f35252ffc1787af49" @@ -4951,7 +5165,7 @@ rxjs@^7.8.1: dependencies: tslib "^2.1.0" -safe-buffer@5.2.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -5653,6 +5867,11 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== +utils-merge@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"