diff --git a/backend/package.json b/backend/package.json index 4c7813f..e2a0e05 100644 --- a/backend/package.json +++ b/backend/package.json @@ -36,8 +36,11 @@ "class-validator": "^0.14.2", "connect-redis": "^7.1.1", "cookie-parser": "^1.4.7", + "device-detector-js": "^3.0.3", "express-session": "^1.18.1", + "geoip-lite": "^1.4.10", "graphql": "^16.11.0", + "i18n-iso-countries": "^7.14.0", "ioredis": "^5.6.1", "prisma": "^6.10.1", "reflect-metadata": "^0.2.2", @@ -55,6 +58,7 @@ "@types/cookie-parser": "^1.4.9", "@types/express": "^5.0.0", "@types/express-session": "^1.18.2", + "@types/geoip-lite": "^1.4.4", "@types/jest": "^29.5.14", "@types/node": "^22.10.7", "@types/supertest": "^6.0.2", diff --git a/backend/src/core/graphql/schema.gql b/backend/src/core/graphql/schema.gql index 3e1017c..0d07660 100644 --- a/backend/src/core/graphql/schema.gql +++ b/backend/src/core/graphql/schema.gql @@ -13,19 +13,49 @@ A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date """ scalar DateTime +type DeviceModel { + browser: String! + os: String! + type: String! +} + +type LocationModel { + city: String! + country: String! + latitude: Float! + longitude: Float! +} + input LoginInput { login: String! password: String! } type Mutation { + clearSessionCookie: Boolean! createUser(data: CreateUserInput!): UserModel! loginUser(data: LoginInput!): UserModel! logoutUser: Boolean! + removeSession(id: String!): Boolean! } type Query { + findCurrentSession: SessionModel! findProfile: UserModel! + findSessionsByUser: [SessionModel!]! +} + +type SessionMetadataModel { + device: DeviceModel! + ip: String! + location: LocationModel! +} + +type SessionModel { + createdAt: String! + id: ID! + metadata: SessionMetadataModel! + userId: String! } type UserModel { diff --git a/backend/src/module/auth/account/models/user.model.ts b/backend/src/module/auth/account/models/user.model.ts index e96d42d..735d4c8 100644 --- a/backend/src/module/auth/account/models/user.model.ts +++ b/backend/src/module/auth/account/models/user.model.ts @@ -1,7 +1,8 @@ import { Field, ID, ObjectType } from '@nestjs/graphql' +import { User } from '@prisma/generated' @ObjectType() -export class UserModel { +export class UserModel implements User { @Field(() => ID) id: string diff --git a/backend/src/module/auth/session/models/session.model.ts b/backend/src/module/auth/session/models/session.model.ts new file mode 100644 index 0000000..26ec05b --- /dev/null +++ b/backend/src/module/auth/session/models/session.model.ts @@ -0,0 +1,56 @@ +import { DeviceInfo, LocationInfo, SessionInfo } from '@/src/shared/types/session-metadata.types' +import { Field, ID, ObjectType } from '@nestjs/graphql' + +@ObjectType() +export class LocationModel implements LocationInfo { + @Field(() => String) + country: string + + @Field(() => String) + city: string + + @Field(() => Number) + latitude: number + + @Field(() => Number) + longitude: number +} + +@ObjectType() +export class DeviceModel implements DeviceInfo { + @Field(() => String) + browser: string + + @Field(() => String) + os: string + + @Field(() => String) + type: string +} + +@ObjectType() +export class SessionMetadataModel implements SessionInfo { + @Field(() => LocationModel) + location: LocationModel + + @Field(() => DeviceModel) + device: DeviceModel + + @Field(() => String) + ip: string +} + +@ObjectType() +export class SessionModel { + @Field(() => ID) + id: string + + @Field(() => String) + userId: string + + @Field(() => String) + createdAt: string + + @Field(() => SessionMetadataModel) + metadata: SessionMetadataModel +} diff --git a/backend/src/module/auth/session/session.module.ts b/backend/src/module/auth/session/session.module.ts index dd2fc67..70a702e 100644 --- a/backend/src/module/auth/session/session.module.ts +++ b/backend/src/module/auth/session/session.module.ts @@ -1,6 +1,6 @@ -import { Module } from '@nestjs/common'; -import { SessionService } from './session.service'; -import { SessionResolver } from './session.resolver'; +import { Module } from '@nestjs/common' +import { SessionService } from './session.service' +import { SessionResolver } from './session.resolver' @Module({ providers: [SessionResolver, SessionService], diff --git a/backend/src/module/auth/session/session.resolver.ts b/backend/src/module/auth/session/session.resolver.ts index 2b79abb..c671900 100644 --- a/backend/src/module/auth/session/session.resolver.ts +++ b/backend/src/module/auth/session/session.resolver.ts @@ -1,23 +1,58 @@ import { UserModel } from '@/src/module/auth/account/models/user.model' import { LoginInput } from '@/src/module/auth/session/inputs/login.input' +import { Authorization } from '@/src/shared/decorators/auth.decorator' +import { UserAgent } from '@/src/shared/decorators/user-agent.decorator' import { GqlContext } from '@/src/shared/types/gql-context.types' -import { Args, Context, Mutation, Resolver } from '@nestjs/graphql' +import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql' import { SessionService } from './session.service' +import { SessionModel } from './models/session.model' @Resolver('Session') export class SessionResolver { constructor(private readonly sessionService: SessionService) {} + @Authorization() + @Query(() => [SessionModel], { name: 'findSessionsByUser' }) + public async findByUser( + @Context() { req }: GqlContext, + ) { + return this.sessionService.findByUser(req) + } + + @Authorization() + @Query(() => SessionModel, { name: 'findCurrentSession' }) + public async findCurrent( + @Context() { req }: GqlContext, + ) { + return this.sessionService.findCurrentSession(req) + } + @Mutation(() => UserModel, { name: 'loginUser' }) public async login( @Context() { req }: GqlContext, @Args('data') input: LoginInput, + @UserAgent() userAgent: string, ) { - return this.sessionService.login(req, input) + return this.sessionService.login(req, input, userAgent) } + @Authorization() @Mutation(() => Boolean, { name: 'logoutUser' }) public async logout(@Context() { req }: GqlContext) { return this.sessionService.logout(req) } + + @Mutation(() => Boolean, { name: 'clearSessionCookie' }) + public clearSession(@Context() { req }: GqlContext) { + return this.sessionService.clearSession(req) + } + + @Authorization() + @Mutation(() => Boolean, { name: 'removeSession' }) + public remove( + @Context() { req }: GqlContext, + @Args('id') id: string, + ) { + return this.sessionService.remove(req, id) + } } diff --git a/backend/src/module/auth/session/session.service.ts b/backend/src/module/auth/session/session.service.ts index 25d0e72..b546729 100644 --- a/backend/src/module/auth/session/session.service.ts +++ b/backend/src/module/auth/session/session.service.ts @@ -1,6 +1,9 @@ import { PrismaService } from '@/src/core/prisma/prisma.service' +import { RedisService } from '@/src/core/redis/redis.service' import { LoginInput } from '@/src/module/auth/session/inputs/login.input' +import { getSessionMetadata } from '@/src/shared/util/session-metadata.util' import { + ConflictException, Injectable, InternalServerErrorException, NotFoundException, @@ -9,15 +12,65 @@ import { import { ConfigService } from '@nestjs/config' import { verify } from 'argon2' import { Request } from 'express' +import { SessionData } from 'express-session' @Injectable() export class SessionService { constructor( private readonly prismaService: PrismaService, + private readonly redisService: RedisService, private readonly configService: ConfigService, ) {} - public async login(req: Request, input: LoginInput) { + public async findByUser(req: Request) { + const userId = req.user?.id + + if (!userId) { + throw new NotFoundException('Пользователь не найден') + } + + const keys = await this.redisService.keys('*') + const userSessions: Request['session'][] = [] + + for (const key of keys) { + const sessionData = await this.redisService.get(key) + if (sessionData) { + const session = JSON.parse(sessionData) as Request['session'] + + if (session.userId === userId) { + userSessions.push({ + ...session, + id: key.split(':')[1], + } as Request['session']) + } + } + } + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- todo fixme + // @ts-expect-error + userSessions.sort((a, b) => b.createdAt - a.createdAt) + + return userSessions.filter(session => session.id !== req.session.id) + } + + public async findCurrentSession(req: Request) { + const sessionId = req.session.id + const key = `${this.configService.getOrThrow('SESSION_FOLDER')}${sessionId}` + const sessionData = await this.redisService.get(key) + + if (!sessionData) { + throw new NotFoundException('Сессия не найдена') + } + + const session = JSON.parse(sessionData) as SessionData + + return { + ...session, + id: sessionId, + } + } + + public async login(req: Request, input: LoginInput, userAgent: string) { const { login, password } = input const user = await this.prismaService.user.findFirst({ where: { @@ -39,6 +92,7 @@ export class SessionService { return new Promise((resolve, reject) => { req.session.userId = user.id req.session.createdAt = new Date() + req.session.metadata = getSessionMetadata(req, userAgent) req.session.save((error) => { if (error) { @@ -62,4 +116,21 @@ export class SessionService { }) }) } + + public clearSession(req: Request) { + req.res?.clearCookie(this.configService.getOrThrow('SESSION_NAME')) + + return true + } + + public async remove(req: Request, id: string) { + if (req.session.id === id) { + throw new ConflictException('Текущую сессию удалить нельзя') + } + + const key = `${this.configService.getOrThrow('SESSION_FOLDER')}${id}` + await this.redisService.del(key) + + return true + } } diff --git a/backend/src/shared/decorators/user-agent.decorator.ts b/backend/src/shared/decorators/user-agent.decorator.ts new file mode 100644 index 0000000..3f89fc9 --- /dev/null +++ b/backend/src/shared/decorators/user-agent.decorator.ts @@ -0,0 +1,14 @@ +import { GqlContext } from '@/src/shared/types/gql-context.types' +import { createParamDecorator } from '@nestjs/common' +import { GqlExecutionContext } from '@nestjs/graphql' +import { Request } from 'express' + +export const UserAgent = createParamDecorator( + (data: unknown, ctx) => { + const req: Request = ctx.getType() === 'http' + ? ctx.switchToHttp().getRequest() + : GqlExecutionContext.create(ctx).getContext().req + + return req.headers['user-agent'] + }, +) diff --git a/backend/src/shared/types/express-session.d.ts b/backend/src/shared/types/express-session.d.ts index 6bfa84d..18ed0d3 100644 --- a/backend/src/shared/types/express-session.d.ts +++ b/backend/src/shared/types/express-session.d.ts @@ -1,8 +1,10 @@ import 'express-session' +import { SessionInfo } from './session-metadata.types' declare module 'express-session' { interface SessionData { userId?: string createdAt?: Date | string + metadata: SessionInfo } } diff --git a/backend/src/shared/types/session-metadata.types.ts b/backend/src/shared/types/session-metadata.types.ts new file mode 100644 index 0000000..82b33ee --- /dev/null +++ b/backend/src/shared/types/session-metadata.types.ts @@ -0,0 +1,18 @@ +export interface LocationInfo { + country: string + city: string + longitude: number + latitude: number +} + +export interface DeviceInfo { + browser: string + os: string + type: string +} + +export interface SessionInfo { + location: LocationInfo + device: DeviceInfo + ip: string +} diff --git a/backend/src/shared/util/session-metadata.util.ts b/backend/src/shared/util/session-metadata.util.ts new file mode 100644 index 0000000..2297f4e --- /dev/null +++ b/backend/src/shared/util/session-metadata.util.ts @@ -0,0 +1,43 @@ +import { SessionInfo } from '@/src/shared/types/session-metadata.types' +import { IS_DEV } from '@/src/shared/util/is-dev.util' +import { Request } from 'express' +import { lookup } from 'geoip-lite' +// eslint-disable-next-line @typescript-eslint/no-require-imports +import DeviceDetector = require('device-detector-js') +import * as countries from 'i18n-iso-countries' + +// eslint-disable-next-line @typescript-eslint/no-require-imports,@typescript-eslint/no-unsafe-argument +countries.registerLocale(require('i18n-iso-countries/langs/en.json')) + +export function getSessionMetadata(req: Request, userAgent: string): SessionInfo { + const ip = IS_DEV + ? '174.136.85.11' + : ( + Array.isArray(req.headers['cf-connecting-ip']) + ? req.headers['cf-connecting-ip'][0] + : req.headers['cf-connecting-ip'] + ) + || ( + typeof req.headers['x-forwarded-for'] === 'string' + ? req.headers['x-forwarded-for'].split(',')[0] + : req.ip + ) as string + + const location = lookup(ip) + const device = new DeviceDetector().parse(userAgent) + + return { + ip, + device: { + browser: device.client?.name ?? 'Неизвестно', + os: device.os?.name ?? 'Неизвестно', + type: device.device?.type ?? 'Неизвестно', + }, + location: { + country: countries.getName(location?.country ?? '', 'en') ?? 'Неизвестно', + city: location?.city ?? 'Неизвестно', + longitude: location?.ll[0] ?? 0, + latitude: location?.ll[1] ?? 0, + }, + } +} diff --git a/backend/yarn.lock b/backend/yarn.lock index 27bcfab..b41e69e 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -1771,6 +1771,11 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/geoip-lite@^1.4.4": + version "1.4.4" + resolved "https://registry.yarnpkg.com/@types/geoip-lite/-/geoip-lite-1.4.4.tgz#6573056a23129f25dcf9d53ca36bc3770192f109" + integrity sha512-2uVfn+C6bX/H356H6mjxsWUA5u8LO8dJgSBIRO/NFlpMe4DESzacutD/rKYrTDKm1Ugv78b4Wz1KvpHrlv3jSw== + "@types/graceful-fs@^4.1.3": version "4.1.9" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4" @@ -2446,6 +2451,13 @@ async-retry@^1.2.1: dependencies: retry "0.13.1" +"async@2.1 - 2.6.4": + version "2.6.4" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" + integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== + dependencies: + lodash "^4.17.14" + async@^3.2.3: version "3.2.6" resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" @@ -2733,7 +2745,7 @@ caniuse-lite@^1.0.30001718: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001723.tgz#c4f3174f02089720736e1887eab345e09bb10944" integrity sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw== -chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.2: +"chalk@4.1 - 4.1.2", chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -3124,6 +3136,11 @@ detect-newline@^3.0.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== +device-detector-js@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/device-detector-js/-/device-detector-js-3.0.3.tgz#03424a45962bd80294e8004a2557dc7e7b98247e" + integrity sha512-jM89LJAvP6uOd84at8OlD9dWP8KeYCCHUde0RT0HQo/stdoRH4b54Xl/fntx2nEXCmqiFhmo+/cJetS2VGUHPw== + dezalgo@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" @@ -3132,6 +3149,11 @@ dezalgo@^1.0.4: asap "^2.0.0" wrappy "1" +diacritics@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/diacritics/-/diacritics-1.3.0.tgz#3efa87323ebb863e6696cebb0082d48ff3d6f7a1" + integrity sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA== + diff-sequences@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" @@ -3606,6 +3628,13 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== + dependencies: + pend "~1.2.0" + fflate@^0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea" @@ -3825,6 +3854,19 @@ gensync@^1.0.0-beta.2: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== +geoip-lite@^1.4.10: + version "1.4.10" + resolved "https://registry.yarnpkg.com/geoip-lite/-/geoip-lite-1.4.10.tgz#138256ba97f1abee1afdec78df97a78d320c50f6" + integrity sha512-4N69uhpS3KFd97m00wiFEefwa+L+HT5xZbzPhwu+sDawStg6UN/dPwWtUfkQuZkGIY1Cj7wDVp80IsqNtGMi2w== + dependencies: + async "2.1 - 2.6.4" + chalk "4.1 - 4.1.2" + iconv-lite "0.4.13 - 0.6.3" + ip-address "5.8.9 - 5.9.4" + lazy "1.0.11" + rimraf "2.5.2 - 2.7.1" + yauzl "2.9.2 - 2.10.0" + get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -4042,6 +4084,20 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +i18n-iso-countries@^7.14.0: + version "7.14.0" + resolved "https://registry.yarnpkg.com/i18n-iso-countries/-/i18n-iso-countries-7.14.0.tgz#cd5ae098198bce1cc40cadbf0a37ce6c8e9d0edf" + integrity sha512-nXHJZYtNrfsi1UQbyRqm3Gou431elgLjKl//CYlnBGt5aTWdRPH1PiS2T/p/n8Q8LnqYqzQJik3Q7mkwvLokeg== + dependencies: + diacritics "1.3.0" + +"iconv-lite@0.4.13 - 0.6.3", iconv-lite@0.6.3, iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -4049,13 +4105,6 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@0.6.3, iconv-lite@^0.6.3: - version "0.6.3" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - ieee754@^1.1.13, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -4112,7 +4161,7 @@ inspect-with-kind@^1.0.5: dependencies: kind-of "^6.0.2" -ioredis@5.6.1: +ioredis@^5.6.1: version "5.6.1" resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.6.1.tgz#1ed7dc9131081e77342503425afceaf7357ae599" integrity sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA== @@ -4127,6 +4176,15 @@ ioredis@5.6.1: redis-parser "^3.0.0" standard-as-callback "^2.1.0" +"ip-address@5.8.9 - 5.9.4": + version "5.9.4" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-5.9.4.tgz#4660ac261ad61bd397a860a007f7e98e4eaee386" + integrity sha512-dHkI3/YNJq4b/qQaz+c8LuarD3pY24JqZWfjB8aZx1gtpc2MDILu9L9jpZe1sHpzo/yWFweQVn+U//FhazUxmw== + dependencies: + jsbn "1.1.0" + lodash "^4.17.15" + sprintf-js "1.1.2" + ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" @@ -4678,6 +4736,11 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +jsbn@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" + integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== + jsesc@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" @@ -4744,6 +4807,11 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +lazy@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/lazy/-/lazy-1.0.11.tgz#daa068206282542c088288e975c297c1ae77b690" + integrity sha512-Y+CjUfLmIpoUCCRl0ub4smrYtGGr5AOa2AKOaWelGHOGz33X/Y/KizefGqbkwfz44+cnq/+9habclf8vOmu2LA== + leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -4821,7 +4889,7 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== -lodash@4.17.21, lodash@^4.17.21: +lodash@4.17.21, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -5596,6 +5664,13 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== +"rimraf@2.5.2 - 2.7.1": + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + router@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/router/-/router-2.2.0.tgz#019be620b711c87641167cc79b99090f00b146ef" @@ -5874,6 +5949,11 @@ source-map@^0.6.0, source-map@^0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +sprintf-js@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673" + integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -6612,6 +6692,14 @@ yargs@^17.3.1: y18n "^5.0.5" yargs-parser "^21.1.1" +"yauzl@2.9.2 - 2.10.0": + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" + yauzl@^3.1.2: version "3.2.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-3.2.0.tgz#7b6cb548f09a48a6177ea0be8ece48deb7da45c0"