add sessions endpoints

This commit is contained in:
Sergey Krylov 2025-06-30 06:37:01 +03:00
parent a243549afb
commit f6ea096842
12 changed files with 379 additions and 17 deletions

View File

@ -36,8 +36,11 @@
"class-validator": "^0.14.2", "class-validator": "^0.14.2",
"connect-redis": "^7.1.1", "connect-redis": "^7.1.1",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"device-detector-js": "^3.0.3",
"express-session": "^1.18.1", "express-session": "^1.18.1",
"geoip-lite": "^1.4.10",
"graphql": "^16.11.0", "graphql": "^16.11.0",
"i18n-iso-countries": "^7.14.0",
"ioredis": "^5.6.1", "ioredis": "^5.6.1",
"prisma": "^6.10.1", "prisma": "^6.10.1",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
@ -55,6 +58,7 @@
"@types/cookie-parser": "^1.4.9", "@types/cookie-parser": "^1.4.9",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/express-session": "^1.18.2", "@types/express-session": "^1.18.2",
"@types/geoip-lite": "^1.4.4",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/node": "^22.10.7", "@types/node": "^22.10.7",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",

View File

@ -13,19 +13,49 @@ A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date
""" """
scalar DateTime scalar DateTime
type DeviceModel {
browser: String!
os: String!
type: String!
}
type LocationModel {
city: String!
country: String!
latitude: Float!
longitude: Float!
}
input LoginInput { input LoginInput {
login: String! login: String!
password: String! password: String!
} }
type Mutation { type Mutation {
clearSessionCookie: Boolean!
createUser(data: CreateUserInput!): UserModel! createUser(data: CreateUserInput!): UserModel!
loginUser(data: LoginInput!): UserModel! loginUser(data: LoginInput!): UserModel!
logoutUser: Boolean! logoutUser: Boolean!
removeSession(id: String!): Boolean!
} }
type Query { type Query {
findCurrentSession: SessionModel!
findProfile: UserModel! findProfile: UserModel!
findSessionsByUser: [SessionModel!]!
}
type SessionMetadataModel {
device: DeviceModel!
ip: String!
location: LocationModel!
}
type SessionModel {
createdAt: String!
id: ID!
metadata: SessionMetadataModel!
userId: String!
} }
type UserModel { type UserModel {

View File

@ -1,7 +1,8 @@
import { Field, ID, ObjectType } from '@nestjs/graphql' import { Field, ID, ObjectType } from '@nestjs/graphql'
import { User } from '@prisma/generated'
@ObjectType() @ObjectType()
export class UserModel { export class UserModel implements User {
@Field(() => ID) @Field(() => ID)
id: string id: string

View File

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

View File

@ -1,6 +1,6 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common'
import { SessionService } from './session.service'; import { SessionService } from './session.service'
import { SessionResolver } from './session.resolver'; import { SessionResolver } from './session.resolver'
@Module({ @Module({
providers: [SessionResolver, SessionService], providers: [SessionResolver, SessionService],

View File

@ -1,23 +1,58 @@
import { UserModel } from '@/src/module/auth/account/models/user.model' import { UserModel } from '@/src/module/auth/account/models/user.model'
import { LoginInput } from '@/src/module/auth/session/inputs/login.input' 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 { 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 { SessionService } from './session.service'
import { SessionModel } from './models/session.model'
@Resolver('Session') @Resolver('Session')
export class SessionResolver { export class SessionResolver {
constructor(private readonly sessionService: SessionService) {} 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' }) @Mutation(() => UserModel, { name: 'loginUser' })
public async login( public async login(
@Context() { req }: GqlContext, @Context() { req }: GqlContext,
@Args('data') input: LoginInput, @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' }) @Mutation(() => Boolean, { name: 'logoutUser' })
public async logout(@Context() { req }: GqlContext) { public async logout(@Context() { req }: GqlContext) {
return this.sessionService.logout(req) 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)
}
} }

View File

@ -1,6 +1,9 @@
import { PrismaService } from '@/src/core/prisma/prisma.service' 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 { LoginInput } from '@/src/module/auth/session/inputs/login.input'
import { getSessionMetadata } from '@/src/shared/util/session-metadata.util'
import { import {
ConflictException,
Injectable, Injectable,
InternalServerErrorException, InternalServerErrorException,
NotFoundException, NotFoundException,
@ -9,15 +12,65 @@ import {
import { ConfigService } from '@nestjs/config' import { ConfigService } from '@nestjs/config'
import { verify } from 'argon2' import { verify } from 'argon2'
import { Request } from 'express' import { Request } from 'express'
import { SessionData } from 'express-session'
@Injectable() @Injectable()
export class SessionService { export class SessionService {
constructor( constructor(
private readonly prismaService: PrismaService, private readonly prismaService: PrismaService,
private readonly redisService: RedisService,
private readonly configService: ConfigService, 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<string>('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 { login, password } = input
const user = await this.prismaService.user.findFirst({ where: { const user = await this.prismaService.user.findFirst({ where: {
@ -39,6 +92,7 @@ export class SessionService {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
req.session.userId = user.id req.session.userId = user.id
req.session.createdAt = new Date() req.session.createdAt = new Date()
req.session.metadata = getSessionMetadata(req, userAgent)
req.session.save((error) => { req.session.save((error) => {
if (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<string>('SESSION_FOLDER')}${id}`
await this.redisService.del(key)
return true
}
} }

View File

@ -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<GqlContext>().req
return req.headers['user-agent']
},
)

View File

@ -1,8 +1,10 @@
import 'express-session' import 'express-session'
import { SessionInfo } from './session-metadata.types'
declare module 'express-session' { declare module 'express-session' {
interface SessionData { interface SessionData {
userId?: string userId?: string
createdAt?: Date | string createdAt?: Date | string
metadata: SessionInfo
} }
} }

View File

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

View File

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

View File

@ -1771,6 +1771,11 @@
"@types/qs" "*" "@types/qs" "*"
"@types/serve-static" "*" "@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": "@types/graceful-fs@^4.1.3":
version "4.1.9" version "4.1.9"
resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4" 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: dependencies:
retry "0.13.1" 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: async@^3.2.3:
version "3.2.6" version "3.2.6"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" 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" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001723.tgz#c4f3174f02089720736e1887eab345e09bb10944"
integrity sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw== 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" version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== 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" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== 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: dezalgo@^1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81"
@ -3132,6 +3149,11 @@ dezalgo@^1.0.4:
asap "^2.0.0" asap "^2.0.0"
wrappy "1" 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: diff-sequences@^29.6.3:
version "29.6.3" version "29.6.3"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921"
@ -3606,6 +3628,13 @@ fb-watchman@^2.0.0:
dependencies: dependencies:
bser "2.1.1" 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: fflate@^0.8.2:
version "0.8.2" version "0.8.2"
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea" 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" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== 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: get-caller-file@^2.0.5:
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" 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" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== 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: iconv-lite@0.4.24, iconv-lite@^0.4.24:
version "0.4.24" version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" 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: dependencies:
safer-buffer ">= 2.1.2 < 3" 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: ieee754@^1.1.13, ieee754@^1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
@ -4112,7 +4161,7 @@ inspect-with-kind@^1.0.5:
dependencies: dependencies:
kind-of "^6.0.2" kind-of "^6.0.2"
ioredis@5.6.1: ioredis@^5.6.1:
version "5.6.1" version "5.6.1"
resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.6.1.tgz#1ed7dc9131081e77342503425afceaf7357ae599" resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.6.1.tgz#1ed7dc9131081e77342503425afceaf7357ae599"
integrity sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA== integrity sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==
@ -4127,6 +4176,15 @@ ioredis@5.6.1:
redis-parser "^3.0.0" redis-parser "^3.0.0"
standard-as-callback "^2.1.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: ipaddr.js@1.9.1:
version "1.9.1" version "1.9.1"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
@ -4678,6 +4736,11 @@ js-yaml@^4.1.0:
dependencies: dependencies:
argparse "^2.0.1" 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: jsesc@^3.0.2:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" 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" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== 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: leven@^3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" 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" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== 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" version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@ -5596,6 +5664,13 @@ reusify@^1.0.4:
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f"
integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== 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: router@^2.2.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/router/-/router-2.2.0.tgz#019be620b711c87641167cc79b99090f00b146ef" 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" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== 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: sprintf-js@~1.0.2:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" 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" y18n "^5.0.5"
yargs-parser "^21.1.1" 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: yauzl@^3.1.2:
version "3.2.0" version "3.2.0"
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-3.2.0.tgz#7b6cb548f09a48a6177ea0be8ece48deb7da45c0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-3.2.0.tgz#7b6cb548f09a48a6177ea0be8ece48deb7da45c0"