From 93a63cd42bfc746aa3a5fdaa6c13dd933b25ba1f Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Fri, 17 Apr 2026 10:14:54 -0400 Subject: [PATCH] fix(register): reject duplicate usernames before passkey prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Friend hit a 500 on /api/register/complete because the Postgres unique constraint fired after the WebAuthn ceremony — they'd already burned a passkey creation by the time the server refused. Pre-check the username in /api/register/start so the join page shows "Username is already taken" before the browser prompts. Also catch the 23505 duplicate-key error in /api/register/complete as a race-condition safety net. --- src/encryptid/server.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/encryptid/server.ts b/src/encryptid/server.ts index d6602413..69b75915 100644 --- a/src/encryptid/server.ts +++ b/src/encryptid/server.ts @@ -569,6 +569,13 @@ app.post('/api/register/start', async (c) => { return c.json({ error: 'Username required' }, 400); } + // Reject duplicates up front so the user isn't prompted to create a passkey + // that the /complete step would then reject with a unique-constraint crash. + const existing = await getUserByUsername(username); + if (existing) { + return c.json({ error: `Username "${username}" is already taken — pick a different one.` }, 409); + } + // Generate challenge const challenge = Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('base64url'); @@ -658,7 +665,14 @@ app.post('/api/register/complete', async (c) => { const did = (clientDid && typeof clientDid === 'string' && clientDid.startsWith('did:key:z')) ? clientDid : `did:key:${userId.slice(0, 32)}`; - await createUser(userId, username, username, did); + try { + await createUser(userId, username, username, did); + } catch (err: any) { + if (err?.code === '23505' || /unique constraint/i.test(err?.message || '')) { + return c.json({ error: `Username "${username}" is already taken — pick a different one.` }, 409); + } + throw err; + } // Set recovery email if provided during registration if (email && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {