From 62b0514da787761be8e43b653f5877dd65509237 Mon Sep 17 00:00:00 2001 From: Yaosanqi137 Date: Sat, 4 Apr 2026 21:21:43 +0800 Subject: [PATCH] feat(api-auth): add totp 2fa enrollment and verify --- apps/api/.env.example | 1 + apps/api/package.json | 2 + apps/api/src/auth/auth.controller.ts | 19 + apps/api/src/auth/auth.service.ts | 50 +++ .../api/src/auth/dto/two-factor-enroll.dto.ts | 6 + .../api/src/auth/dto/two-factor-verify.dto.ts | 11 + pnpm-lock.yaml | 411 +++++++++++++----- 7 files changed, 384 insertions(+), 116 deletions(-) create mode 100644 apps/api/src/auth/dto/two-factor-enroll.dto.ts create mode 100644 apps/api/src/auth/dto/two-factor-verify.dto.ts diff --git a/apps/api/.env.example b/apps/api/.env.example index 69712b2..4394718 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -3,6 +3,7 @@ AUTH_ACCESS_SECRET="dev-access-secret" AUTH_ACCESS_EXPIRES_IN_SECONDS="900" AUTH_REFRESH_EXPIRES_IN_SECONDS="2592000" AUTH_EMAIL_CODE_TTL_SECONDS="300" +AUTH_TOTP_ISSUER="TodoList" OAUTH_GITHUB_CLIENT_ID="github-client-id" OAUTH_GITHUB_CLIENT_SECRET="github-client-secret" OAUTH_GITHUB_CALLBACK_URL="http://localhost:3000/auth/oauth/github/callback" diff --git a/apps/api/package.json b/apps/api/package.json index a136df6..a1327aa 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -30,9 +30,11 @@ "@nestjs/core": "^11.1.18", "@nestjs/jwt": "^11.0.2", "@nestjs/platform-express": "^11.1.18", + "@otplib/preset-default": "^12.0.1", "@prisma/client": "^7.6.0", "class-transformer": "^0.5.1", "class-validator": "^0.15.1", + "otplib": "^13.4.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.2" } diff --git a/apps/api/src/auth/auth.controller.ts b/apps/api/src/auth/auth.controller.ts index ecf7bd3..1641223 100644 --- a/apps/api/src/auth/auth.controller.ts +++ b/apps/api/src/auth/auth.controller.ts @@ -4,6 +4,8 @@ import { AuthService } from "./auth.service"; import { EmailLoginDto } from "./dto/email-login.dto"; import { RefreshTokenDto } from "./dto/refresh-token.dto"; import { SendEmailCodeDto } from "./dto/send-email-code.dto"; +import { TwoFactorEnrollDto } from "./dto/two-factor-enroll.dto"; +import { TwoFactorVerifyDto } from "./dto/two-factor-verify.dto"; @Controller("auth") export class AuthController { @@ -45,6 +47,23 @@ export class AuthController { return this.authService.revokeRefreshToken(body.refreshToken); } + @Post("2fa/enroll") + async enrollTwoFactor(@Body() body: TwoFactorEnrollDto): Promise<{ + userId: string; + secret: string; + otpauthUrl: string; + enabled: boolean; + }> { + return this.authService.enrollTwoFactor(body.email); + } + + @Post("2fa/verify") + async verifyTwoFactor( + @Body() body: TwoFactorVerifyDto + ): Promise<{ success: boolean; enabled: boolean }> { + return this.authService.verifyTwoFactor(body.email, body.token); + } + @Get("oauth/github") @UseGuards(AuthGuard("github")) githubLogin(): void {} diff --git a/apps/api/src/auth/auth.service.ts b/apps/api/src/auth/auth.service.ts index d48f917..4e25fa1 100644 --- a/apps/api/src/auth/auth.service.ts +++ b/apps/api/src/auth/auth.service.ts @@ -2,6 +2,7 @@ import { Injectable, UnauthorizedException } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { JwtService } from "@nestjs/jwt"; import { randomUUID } from "node:crypto"; +import { authenticator } from "@otplib/preset-default"; type EmailCodeEntry = { code: string; @@ -19,6 +20,11 @@ type RefreshTokenEntry = { revokedAt?: number; }; +type TwoFactorEntry = { + secret: string; + enabled: boolean; +}; + type AuthTokenResult = { accessToken: string; tokenType: "Bearer"; @@ -34,6 +40,7 @@ export class AuthService { private readonly userStoreByEmail = new Map(); private readonly userStoreById = new Map(); private readonly refreshTokenStore = new Map(); + private readonly twoFactorStore = new Map(); constructor( private readonly configService: ConfigService, @@ -111,6 +118,49 @@ export class AuthService { return { success: true }; } + async enrollTwoFactor( + email: string + ): Promise<{ userId: string; secret: string; otpauthUrl: string; enabled: boolean }> { + const user = this.getOrCreateUser(email.toLowerCase()); + const secret = authenticator.generateSecret(); + const issuer = this.configService.get("AUTH_TOTP_ISSUER") ?? "TodoList"; + const otpauthUrl = authenticator.keyuri(user.email, issuer, secret); + + this.twoFactorStore.set(user.id, { + secret, + enabled: false + }); + + return { + userId: user.id, + secret, + otpauthUrl, + enabled: false + }; + } + + async verifyTwoFactor( + email: string, + token: string + ): Promise<{ success: boolean; enabled: boolean }> { + const user = this.getOrCreateUser(email.toLowerCase()); + const entry = this.twoFactorStore.get(user.id); + if (!entry) { + throw new UnauthorizedException("尚未启用两步验证"); + } + + const valid = authenticator.check(token, entry.secret); + if (!valid) { + throw new UnauthorizedException("两步验证码错误"); + } + + entry.enabled = true; + return { + success: true, + enabled: true + }; + } + private getOrCreateUser(email: string): AuthUser { const existingUser = this.userStoreByEmail.get(email); if (existingUser) { diff --git a/apps/api/src/auth/dto/two-factor-enroll.dto.ts b/apps/api/src/auth/dto/two-factor-enroll.dto.ts new file mode 100644 index 0000000..2dea4ef --- /dev/null +++ b/apps/api/src/auth/dto/two-factor-enroll.dto.ts @@ -0,0 +1,6 @@ +import { IsEmail } from "class-validator"; + +export class TwoFactorEnrollDto { + @IsEmail() + email!: string; +} diff --git a/apps/api/src/auth/dto/two-factor-verify.dto.ts b/apps/api/src/auth/dto/two-factor-verify.dto.ts new file mode 100644 index 0000000..f9317bd --- /dev/null +++ b/apps/api/src/auth/dto/two-factor-verify.dto.ts @@ -0,0 +1,11 @@ +import { IsEmail, IsString, Length, Matches } from "class-validator"; + +export class TwoFactorVerifyDto { + @IsEmail() + email!: string; + + @IsString() + @Length(6, 6) + @Matches(/^\d{6}$/) + token!: string; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a4d24c9..051c827 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,12 +43,12 @@ importers: "@nestjs/jwt": specifier: ^11.0.2 version: 11.0.2(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)) - "@nestjs/passport": - specifier: ^11.0.5 - version: 11.0.5(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) "@nestjs/platform-express": specifier: ^11.1.18 version: 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) + "@otplib/preset-default": + specifier: ^12.0.1 + version: 12.0.1 "@prisma/client": specifier: ^7.6.0 version: 7.6.0(prisma@7.6.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3) @@ -58,15 +58,9 @@ importers: class-validator: specifier: ^0.15.1 version: 0.15.1 - passport: - specifier: ^0.7.0 - version: 0.7.0 - passport-github2: - specifier: ^0.1.12 - version: 0.1.12 - passport-oauth2: - specifier: ^1.8.0 - version: 1.8.0 + otplib: + specifier: ^13.4.0 + version: 13.4.0 reflect-metadata: specifier: ^0.2.2 version: 0.2.2 @@ -77,6 +71,12 @@ importers: "@types/node": specifier: ^25.5.2 version: 25.5.2 + "@types/passport-github2": + specifier: ^1.2.9 + version: 1.2.9 + "@types/passport-oauth2": + specifier: ^1.8.0 + version: 1.8.0 dotenv: specifier: ^16.6.1 version: 16.6.1 @@ -332,15 +332,6 @@ packages: peerDependencies: "@nestjs/common": ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 - "@nestjs/passport@11.0.5": - resolution: - { - integrity: sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ== - } - peerDependencies: - "@nestjs/common": ^10.0.0 || ^11.0.0 - passport: ^0.5.0 || ^0.6.0 || ^0.7.0 - "@nestjs/platform-express@11.1.18": resolution: { @@ -350,6 +341,13 @@ packages: "@nestjs/common": ^11.0.0 "@nestjs/core": ^11.0.0 + "@noble/hashes@2.0.1": + resolution: + { + integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw== + } + engines: { node: ">= 20.19.0" } + "@nuxt/opencollective@0.4.1": resolution: { @@ -358,6 +356,69 @@ packages: engines: { node: ^14.18.0 || >=16.10.0, npm: ">=5.10.0" } hasBin: true + "@otplib/core@12.0.1": + resolution: + { + integrity: sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA== + } + + "@otplib/core@13.4.0": + resolution: + { + integrity: sha512-JqOGcvZQi2wIkEQo8f3/iAjstavpXy6gouIDMHygjNuH6Q0FjbHOiXMdcE94RwfgDNMABhzwUmvaPsxvgm9NYw== + } + + "@otplib/hotp@13.4.0": + resolution: + { + integrity: sha512-MJjE0x06mn2ptymz5qZmQveb+vWFuaIftqE0b5/TZZqUOK7l97cV8lRTmid5BpAQMwJDNLW6RnYxGeCRiNdekw== + } + + "@otplib/plugin-base32-scure@13.4.0": + resolution: + { + integrity: sha512-/t9YWJmMbB8bF5z8mXrBZc2FXBe8B/3hG5FhWr9K8cFwFhyxScbPysmZe8s1UTzSA6N+s8Uv8aIfCtVXPNjJWw== + } + + "@otplib/plugin-crypto-noble@13.4.0": + resolution: + { + integrity: sha512-KrvE4m7Zv+TT1944HzgqFJWJpKb6AyoxDbvhPStmBqdMlv5Gekb80d66cuFRL08kkPgJ5gXUSb5SFpYeB+bACg== + } + + "@otplib/plugin-crypto@12.0.1": + resolution: + { + integrity: sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g== + } + deprecated: Please upgrade to v13 of otplib. Refer to otplib docs for migration paths + + "@otplib/plugin-thirty-two@12.0.1": + resolution: + { + integrity: sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA== + } + deprecated: Please upgrade to v13 of otplib. Refer to otplib docs for migration paths + + "@otplib/preset-default@12.0.1": + resolution: + { + integrity: sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ== + } + deprecated: Please upgrade to v13 of otplib. Refer to otplib docs for migration paths + + "@otplib/totp@13.4.0": + resolution: + { + integrity: sha512-dK+vl0f0ekzf6mCENRI9AKS2NJUC7OjI3+X8e7QSnhQ2WM7I+i4PGpb3QxKi5hxjTtwVuoZwXR2CFtXdcRtNdQ== + } + + "@otplib/uri@13.4.0": + resolution: + { + integrity: sha512-x1ozBa5bPbdZCrrTL/HK21qchiK7jYElTu+0ft22abeEhiLYgH1+SIULvOcVk3CK8YwF4kdcidvkq4ciejucJA== + } + "@prisma/client-runtime-utils@7.6.0": resolution: { @@ -555,6 +616,12 @@ packages: "@types/react": optional: true + "@scure/base@2.0.0": + resolution: + { + integrity: sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w== + } + "@standard-schema/spec@1.1.0": resolution: { @@ -646,6 +713,18 @@ packages: cpu: [arm64] os: [win32] + "@types/body-parser@1.19.6": + resolution: + { + integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g== + } + + "@types/connect@3.4.38": + resolution: + { + integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + } + "@types/esrecurse@4.3.1": resolution: { @@ -658,6 +737,24 @@ packages: integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== } + "@types/express-serve-static-core@5.1.1": + resolution: + { + integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A== + } + + "@types/express@5.0.6": + resolution: + { + integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA== + } + + "@types/http-errors@2.0.5": + resolution: + { + integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg== + } + "@types/json-schema@7.0.15": resolution: { @@ -682,12 +779,60 @@ packages: integrity: sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg== } + "@types/oauth@0.9.6": + resolution: + { + integrity: sha512-H9TRCVKBNOhZZmyHLqFt9drPM9l+ShWiqqJijU1B8P3DX3ub84NjxDuy+Hjrz+fEca5Kwip3qPMKNyiLgNJtIA== + } + + "@types/passport-github2@1.2.9": + resolution: + { + integrity: sha512-/nMfiPK2E6GKttwBzwj0Wjaot8eHrM57hnWxu52o6becr5/kXlH/4yE2v2rh234WGvSgEEzIII02Nc5oC5xEHA== + } + + "@types/passport-oauth2@1.8.0": + resolution: + { + integrity: sha512-6//z+4orIOy/g3zx17HyQ71GSRK4bs7Sb+zFasRoc2xzlv7ZCJ+vkDBYFci8U6HY+or6Zy7ajf4mz4rK7nsWJQ== + } + + "@types/passport@1.0.17": + resolution: + { + integrity: sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg== + } + + "@types/qs@6.15.0": + resolution: + { + integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow== + } + + "@types/range-parser@1.2.7": + resolution: + { + integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + } + "@types/react@19.2.14": resolution: { integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w== } + "@types/send@1.2.1": + resolution: + { + integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ== + } + + "@types/serve-static@2.2.0": + resolution: + { + integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ== + } + "@types/strip-bom@3.0.0": resolution: { @@ -808,13 +953,6 @@ packages: } engines: { node: 18 || 20 || >=22 } - base64url@3.0.1: - resolution: - { - integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== - } - engines: { node: ">=6.0.0" } - better-result@2.7.0: resolution: { @@ -2030,12 +2168,6 @@ packages: engines: { node: ">=18" } hasBin: true - oauth@0.10.2: - resolution: - { - integrity: sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q== - } - object-assign@4.1.1: resolution: { @@ -2083,6 +2215,12 @@ packages: } engines: { node: ">= 0.8.0" } + otplib@13.4.0: + resolution: + { + integrity: sha512-RUcYcRMCgRWhUE/XabRppXpUwCwaWBNHe5iPXhdvP8wwDGpGpsIf/kxX/ec3zFsOaM1Oq8lEhUqDwk6W7DHkwg== + } + p-limit@3.1.0: resolution: { @@ -2104,34 +2242,6 @@ packages: } engines: { node: ">= 0.8" } - passport-github2@0.1.12: - resolution: - { - integrity: sha512-3nPUCc7ttF/3HSP/k9sAXjz3SkGv5Nki84I05kSQPo01Jqq1NzJACgMblCK0fGcv9pKCG/KXU3AJRDGLqHLoIw== - } - engines: { node: ">= 0.8.0" } - - passport-oauth2@1.8.0: - resolution: - { - integrity: sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA== - } - engines: { node: ">= 0.4.0" } - - passport-strategy@1.0.0: - resolution: - { - integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA== - } - engines: { node: ">= 0.4.0" } - - passport@0.7.0: - resolution: - { - integrity: sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ== - } - engines: { node: ">= 0.4.0" } - path-exists@4.0.0: resolution: { @@ -2171,12 +2281,6 @@ packages: integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== } - pause@0.0.1: - resolution: - { - integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg== - } - perfect-debounce@1.0.0: resolution: { @@ -2633,6 +2737,13 @@ packages: } engines: { node: ">= 0.4" } + thirty-two@1.0.2: + resolution: + { + integrity: sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA== + } + engines: { node: ">=0.2.6" } + tinyexec@1.0.4: resolution: { @@ -2753,12 +2864,6 @@ packages: engines: { node: ">=14.17" } hasBin: true - uid2@0.0.4: - resolution: - { - integrity: sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA== - } - uid@2.0.2: resolution: { @@ -2798,13 +2903,6 @@ packages: integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== } - utils-merge@1.0.1: - resolution: - { - integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== - } - engines: { node: ">= 0.4.0" } - v8-compile-cache-lib@3.0.1: resolution: { @@ -3032,11 +3130,6 @@ snapshots: "@types/jsonwebtoken": 9.0.10 jsonwebtoken: 9.0.3 - "@nestjs/passport@11.0.5(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)": - dependencies: - "@nestjs/common": 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - passport: 0.7.0 - "@nestjs/platform-express@11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)": dependencies: "@nestjs/common": 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -3049,10 +3142,56 @@ snapshots: transitivePeerDependencies: - supports-color + "@noble/hashes@2.0.1": {} + "@nuxt/opencollective@0.4.1": dependencies: consola: 3.4.2 + "@otplib/core@12.0.1": {} + + "@otplib/core@13.4.0": {} + + "@otplib/hotp@13.4.0": + dependencies: + "@otplib/core": 13.4.0 + "@otplib/uri": 13.4.0 + + "@otplib/plugin-base32-scure@13.4.0": + dependencies: + "@otplib/core": 13.4.0 + "@scure/base": 2.0.0 + + "@otplib/plugin-crypto-noble@13.4.0": + dependencies: + "@noble/hashes": 2.0.1 + "@otplib/core": 13.4.0 + + "@otplib/plugin-crypto@12.0.1": + dependencies: + "@otplib/core": 12.0.1 + + "@otplib/plugin-thirty-two@12.0.1": + dependencies: + "@otplib/core": 12.0.1 + thirty-two: 1.0.2 + + "@otplib/preset-default@12.0.1": + dependencies: + "@otplib/core": 12.0.1 + "@otplib/plugin-crypto": 12.0.1 + "@otplib/plugin-thirty-two": 12.0.1 + + "@otplib/totp@13.4.0": + dependencies: + "@otplib/core": 13.4.0 + "@otplib/hotp": 13.4.0 + "@otplib/uri": 13.4.0 + + "@otplib/uri@13.4.0": + dependencies: + "@otplib/core": 13.4.0 + "@prisma/client-runtime-utils@7.6.0": {} "@prisma/client@7.6.0(prisma@7.6.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3)": @@ -3193,6 +3332,8 @@ snapshots: optionalDependencies: "@types/react": 19.2.14 + "@scure/base@2.0.0": {} + "@standard-schema/spec@1.1.0": {} "@tokenizer/inflate@0.4.1": @@ -3230,10 +3371,34 @@ snapshots: "@turbo/windows-arm64@2.9.3": optional: true + "@types/body-parser@1.19.6": + dependencies: + "@types/connect": 3.4.38 + "@types/node": 25.5.2 + + "@types/connect@3.4.38": + dependencies: + "@types/node": 25.5.2 + "@types/esrecurse@4.3.1": {} "@types/estree@1.0.8": {} + "@types/express-serve-static-core@5.1.1": + dependencies: + "@types/node": 25.5.2 + "@types/qs": 6.15.0 + "@types/range-parser": 1.2.7 + "@types/send": 1.2.1 + + "@types/express@5.0.6": + dependencies: + "@types/body-parser": 1.19.6 + "@types/express-serve-static-core": 5.1.1 + "@types/serve-static": 2.2.0 + + "@types/http-errors@2.0.5": {} + "@types/json-schema@7.0.15": {} "@types/jsonwebtoken@9.0.10": @@ -3247,10 +3412,43 @@ snapshots: dependencies: undici-types: 7.18.2 + "@types/oauth@0.9.6": + dependencies: + "@types/node": 25.5.2 + + "@types/passport-github2@1.2.9": + dependencies: + "@types/express": 5.0.6 + "@types/passport": 1.0.17 + "@types/passport-oauth2": 1.8.0 + + "@types/passport-oauth2@1.8.0": + dependencies: + "@types/express": 5.0.6 + "@types/oauth": 0.9.6 + "@types/passport": 1.0.17 + + "@types/passport@1.0.17": + dependencies: + "@types/express": 5.0.6 + + "@types/qs@6.15.0": {} + + "@types/range-parser@1.2.7": {} + "@types/react@19.2.14": dependencies: csstype: 3.2.3 + "@types/send@1.2.1": + dependencies: + "@types/node": 25.5.2 + + "@types/serve-static@2.2.0": + dependencies: + "@types/http-errors": 2.0.5 + "@types/node": 25.5.2 + "@types/strip-bom@3.0.0": {} "@types/strip-json-comments@0.0.30": {} @@ -3309,8 +3507,6 @@ snapshots: balanced-match@4.0.4: {} - base64url@3.0.1: {} - better-result@2.7.0: dependencies: "@clack/prompts": 0.11.0 @@ -4007,8 +4203,6 @@ snapshots: pathe: 2.0.3 tinyexec: 1.0.4 - oauth@0.10.2: {} - object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -4036,6 +4230,15 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + otplib@13.4.0: + dependencies: + "@otplib/core": 13.4.0 + "@otplib/hotp": 13.4.0 + "@otplib/plugin-base32-scure": 13.4.0 + "@otplib/plugin-crypto-noble": 13.4.0 + "@otplib/totp": 13.4.0 + "@otplib/uri": 13.4.0 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -4046,26 +4249,6 @@ snapshots: parseurl@1.3.3: {} - passport-github2@0.1.12: - dependencies: - passport-oauth2: 1.8.0 - - passport-oauth2@1.8.0: - dependencies: - base64url: 3.0.1 - oauth: 0.10.2 - passport-strategy: 1.0.0 - uid2: 0.0.4 - utils-merge: 1.0.1 - - passport-strategy@1.0.0: {} - - passport@0.7.0: - dependencies: - passport-strategy: 1.0.0 - pause: 0.0.1 - utils-merge: 1.0.1 - path-exists@4.0.0: {} path-is-absolute@1.0.1: {} @@ -4078,8 +4261,6 @@ snapshots: pathe@2.0.3: {} - pause@0.0.1: {} - perfect-debounce@1.0.0: {} picocolors@1.1.1: {} @@ -4341,6 +4522,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + thirty-two@1.0.2: {} + tinyexec@1.0.4: {} to-regex-range@5.0.1: @@ -4430,8 +4613,6 @@ snapshots: typescript@5.9.3: {} - uid2@0.0.4: {} - uid@2.0.2: dependencies: "@lukeed/csprng": 1.1.0 @@ -4448,8 +4629,6 @@ snapshots: util-deprecate@1.0.2: {} - utils-merge@1.0.1: {} - v8-compile-cache-lib@3.0.1: {} valibot@1.2.0(typescript@5.9.3):