diff --git a/docker-compose.yml b/docker-compose.yml index 4bf14a5..6baaa59 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,7 +21,9 @@ services: start_period: 20s postgres: - image: pgvector/pgvector:pg16 + # Pinned by digest — changing the pgvector image can change the bundled + # ICU library and corrupt existing text indexes. REINDEX after any bump. + image: pgvector/pgvector:pg16@sha256:7d400e340efb42f4d8c9c12c6427adb253f726881a9985d2a471bf0eed824dff environment: POSTGRES_DB: pedscribe POSTGRES_USER: pedscribe diff --git a/src/db/database.js b/src/db/database.js index 2682baf..b17ac18 100644 --- a/src/db/database.js +++ b/src/db/database.js @@ -26,6 +26,30 @@ async function initDatabase() { console.warn('⚠️ pgvector extension not available. Vector search disabled. Install: apt-get install postgresql-16-pgvector'); } + // Collation-drift check — btree indexes on text become silently corrupt + // when the ICU / glibc library version bundled with the postgres image + // changes. pg_database tracks the collation version used when the DB + // was created/last refreshed; if it differs from what the library now + // reports, REINDEX and REFRESH to clear the flag. + try { + var dbDrift = await client.query( + "SELECT datcollversion AS recorded, " + + "pg_database_collation_actual_version(oid) AS actual " + + "FROM pg_database WHERE datname = current_database()" + ); + var row = dbDrift.rows[0] || {}; + if (row.recorded && row.actual && row.recorded !== row.actual) { + console.warn('⚠️ Collation drift: DB recorded ' + row.recorded + ', library reports ' + row.actual + '. REINDEX-ing to prevent silent index corruption...'); + await client.query('REINDEX DATABASE pedscribe'); + await client.query('ALTER DATABASE pedscribe REFRESH COLLATION VERSION').catch(function() {}); + console.log('✅ REINDEX complete. Collation version refreshed.'); + } else { + console.log('✅ Collation versions: aligned'); + } + } catch (e) { + console.warn('⚠️ Collation check skipped:', e.message); + } + await client.query(` CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, diff --git a/src/routes/auth.js b/src/routes/auth.js index 94054fa..fcb22e3 100644 --- a/src/routes/auth.js +++ b/src/routes/auth.js @@ -305,6 +305,9 @@ router.post('/login', async (req, res) => { // the same generic message for unknown-user, wrong-password, disabled, unverified. var DUMMY_HASH = '$2b$12$CwTycUXWue0Thq9StjUM0uJ8.aDLA18dY6nB5xuWz5M6l8lR6rYS.'; if (!user) { + // Server-side only — no email in the message, so operators can watch + // lookup-miss rates in Grafana without leaking which addresses exist. + console.warn('[Auth] login: user not found (ip=' + req.ip + ')'); await bcrypt.compare(password, DUMMY_HASH).catch(function(){}); return res.status(401).json({ error: 'Invalid credentials' }); }