diff --git a/apps/web/src/app/(dashboard)/settings/security/passkeys/create-passkey-dialog.tsx b/apps/web/src/app/(dashboard)/settings/security/passkeys/create-passkey-dialog.tsx index 0262f199c..c07d638c0 100644 --- a/apps/web/src/app/(dashboard)/settings/security/passkeys/create-passkey-dialog.tsx +++ b/apps/web/src/app/(dashboard)/settings/security/passkeys/create-passkey-dialog.tsx @@ -11,6 +11,7 @@ import { match } from 'ts-pattern'; import { UAParser } from 'ua-parser-js'; import { z } from 'zod'; +import { MAXIMUM_PASSKEYS } from '@documenso/lib/constants/auth'; import { AppError } from '@documenso/lib/errors/app-error'; import { trpc } from '@documenso/trpc/react'; import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; @@ -179,7 +180,7 @@ export const CreatePasskeyDialog = ({ trigger, ...props }: CreatePasskeyDialogPr If you do not want to use the authenticator prompted, you can close it, which will - then display the next avaliable authenticator. + then display the next available authenticator. @@ -189,6 +190,11 @@ export const CreatePasskeyDialog = ({ trigger, ...props }: CreatePasskeyDialogPr .with('ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED', () => ( This passkey has already been registered. )) + .with('TOO_MANY_PASSKEYS', () => ( + + You cannot have more than {MAXIMUM_PASSKEYS} passkeys. + + )) .with('InvalidStateError', () => ( <> diff --git a/packages/lib/constants/auth.ts b/packages/lib/constants/auth.ts index 1587c1780..137ebe640 100644 --- a/packages/lib/constants/auth.ts +++ b/packages/lib/constants/auth.ts @@ -32,3 +32,8 @@ export const USER_SECURITY_AUDIT_LOG_MAP: { [key in UserSecurityAuditLogType]: s * The duration to wait for a passkey to be verified in MS. */ export const PASSKEY_TIMEOUT = 60000; + +/** + * The maximum number of passkeys are user can have. + */ +export const MAXIMUM_PASSKEYS = 50; diff --git a/packages/lib/server-only/auth/create-passkey.ts b/packages/lib/server-only/auth/create-passkey.ts index 98f3287b9..c493d8205 100644 --- a/packages/lib/server-only/auth/create-passkey.ts +++ b/packages/lib/server-only/auth/create-passkey.ts @@ -4,6 +4,7 @@ import type { RegistrationResponseJSON } from '@simplewebauthn/types'; import { prisma } from '@documenso/prisma'; import { UserSecurityAuditLogType } from '@documenso/prisma/client'; +import { MAXIMUM_PASSKEYS } from '../../constants/auth'; import { AppError, AppErrorCode } from '../../errors/app-error'; import type { RequestMetadata } from '../../universal/extract-request-metadata'; import { getAuthenticatorRegistrationOptions } from '../../utils/authenticator'; @@ -21,12 +22,23 @@ export const createPasskey = async ({ verificationResponse, requestMetadata, }: CreatePasskeyOptions) => { - await prisma.user.findFirstOrThrow({ + const { _count } = await prisma.user.findFirstOrThrow({ where: { id: userId, }, + include: { + _count: { + select: { + passkeys: true, + }, + }, + }, }); + if (_count.passkeys >= MAXIMUM_PASSKEYS) { + throw new AppError('TOO_MANY_PASSKEYS'); + } + const verificationToken = await prisma.verificationToken.findFirst({ where: { userId, diff --git a/packages/trpc/server/auth-router/router.ts b/packages/trpc/server/auth-router/router.ts index 7ee86b17b..86d0e3491 100644 --- a/packages/trpc/server/auth-router/router.ts +++ b/packages/trpc/server/auth-router/router.ts @@ -110,10 +110,7 @@ export const authRouter = router({ } catch (err) { console.error(err); - throw new TRPCError({ - code: 'BAD_REQUEST', - message: 'We were unable to create this passkey. Please try again later.', - }); + throw AppError.parseErrorToTRPCError(err); } }),