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

@@ -1,6 +1,9 @@
import { afterEach, describe, expect, it } from "vitest";
import { TestSqliteDatabase } from "./test-support.js";
import { UserRepository } from "../../../database/repositories/user-repository.js";
import {
UserRepository,
isUsernameTaken,
} from "../../../database/repositories/user-repository.js";
describe("UserRepository.listPage", () => {
let adapter: TestSqliteDatabase | null = null;
@@ -93,3 +96,27 @@ describe("UserRepository.listPage", () => {
expect(page.total).toBe(5);
});
});
describe("UserRepository.create", () => {
let adapter: TestSqliteDatabase | null = null;
afterEach(async () => {
if (adapter) {
await adapter.close();
adapter = null;
}
});
it("rejects a taken username in a way callers can recognise", async () => {
adapter = new TestSqliteDatabase();
const repo = new UserRepository(await adapter.connect());
await repo.create({ id: "u1", username: "alice", passwordHash: "hash" });
const error = await repo
.create({ id: "u2", username: "alice", passwordHash: "hash" })
.catch((e: unknown) => e);
expect(isUsernameTaken(error)).toBe(true);
expect(isUsernameTaken(new Error("unrelated"))).toBe(false);
});
});