mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 05:56:53 +00:00
bb5cc8f795
Login, password reset and the sign-up checks all match emails case-insensitively, but the only constraint was the column's own UNIQUE, which SQLite compares case-sensitively. Two accounts whose addresses differ only in case (legacy rows, two sign-ups at once) left a login to resolve to whichever row came first, and LOWER(email) lookups had no index to use. A migration adds a unique index on lower(email). An install that already holds such a pair must still start, so the migration then leaves the index out, logs how many addresses are shared and by which user ids (not the addresses), and records email_case_index_pending. Since a migration runs once, db/email-case-index.ts runs on every boot while that flag is set: it repeats the warning until the accounts are told apart, then creates the index and clears the flag. The app-level checks keep guarding meanwhile. A write that passes the up-front check and still loses the race is now refused by the database. The users repository turns that unique violation into UserIdentityTakenError, and sign-up, admin create and edit, and the profile update answer it with the same 409 their checks give, instead of a 500 or an unhandled rejection (ADMIN-SVC-094 pinned the old behaviour and now pins the 409). The entity generator could not see an expression index (PRAGMA index_info names its column NULL), so PARITY-009c found a db index with no entity declaration. Rule 14 carries such an index into the entity as a named entry with its own statement; Users.entity.ts is regenerated.
39 lines
1.9 KiB
TypeScript
39 lines
1.9 KiB
TypeScript
import type { Connection } from '@mikro-orm/core';
|
|
|
|
/**
|
|
* The case-insensitive unique index on users.email, when its migration had to
|
|
* leave it out.
|
|
*
|
|
* `Migration20200101042400_one_account_per_email_whatever_its_case` creates
|
|
* `idx_users_email_lower` unless two accounts already share an address in
|
|
* different case; then it records `email_case_index_pending` and the install
|
|
* boots without it. A migration runs once, so this runs on every boot while
|
|
* that flag is set: once the accounts were told apart it creates the index and
|
|
* clears the flag, until then it repeats the warning.
|
|
*/
|
|
|
|
const PENDING_KEY = 'email_case_index_pending';
|
|
|
|
export async function ensureEmailCaseIndex(connection: Connection): Promise<void> {
|
|
const pending: unknown[] = await connection.execute(`SELECT value FROM app_settings WHERE key = ?`, [PENDING_KEY]);
|
|
if (pending.length === 0) return;
|
|
|
|
const duplicates: Array<{ ids: string }> = await connection.execute(
|
|
`SELECT group_concat(id, ', ') AS ids FROM (SELECT id, email FROM users ORDER BY id) GROUP BY lower(email) HAVING COUNT(*) > 1`,
|
|
);
|
|
if (duplicates.length > 0) {
|
|
console.warn(
|
|
`[DB] ${duplicates.length} email address(es) still belong to more than one account when case is ignored ` +
|
|
`(user ids ${duplicates.map((row) => row.ids).join('; ')}), so users.email has no case-insensitive unique ` +
|
|
'index yet. Give each of those accounts its own address under Admin > Users; the next start creates it.',
|
|
);
|
|
return;
|
|
}
|
|
|
|
await connection.transactional(async (trx) => {
|
|
await connection.execute('CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email_lower ON users (lower(email))', [], 'run', trx);
|
|
await connection.execute('DELETE FROM app_settings WHERE key = ?', [PENDING_KEY], 'run', trx);
|
|
});
|
|
console.log('[DB] Every account has its own email address now; created the case-insensitive unique index on users.email');
|
|
}
|