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);
}
}),