/** * Makes usernames and emails unique regardless of case. Usernames were unique * only as typed, so "Bob" and "bob" could both sign up, and a mention, which * looks names up in lower case, then reached one of them at random. Emails * had no constraint at all: two sign-ups racing past the service's check could * share an address, and password resets are looked up by address. * * @module */ import type { DatabaseSync } from "node:sqlite"; /** * Run once by `runMigrations`. It refuses to run, naming the clashes, when two * accounts already share a name or an address in some case: which one keeps * it is for whoever runs the site to decide, by editing one of them. */ export function up(db: DatabaseSync): void { for (const column of ["username", "email"]) { const clashes = db.prepare( `SELECT lower(${column}) AS value FROM users GROUP BY lower(${column}) HAVING COUNT(*) > 1;`, ).all() as { value: string }[]; if (clashes.length > 0) { throw new Error( `Several accounts share a ${column} in different cases: ${ clashes.map((c) => c.value).join(", ") }. Change all but one of each, then restart.`, ); } } db.exec(` DROP INDEX IF EXISTS idx_users_username_lower; CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username_lower ON users(lower(username)); CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email_lower ON users(lower(email)); `); }