diff --git a/server/auth.ts b/server/auth.ts index 2cc5afa..09e90c4 100644 --- a/server/auth.ts +++ b/server/auth.ts @@ -306,14 +306,11 @@ authRoutes.patch('/profile', requireAuth, async (c) => { detail: 'lowercase a-z, 0-9, _ or -; 2-31 characters; must start with a letter or digit', }, 400); } - if (next !== null) { - // Pre-check uniqueness so we can return a clear 409 instead of the - // SQLite UNIQUE-constraint error bubbling up as a 500. - const taken = db.prepare( - 'SELECT 1 FROM users WHERE username = ? AND id <> ?', - ).get(next, userId); - if (taken) return c.json({ error: 'username_taken' }, 409); - } + // Uniqueness is enforced by the UNIQUE constraint on users.username + // (column-level for fresh DBs, partial unique index for migrated DBs). + // We rely on it directly rather than pre-checking — a pre-check would + // be racy (two concurrent PATCHes could both pass it). The constraint + // catch below converts SQLITE_CONSTRAINT_UNIQUE into a clean 409. updates.push('username = ?'); params.push(next); } @@ -333,7 +330,18 @@ authRoutes.patch('/profile', requireAuth, async (c) => { } params.push(userId); - db.prepare(`UPDATE users SET ${updates.join(', ')} WHERE id = ?`).run(...params); + try { + db.prepare(`UPDATE users SET ${updates.join(', ')} WHERE id = ?`).run(...params); + } catch (err) { + // SQLite throws an SqliteError with a message containing "UNIQUE + // constraint failed: users.username" (column-level constraint) or + // ".users_username_idx" (the partial unique index used by migrated DBs). + // Either way the user-facing meaning is the same — slug taken. + if (err instanceof Error && /UNIQUE constraint failed.*username/i.test(err.message)) { + return c.json({ error: 'username_taken' }, 409); + } + throw err; + } const me = loadMe(userId); if (!me) return c.json({ error: 'internal_error' }, 500);