mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-15 02:33:15 -06:00
Self-hosters rebuilding could end up schema-behind-code, failing only at runtime
(a missing users.must_change_password locked out all logins). Two root causes:
1. The migration loop swallowed EVERY error (catch {}), so a real ALTER failure
was indistinguishable from the benign 'duplicate column' on an already-migrated
DB. Now only 'duplicate column'/'already exists' is treated as a no-op; any
other error is logged loudly, and a one-line summary reports how many new
column migrations actually applied this boot.
2. Nothing verified the schema after migrations. Added lib/schema-check.js:
verifyAndRepairSchema() checks the tables + columns the request path REQUIRES,
idempotently repairs missing repairable columns (logging each), and if anything
required is STILL missing, prints a loud FATAL block and exits - failing fast at
boot instead of at the first authed request.
Note: the reported 'audit_log missing' was a misdiagnosis - the code uses
activity_log (0 refs to audit_log), created by schema.sql on every boot.
Tests: healthy (no-op), auto-repair of must_change_password, missing-table report.
70 lines
3.2 KiB
JavaScript
70 lines
3.2 KiB
JavaScript
'use strict';
|
|
|
|
// #37: verify the DB has the schema the running code REQUIRES, after all
|
|
// migrations have run. On a partial/stale DB (e.g. a Docker rebuild that missed a
|
|
// migration) it repairs missing repairable columns idempotently and logs clearly;
|
|
// if anything required is STILL missing it calls onMissing (default: loud log +
|
|
// process.exit(1)) so the server fails fast at boot instead of limping along and
|
|
// breaking at the first authed request. The #37 lockout was a silently-absent
|
|
// users.must_change_password, which the auth middleware gates every request on.
|
|
|
|
// Tables the request path depends on (schema.sql creates them with CREATE TABLE
|
|
// IF NOT EXISTS on every boot; listed so their absence is still caught loudly).
|
|
const REQUIRED_TABLES = [
|
|
'users', 'organizations', 'organization_members', 'workspaces', 'workspace_members',
|
|
'devices', 'content', 'playlists', 'activity_log', 'schema_migrations',
|
|
];
|
|
|
|
// [table, column, repairSQL] — columns the code SELECTs / gates on. repairSQL is
|
|
// the idempotent ALTER that adds it if missing (null = base column, assert only).
|
|
const REQUIRED_COLUMNS = [
|
|
['users', 'must_change_password', "ALTER TABLE users ADD COLUMN must_change_password INTEGER NOT NULL DEFAULT 0"],
|
|
['users', 'role', null],
|
|
['users', 'plan_id', "ALTER TABLE users ADD COLUMN plan_id TEXT DEFAULT 'free'"],
|
|
];
|
|
|
|
function defaultOnMissing(missing) {
|
|
const bar = '='.repeat(72);
|
|
console.error(`\n${bar}`);
|
|
console.error('[schema-check] FATAL: database is missing required schema:');
|
|
for (const m of missing) console.error(` - ${m}`);
|
|
console.error('Migrations did not make the schema code-complete. The server is');
|
|
console.error('refusing to start to avoid silent runtime failures (e.g. issue #37,');
|
|
console.error('where a missing users.must_change_password failed every login).');
|
|
console.error('Fix: restore the newest db/remote_display.pre-*.db snapshot, or add');
|
|
console.error('the missing column/table manually, then restart.');
|
|
console.error(`${bar}\n`);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Returns the list of still-missing items (empty when healthy). Calls
|
|
// opts.onMissing(missing) when non-empty (default exits the process).
|
|
function verifyAndRepairSchema(db, opts = {}) {
|
|
const onMissing = opts.onMissing || defaultOnMissing;
|
|
const tableSet = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(r => r.name));
|
|
const columns = (t) => new Set(db.prepare(`PRAGMA table_info(${t})`).all().map(c => c.name));
|
|
|
|
const missing = [];
|
|
for (const t of REQUIRED_TABLES) if (!tableSet.has(t)) missing.push(`table "${t}"`);
|
|
|
|
for (const [t, c, repair] of REQUIRED_COLUMNS) {
|
|
if (!tableSet.has(t)) continue; // table-missing already recorded
|
|
if (columns(t).has(c)) continue;
|
|
if (repair) {
|
|
try {
|
|
console.warn(`[schema-check] required column ${t}.${c} is missing — applying repair...`);
|
|
db.exec(repair);
|
|
console.warn(`[schema-check] repaired ${t}.${c}`);
|
|
} catch (e) {
|
|
console.error(`[schema-check] repair of ${t}.${c} FAILED: ${e.message}`);
|
|
}
|
|
}
|
|
if (!columns(t).has(c)) missing.push(`column "${t}.${c}"`);
|
|
}
|
|
|
|
if (missing.length) onMissing(missing);
|
|
return missing;
|
|
}
|
|
|
|
module.exports = { verifyAndRepairSchema, REQUIRED_TABLES, REQUIRED_COLUMNS };
|