add JWT authorization

This commit is contained in:
Sergey Krylov 2025-06-14 13:43:24 +03:00
parent 8024b36280
commit 10ba9e5550
46 changed files with 715 additions and 846 deletions

View File

@ -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",

View File

@ -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")
}

View File

@ -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);
}
}

View File

@ -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 {}

View File

@ -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<Actor> {
const { name } = dto;
return this.prismaService.actor.create({
data: {
name,
},
});
}
}

View File

@ -1,8 +0,0 @@
import { IsNotEmpty, IsString, MaxLength } from 'class-validator';
export class CreateActorDto {
@IsNotEmpty()
@IsString()
@MaxLength(64)
name: string;
}

View File

@ -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 {}

View File

@ -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: [],

View File

@ -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 };
}
}

22
src/auth/auth.module.ts Normal file
View File

@ -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 {}

165
src/auth/auth.service.ts Normal file
View File

@ -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',
});
}
}

View File

@ -0,0 +1,6 @@
import { applyDecorators, UseGuards } from '@nestjs/common';
import { JwtGuard } from '../guards/auth.guard';
export function Authorization() {
return applyDecorators(UseGuards(JwtGuard));
}

View File

@ -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;
},
);

12
src/auth/dto/auth.dto.ts Normal file
View File

@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
export class AuthResponse {
@ApiProperty({
title: 'JWT Refresh Token',
example:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' +
'eyJpZCI6Ijk4NGFlODk3LWY2ZjMtNGZhNC1iMjYxLWExOWU5NDY3OWVmMCIsImlhdCI6MTc0OTg5NTc0MywiZXhwIjoxNzQ5OTAyOTQzfQ.' +
'SkW6SW9wcTy2Vc9NPVbg5Q6RExscH4Mgth5HerBRGrs',
})
accessToken: string;
}

33
src/auth/dto/login.dto.ts Normal file
View File

@ -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;
}

View File

@ -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;
}

View File

@ -0,0 +1,3 @@
import { AuthGuard } from '@nestjs/passport';
export class JwtGuard extends AuthGuard('jwt') {}

View File

@ -0,0 +1,5 @@
import { User } from 'generated/prisma';
export interface JwtPayload {
id: User['id'];
}

View File

@ -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);
}
}

View File

@ -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'];
},
);

View File

@ -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,
});
}
}

View File

@ -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<boolean> | Observable<boolean> {
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;
}
}

View File

@ -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<any>,
): Observable<any> {
return next.handle().pipe(
map((data) => ({
status: 'OK',
data,
})),
);
}
}

View File

@ -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();
}

View File

@ -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;
}
}

17
src/config/jwt.config.ts Normal file
View File

@ -0,0 +1,17 @@
import { ConfigService } from '@nestjs/config';
import { JwtModuleOptions } from '@nestjs/jwt';
export async function getJwtConfig(
configService: ConfigService,
): Promise<JwtModuleOptions> {
return {
secret: configService.getOrThrow('JWT_SECRET'),
signOptions: {
algorithm: 'HS256',
},
verifyOptions: {
algorithms: ['HS256'],
ignoreExpiration: false,
},
};
}

View File

@ -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();
}

View File

@ -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();

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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 {}

View File

@ -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<Movie> {
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<boolean> {
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<Movie> {
const movie = await this.findById(id);
await this.prismaService.movie.delete({
where: {
id: movie.id,
},
});
return movie;
}
}

View File

@ -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'];
}

View File

@ -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();
}
}

View File

@ -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 {}

View File

@ -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<Review[]> {
return this.prismaService.review.findMany();
}
async create(dto: CreateReviewDto): Promise<Review> {
const { movieId, rating, text } = dto;
return this.prismaService.review.create({
data: {
text,
rating,
movie: {
connect: {
id: movieId,
},
},
},
});
}
}

View File

@ -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> | boolean {
console.log('value', value);
return typeof value === 'string' && value.startsWith(prefix);
},
defaultMessage(): string {
return `Should start with "${prefix}"`;
},
},
});
};
}

View File

@ -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[];
}

View File

@ -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;
}

View File

@ -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<UpdateTaskDto>) {
return this.taskService.patchTask(Number(id), dto);
}
@Delete(':id')
delete(@Param('id') id: string) {
return this.taskService.delete(Number(id));
}
}

View File

@ -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 {}

View File

@ -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<UpdateTaskDto>) {
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;
}
}

5
src/utils/is-dev.ts Normal file
View File

@ -0,0 +1,5 @@
import { ConfigService } from '@nestjs/config';
export const isDev = (configService: ConfigService) => {
return configService.getOrThrow('NODE_ENV') === 'development';
};

14
src/utils/swagger.ts Normal file
View File

@ -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',
});
}

225
yarn.lock
View File

@ -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"