fix: enforce unique usernames (#1628)

* fix: enforce unique usernames

Registration checked for a taken name, hashed the password, then
inserted, with nothing in the schema enforcing uniqueness. Concurrent
sign-ups could create two accounts with one name, after which login,
password reset, proxy login and admin lookups by username picked
between them arbitrarily.

Add a unique index on users.username for every dialect. Existing
duplicates are renamed (later rows get a -<id> suffix, the oldest keeps
the name) rather than deleted. Registration and admin user creation now
return the same 409 when the insert loses the race.

* fix: avoid username collisions during migration
This commit is contained in:
ZacharyZcR authored and GitHub committed 2026-10-14 08:17:47 +08:00
1 parent 3941dd4ecc
commit e610aba0e5
19 files changed
+14354 -26

No files matched your search

@@ -17,9 +17,10 @@ import {
listUserSecondFactors,
resetUserSecondFactors,
} from "../../auth/second-factor-admin.js";
import type {
UserRecord,
UserRepository,
import {
isUsernameTaken,
type UserRecord,
type UserRepository,
} from "../repositories/user-repository.js";
function isNonEmptyString(val: unknown): val is string {
@@ -541,6 +542,9 @@ export function registerUserAdminRoutes(
toast: { type: "success", message: `User created: ${username}` },
});
} catch (err) {
if (isUsernameTaken(err)) {
return res.status(409).json({ error: "Username already exists" });
}
authLogger.error("Failed to admin-create user", err);
res.status(500).json({ error: "Failed to create user" });
}
+8 -1
View File
@@ -41,7 +41,10 @@ import {
createCurrentUserAuthRepository,
createCurrentUserRepository,
} from "../repositories/factory.js";
import type { UserRecord } from "../repositories/user-repository.js";
import {
isUsernameTaken,
type UserRecord,
} from "../repositories/user-repository.js";
import {
getTrustedProxyAuthConfig,
getTrustedProxySourceAddress,
@@ -265,6 +268,10 @@ router.post("/create", async (req, res) => {
toast: { type: "success", message: `User created: ${username}` },
});
} catch (err) {
// Lost a race with a concurrent sign-up for the same name.
if (isUsernameTaken(err)) {
return res.status(409).json({ error: "Username already exists" });
}
authLogger.error("Failed to create user", err);
res.status(500).json({ error: "Failed to create user" });
}