diff --git a/backend/package.json b/backend/package.json index 76ef320..2fb2469 100644 --- a/backend/package.json +++ b/backend/package.json @@ -35,6 +35,7 @@ "@react-email/components": "^0.1.1", "@react-email/html": "^0.0.11", "@react-email/tailwind": "^1.0.5", + "@types/qrcode": "^1.5.5", "argon2": "^0.43.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", @@ -44,9 +45,12 @@ "express-session": "^1.18.1", "geoip-lite": "^1.4.10", "graphql": "^16.11.0", + "hi-base32": "^0.5.1", "i18n-iso-countries": "^7.14.0", "ioredis": "^5.6.1", + "otpauth": "^9.4.0", "prisma": "^6.10.1", + "qrcode": "^1.5.4", "react": "^19.1.0", "react-dom": "^19.1.0", "reflect-metadata": "^0.2.2", diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index e5a4a27..7ee84f9 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -19,6 +19,8 @@ model User { token Token[] isVerified Boolean @default(false) @map("is_verified") isEmailVerified Boolean @default(false) @map("is_email_verified") + isTotpEnabled Boolean @default(false) @map("is_totp_enabled") + totpSecret String? @map("totp_secret") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") diff --git a/backend/src/core/core.module.ts b/backend/src/core/core.module.ts index e151419..500e7e1 100644 --- a/backend/src/core/core.module.ts +++ b/backend/src/core/core.module.ts @@ -2,6 +2,7 @@ import { getGraphQLConfig } from '@/src/core/config/graphql.config' import { AccountModule } from '@/src/module/auth/account/account.module' import { PasswordRecoveryModule } from '@/src/module/auth/password-recovery/password-recovery.module' import { SessionModule } from '@/src/module/auth/session/session.module' +import { TotpModule } from '@/src/module/auth/totp/totp.module' import { VerificationModule } from '@/src/module/auth/verification/verification.module' import { MailModule } from '@/src/module/libs/mail/mail.module' import { IS_DEV } from '@/src/shared/util/is-dev.util' @@ -31,6 +32,7 @@ import { RedisModule } from './redis/redis.module' SessionModule, VerificationModule, PasswordRecoveryModule, + TotpModule, ], }) export class CoreModule {} diff --git a/backend/src/core/graphql/schema.gql b/backend/src/core/graphql/schema.gql index fe3b2e2..f2dc7d1 100644 --- a/backend/src/core/graphql/schema.gql +++ b/backend/src/core/graphql/schema.gql @@ -2,6 +2,11 @@ # THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) # ------------------------------------------------------ +type AuthModel { + message: String + user: UserModel +} + input CreateUserInput { email: String! name: String! @@ -19,6 +24,11 @@ type DeviceModel { type: String! } +input EnableTotpInput { + pin: String! + secret: String! +} + type LocationModel { city: String! country: String! @@ -29,12 +39,15 @@ type LocationModel { input LoginInput { login: String! password: String! + pin: String } type Mutation { clearSessionCookie: Boolean! createUser(data: CreateUserInput!): UserModel! - loginUser(data: LoginInput!): UserModel! + disableTotp: Boolean! + enableTotp(data: EnableTotpInput!): Boolean! + loginUser(data: LoginInput!): AuthModel! logoutUser: Boolean! removeSession(id: String!): Boolean! resetPassword(data: ResetPasswordInput!): Boolean! @@ -52,6 +65,7 @@ type Query { findCurrentSession: SessionModel! findProfile: UserModel! findSessionsByUser: [SessionModel!]! + generateTotpSecret: TotpModel! } input ResetPasswordInput { @@ -71,6 +85,11 @@ type SessionModel { userId: String! } +type TotpModel { + qrcodeUrl: String! + secret: String! +} + type UserModel { avatar: String bio: String @@ -79,9 +98,11 @@ type UserModel { email: String! id: ID! isEmailVerified: Boolean! + isTotpEnabled: Boolean! isVerified: Boolean! name: String! password: String! + totpSecret: String updatedAt: DateTime! } diff --git a/backend/src/module/auth/account/models/auth.model.ts b/backend/src/module/auth/account/models/auth.model.ts new file mode 100644 index 0000000..53b635f --- /dev/null +++ b/backend/src/module/auth/account/models/auth.model.ts @@ -0,0 +1,11 @@ +import { UserModel } from '@/src/module/auth/account/models/user.model' +import { Field, ObjectType } from '@nestjs/graphql' + +@ObjectType() +export class AuthModel { + @Field(() => UserModel, { nullable: true }) + public user?: UserModel + + @Field(() => String, { nullable: true }) + public message: string +} diff --git a/backend/src/module/auth/account/models/user.model.ts b/backend/src/module/auth/account/models/user.model.ts index 5e976e5..c0c00ee 100644 --- a/backend/src/module/auth/account/models/user.model.ts +++ b/backend/src/module/auth/account/models/user.model.ts @@ -30,6 +30,12 @@ export class UserModel implements User { @Field(() => Boolean) isVerified: boolean + @Field(() => Boolean) + isTotpEnabled: boolean + + @Field(() => String, { nullable: true }) + totpSecret: string + @Field(() => Date) createdAt: Date diff --git a/backend/src/module/auth/password-recovery/input/new-password.input.ts b/backend/src/module/auth/password-recovery/inputs/new-password.input.ts similarity index 100% rename from backend/src/module/auth/password-recovery/input/new-password.input.ts rename to backend/src/module/auth/password-recovery/inputs/new-password.input.ts diff --git a/backend/src/module/auth/password-recovery/input/reset-password.input.ts b/backend/src/module/auth/password-recovery/inputs/reset-password.input.ts similarity index 100% rename from backend/src/module/auth/password-recovery/input/reset-password.input.ts rename to backend/src/module/auth/password-recovery/inputs/reset-password.input.ts diff --git a/backend/src/module/auth/password-recovery/password-recovery.resolver.ts b/backend/src/module/auth/password-recovery/password-recovery.resolver.ts index ac0de9c..0300f5b 100644 --- a/backend/src/module/auth/password-recovery/password-recovery.resolver.ts +++ b/backend/src/module/auth/password-recovery/password-recovery.resolver.ts @@ -1,5 +1,5 @@ -import { NewPasswordInput } from '@/src/module/auth/password-recovery/input/new-password.input' -import { ResetPasswordInput } from '@/src/module/auth/password-recovery/input/reset-password.input' +import { NewPasswordInput } from '@/src/module/auth/password-recovery/inputs/new-password.input' +import { ResetPasswordInput } from '@/src/module/auth/password-recovery/inputs/reset-password.input' 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' diff --git a/backend/src/module/auth/password-recovery/password-recovery.service.ts b/backend/src/module/auth/password-recovery/password-recovery.service.ts index 0670224..42ff743 100644 --- a/backend/src/module/auth/password-recovery/password-recovery.service.ts +++ b/backend/src/module/auth/password-recovery/password-recovery.service.ts @@ -1,6 +1,6 @@ import { PrismaService } from '@/src/core/prisma/prisma.service' -import { NewPasswordInput } from '@/src/module/auth/password-recovery/input/new-password.input' -import { ResetPasswordInput } from '@/src/module/auth/password-recovery/input/reset-password.input' +import { NewPasswordInput } from '@/src/module/auth/password-recovery/inputs/new-password.input' +import { ResetPasswordInput } from '@/src/module/auth/password-recovery/inputs/reset-password.input' import { MailService } from '@/src/module/libs/mail/mail.service' import { generateToken } from '@/src/shared/util/generate-token.util' import { getSessionMetadata } from '@/src/shared/util/session-metadata.util' diff --git a/backend/src/module/auth/session/inputs/login.input.ts b/backend/src/module/auth/session/inputs/login.input.ts index 46db919..db0cb4d 100644 --- a/backend/src/module/auth/session/inputs/login.input.ts +++ b/backend/src/module/auth/session/inputs/login.input.ts @@ -1,5 +1,5 @@ import { Field, InputType } from '@nestjs/graphql' -import { IsNotEmpty, IsString, MinLength } from 'class-validator' +import { IsNotEmpty, IsOptional, IsString, Length, MinLength } from 'class-validator' @InputType() export class LoginInput { @@ -13,4 +13,11 @@ export class LoginInput { @IsNotEmpty() @MinLength(8) password: string + + @Field(() => String, { nullable: true }) + @IsString() + @IsNotEmpty() + @IsOptional() + @Length(6, 6) + pin?: string } diff --git a/backend/src/module/auth/session/session.resolver.ts b/backend/src/module/auth/session/session.resolver.ts index c671900..0ae7136 100644 --- a/backend/src/module/auth/session/session.resolver.ts +++ b/backend/src/module/auth/session/session.resolver.ts @@ -1,3 +1,4 @@ +import { AuthModel } from '@/src/module/auth/account/models/auth.model'; 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' @@ -27,7 +28,7 @@ export class SessionResolver { return this.sessionService.findCurrentSession(req) } - @Mutation(() => UserModel, { name: 'loginUser' }) + @Mutation(() => AuthModel, { name: 'loginUser' }) public async login( @Context() { req }: GqlContext, @Args('data') input: LoginInput, diff --git a/backend/src/module/auth/session/session.service.ts b/backend/src/module/auth/session/session.service.ts index 58bb214..5311d35 100644 --- a/backend/src/module/auth/session/session.service.ts +++ b/backend/src/module/auth/session/session.service.ts @@ -15,6 +15,7 @@ import { ConfigService } from '@nestjs/config' import { verify } from 'argon2' import { Request } from 'express' import { SessionData } from 'express-session' +import { TOTP } from 'otpauth' @Injectable() export class SessionService { @@ -74,7 +75,7 @@ export class SessionService { } public async login(req: Request, input: LoginInput, userAgent: string) { - const { login, password } = input + const { login, password, pin } = input const user = await this.prismaService.user.findFirst({ where: { OR: [ @@ -97,6 +98,28 @@ export class SessionService { throw new BadRequestException('Аккаунт не верифицирован. Проверьте свою почту для подтверждения') } + if (user.isTotpEnabled) { + if (!pin) { + return { + message: 'Необходимо ввести пин-код для завершения операции', + } + } + + const totp = new TOTP({ + issuer: 'TeaStream', + label: user.email, + algorithm: 'SHA-1', + digits: 6, + secret: user.totpSecret!, + }) + + const delta = totp.validate({ token: pin }) + + if (delta === null) { + throw new BadRequestException('Невереый код') + } + } + return saveSession(req, user, getSessionMetadata(req, userAgent)) } diff --git a/backend/src/module/auth/totp/inputs/enable-totp.input.ts b/backend/src/module/auth/totp/inputs/enable-totp.input.ts new file mode 100644 index 0000000..ec1bdb9 --- /dev/null +++ b/backend/src/module/auth/totp/inputs/enable-totp.input.ts @@ -0,0 +1,16 @@ +import { Field, InputType } from '@nestjs/graphql' +import { IsNotEmpty, IsString, Length } from 'class-validator' + +@InputType() +export class EnableTotpInput { + @Field(() => String) + @IsString() + @IsNotEmpty() + secret: string + + @Field(() => String) + @IsString() + @IsNotEmpty() + @Length(6, 6) + pin: string +} diff --git a/backend/src/module/auth/totp/models/totp.model.ts b/backend/src/module/auth/totp/models/totp.model.ts new file mode 100644 index 0000000..e0edc80 --- /dev/null +++ b/backend/src/module/auth/totp/models/totp.model.ts @@ -0,0 +1,10 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +export class TotpModel { + @Field(() => String) + public qrcodeUrl: string + + @Field(() => String) + public secret: string +} diff --git a/backend/src/module/auth/totp/totp.module.ts b/backend/src/module/auth/totp/totp.module.ts new file mode 100644 index 0000000..644e67b --- /dev/null +++ b/backend/src/module/auth/totp/totp.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { TotpService } from './totp.service'; +import { TotpResolver } from './totp.resolver'; + +@Module({ + providers: [TotpResolver, TotpService], +}) +export class TotpModule {} diff --git a/backend/src/module/auth/totp/totp.resolver.ts b/backend/src/module/auth/totp/totp.resolver.ts new file mode 100644 index 0000000..1984a1a --- /dev/null +++ b/backend/src/module/auth/totp/totp.resolver.ts @@ -0,0 +1,33 @@ +import { EnableTotpInput } from '@/src/module/auth/totp/inputs/enable-totp.input' +import { TotpModel } from '@/src/module/auth/totp/models/totp.model' +import { Authorization } from '@/src/shared/decorators/auth.decorator' +import { Authorized } from '@/src/shared/decorators/authorized.decorator' +import { Args, Mutation, Query, Resolver } from '@nestjs/graphql' +import { User } from '@prisma/generated' +import { TotpService } from './totp.service' + +@Resolver('Totp') +export class TotpResolver { + constructor(private readonly totpService: TotpService) {} + + @Authorization() + @Query(() => TotpModel, { name: 'generateTotpSecret' }) + public async generate(@Authorized() user: User) { + return this.totpService.generate(user) + } + + @Authorization() + @Mutation(() => Boolean, { name: 'enableTotp' }) + public async enable( + @Authorized() user: User, + @Args('data') input: EnableTotpInput, + ) { + return this.totpService.enable(user, input) + } + + @Authorization() + @Mutation(() => Boolean, { name: 'disableTotp' }) + public async disable(@Authorized() user: User) { + return this.totpService.disable(user) + } +} diff --git a/backend/src/module/auth/totp/totp.service.ts b/backend/src/module/auth/totp/totp.service.ts new file mode 100644 index 0000000..7ebb0c1 --- /dev/null +++ b/backend/src/module/auth/totp/totp.service.ts @@ -0,0 +1,77 @@ +import { PrismaService } from '@/src/core/prisma/prisma.service' +import { EnableTotpInput } from '@/src/module/auth/totp/inputs/enable-totp.input' +import { BadRequestException, Injectable } from '@nestjs/common' +import { User } from '@prisma/generated' +import { encode } from 'hi-base32' +import { randomBytes } from 'node:crypto' +import { TOTP } from 'otpauth' +import * as QRCode from 'qrcode' + +@Injectable() +export class TotpService { + constructor( + private readonly prismaService: PrismaService, + ) { + } + + async generate(user: User) { + const secret = encode(randomBytes(15)) + .replace(/=/g, '') + .substring(0, 24) + + const totp = new TOTP({ + issuer: 'TeaStream', + label: user.email, + algorithm: 'SHA-1', + digits: 6, + secret, + }) + + const qrcodeUrl = await QRCode.toDataURL(totp.toString()) + + return { + qrcodeUrl, + secret, + } + } + + async enable(user: User, input: EnableTotpInput) { + const { pin, secret } = input + + const totp = new TOTP({ + issuer: 'TeaStream', + label: user.email, + algorithm: 'SHA-1', + digits: 6, + secret, + }) + + const delta = totp.validate({ token: pin }) + + if (delta === null) { + throw new BadRequestException('Невереый код') + } + + await this.prismaService.user.update({ + where: { id: user.id }, + data: { + isTotpEnabled: true, + totpSecret: secret, + }, + }) + + return true + } + + async disable(user: User) { + await this.prismaService.user.update({ + where: { id: user.id }, + data: { + isTotpEnabled: false, + totpSecret: null, + }, + }) + + return true + } +} diff --git a/backend/src/shared/decorators/is-password-matching-constraint.decorator.ts b/backend/src/shared/decorators/is-password-matching-constraint.decorator.ts index 4aa320e..1bf78f4 100644 --- a/backend/src/shared/decorators/is-password-matching-constraint.decorator.ts +++ b/backend/src/shared/decorators/is-password-matching-constraint.decorator.ts @@ -1,4 +1,4 @@ -import { NewPasswordInput } from '@/src/module/auth/password-recovery/input/new-password.input' +import { NewPasswordInput } from '@/src/module/auth/password-recovery/inputs/new-password.input' import { ValidationArguments, ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator' @ValidatorConstraint({ name: 'IsPasswordMatching', async: false }) diff --git a/backend/src/shared/util/session.util.ts b/backend/src/shared/util/session.util.ts index b625765..a04d7ae 100644 --- a/backend/src/shared/util/session.util.ts +++ b/backend/src/shared/util/session.util.ts @@ -1,11 +1,11 @@ import { SessionInfo } from '@/src/shared/types/session-metadata.types' import { InternalServerErrorException } from '@nestjs/common' -import { ConfigService } from '@nestjs/config'; +import { ConfigService } from '@nestjs/config' import { User } from '@prisma/generated' import { Request } from 'express' export function saveSession(req: Request, user: User, metadata: SessionInfo) { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { req.session.createdAt = new Date() req.session.userId = user.id req.session.metadata = metadata @@ -15,7 +15,7 @@ export function saveSession(req: Request, user: User, metadata: SessionInfo) { reject(new InternalServerErrorException('Не удалось сохранить сессию')) } - resolve(user) + resolve({ user }) }) }) } diff --git a/backend/yarn.lock b/backend/yarn.lock index 8dd57ac..35fbdc7 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -1426,6 +1426,11 @@ dependencies: tslib "2.8.1" +"@noble/hashes@1.7.1": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.7.1.tgz#5738f6d765710921e7a751e00c20ae091ed8db0f" + integrity sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ== + "@noble/hashes@^1.1.5": version "1.8.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a" @@ -2140,6 +2145,13 @@ resolved "https://registry.yarnpkg.com/@types/pug/-/pug-2.0.10.tgz#52f8dbd6113517aef901db20b4f3fca543b88c1f" integrity sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA== +"@types/qrcode@^1.5.5": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.5.5.tgz#993ff7c6b584277eee7aac0a20861eab682f9dac" + integrity sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg== + dependencies: + "@types/node" "*" + "@types/qs@*": version "6.14.0" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.14.0.tgz#d8b60cecf62f2db0fb68e5e006077b9178b85de5" @@ -3068,7 +3080,7 @@ camel-case@^3.0.0: no-case "^2.2.0" upper-case "^1.1.1" -camelcase@^5.3.1: +camelcase@^5.0.0, camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== @@ -3225,6 +3237,15 @@ cli-width@^4.1.0: resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-4.1.0.tgz#42daac41d3c254ef38ad8ac037672130173691c5" integrity sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ== +cliui@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" + integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^6.2.0" + cliui@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" @@ -3525,6 +3546,11 @@ debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3 dependencies: ms "^2.1.3" +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== + decompress-response@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" @@ -3632,6 +3658,11 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== +dijkstrajs@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz#4c8dbdea1f0f6478bff94d9c49c784d623e4fc23" + integrity sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA== + display-notification@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/display-notification/-/display-notification-2.0.0.tgz#49fad2e03289b4f668c296e1855c2cf8ba893d49" @@ -4495,7 +4526,7 @@ geoip-lite@^1.4.10: 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.1, 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" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== @@ -4728,6 +4759,11 @@ he@1.2.0, he@^1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== +hi-base32@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/hi-base32/-/hi-base32-0.5.1.tgz#1279f2ddae2673219ea5870c2121d2a33132857e" + integrity sha512-EmBBpvdYh/4XxsnUybsPag6VikPYnN30td+vQk+GI3qpahVEG9+gTkG0aXVxTjBqQ5T6ijbWIu77O+C5WFWsnA== + html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" @@ -6610,6 +6646,13 @@ os-tmpdir@~1.0.2: resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== +otpauth@^9.4.0: + version "9.4.0" + resolved "https://registry.yarnpkg.com/otpauth/-/otpauth-9.4.0.tgz#ed52538848cc52138e4dcd75cef45412b6175ea1" + integrity sha512-fHIfzIG5RqCkK9cmV8WU+dPQr9/ebR5QOwGZn2JAr1RQF+lmAuLL2YdtdqvmBjNmgJlYk3KZ4a0XokaEhg1Jsw== + dependencies: + "@noble/hashes" "1.7.1" + p-cancelable@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-3.0.0.tgz#63826694b54d61ca1c20ebcb6d3ecf5e14cd8050" @@ -6841,6 +6884,11 @@ pluralize@8.0.0: resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== +pngjs@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" + integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -7036,6 +7084,15 @@ pure-rand@^6.0.0: resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== +qrcode@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.4.tgz#5cb81d86eb57c675febb08cf007fff963405da88" + integrity sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg== + dependencies: + dijkstrajs "^1.0.1" + pngjs "^5.0.0" + yargs "^15.3.1" + qs@6.13.0: version "6.13.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" @@ -7189,6 +7246,11 @@ require-from-string@^2.0.2: resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + resolve-alpn@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" @@ -7434,6 +7496,11 @@ serve-static@^2.2.0: parseurl "^1.3.3" send "^1.2.0" +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== + setprototypeof@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" @@ -8264,6 +8331,11 @@ whatwg-url@^5.0.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" +which-module@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" + integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== + which@^1.2.9: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" @@ -8370,6 +8442,11 @@ xtend@^4.0.2: resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== +y18n@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" + integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== + y18n@^5.0.5: version "5.0.8" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" @@ -8385,6 +8462,31 @@ yargs-parser@21.1.1, yargs-parser@^21.1.1: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== +yargs-parser@^18.1.2: + version "18.1.3" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs@^15.3.1: + version "15.4.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" + integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== + dependencies: + cliui "^6.0.0" + decamelize "^1.2.0" + find-up "^4.1.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^4.2.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^18.1.2" + yargs@^17.3.1, yargs@^17.7.2: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"