Error statuses follow one rule: APIException takes a status and derives the code from it (BAD_REQUEST 400, UNAUTHORIZED 401, FORBIDDEN 403, NOT_FOUND 404, CONFLICT 409, RATE_LIMITED 429, SERVER_ERROR 500, BAD_GATEWAY 502), so a code can't contradict its status. That fixed real bugs: a wrong current password on change-password answered 401, on which authFetch signs the user out; editing or deleting someone else's comment or chat message answered 401 rather than 403; one invalid dump edit answered 422. A taken username is 409, like a taken email. Along the way: the database is created from schema.sql by connection.ts when the file doesn't exist, so every task works on a fresh checkout (dev and serve never ran db/init.ts, which is gone); the startup purge compared ISO dates with SQLite's datetime() as text and kept anything from the current UTC day, and now compares ISO cutoffs; the seeded admin's hash comes from lib/password.ts (split out of jwt.ts, which needs the JWT secret) rather than a copy of its format; avatar, thumbnail and playlist-cover uploads go through replaceUploadedFile, which writes beside the old file and renames over it before the record announces it (a failed avatar write lost the old avatar; a thumbnail was announced before it existed); upload.ts derives its size message from MAX_IMAGE_SIZE_BYTES and stops re-exporting the upload folders; ALLOWED_IMAGE_MIMES, which duplicated detectImageMime, is gone; the SPA fallback no longer calls next() after answering; a reply must answer a comment on the same dump (a foreign or unknown parent was a 500 or a cross-dump thread). 211 backend and 628 frontend tests pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
37 lines
1.4 KiB
TypeScript
37 lines
1.4 KiB
TypeScript
/**
|
|
* 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));
|
|
`);
|
|
}
|