ops: blindar reset preservando configuración estructural

This commit is contained in:
2026-09-05 22:17:45 -03:00
parent 06f809b4c8
commit 66333fb5dd
@@ -26,11 +26,7 @@ export class ResetProductionOperationalData1788652800000 implements MigrationInt
const adminId = adminRows[0].id;
// Product configuration that must survive a clean operational start.
const preservedTables = new Set([
'typeorm_migrations',
'users',
'user_roles',
const structuralTables = [
'roles',
'permissions',
'role_permissions',
@@ -41,6 +37,34 @@ export class ResetProductionOperationalData1788652800000 implements MigrationInt
'finding_catalog_items',
'finding_catalog_item_asset_types',
'finding_catalog_asset_type_profiles',
];
// Snapshot structural row counts so TRUNCATE ... CASCADE can never silently
// remove product configuration while still leaving operational tables empty.
const structuralCounts = new Map<string, string>();
for (const table of structuralTables) {
const safeTable = `"${table.replace(/"/g, '""')}"`;
const rows: Array<{ total: string }> = await queryRunner.query(
`SELECT count(*)::text AS total FROM ${safeTable}`,
);
structuralCounts.set(table, rows[0]?.total ?? '0');
}
const adminRolesBefore: Array<{ total: string }> = await queryRunner.query(
`SELECT count(*)::text AS total FROM user_roles WHERE user_id = $1`,
[adminId],
);
const adminRoleCount = adminRolesBefore[0]?.total ?? '0';
if (adminRoleCount === '0') {
throw new Error('Production reset aborted: admin has no assigned role');
}
// Product configuration that must survive a clean operational start.
const preservedTables = new Set([
'typeorm_migrations',
'users',
'user_roles',
...structuralTables,
]);
const tableRows: Array<{ table_name: string }> = await queryRunner.query(`
@@ -68,14 +92,17 @@ export class ResetProductionOperationalData1788652800000 implements MigrationInt
// A reset must invalidate every prior login token, including admin's.
// auth_sessions is operational and was truncated above; admin simply logs in again.
await queryRunner.query(`
await queryRunner.query(
`
UPDATE users
SET failed_login_attempts = 0,
locked_until = NULL,
last_login_at = NULL,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`, [adminId]);
`,
[adminId],
);
const finalUsers: Array<{ total: string; admins: string }> = await queryRunner.query(`
SELECT
@@ -88,6 +115,37 @@ export class ResetProductionOperationalData1788652800000 implements MigrationInt
throw new Error('Production reset verification failed: users table is not admin-only');
}
const finalAdminRoles: Array<{ total: string; foreign_users: string }> = await queryRunner.query(
`
SELECT
count(*) FILTER (WHERE user_id = $1)::text AS total,
count(*) FILTER (WHERE user_id <> $1)::text AS foreign_users
FROM user_roles
`,
[adminId],
);
if (
finalAdminRoles[0]?.total !== adminRoleCount ||
finalAdminRoles[0]?.foreign_users !== '0'
) {
throw new Error('Production reset verification failed: admin role assignments changed');
}
// Assert every structural table kept exactly the same number of rows.
for (const table of structuralTables) {
const safeTable = `"${table.replace(/"/g, '""')}"`;
const rows: Array<{ total: string }> = await queryRunner.query(
`SELECT count(*)::text AS total FROM ${safeTable}`,
);
const before = structuralCounts.get(table) ?? '0';
if (rows[0]?.total !== before) {
throw new Error(
`Production reset verification failed: structural table ${table} changed (${before} -> ${rows[0]?.total ?? 'unknown'})`,
);
}
}
// Assert that every operational table is empty. This makes the migration
// fail atomically if a table was repopulated during the reset transaction.
for (const table of operationalTables) {
@@ -103,7 +161,7 @@ export class ResetProductionOperationalData1788652800000 implements MigrationInt
// Keep a concise server-side record in the migration log for deploy diagnostics.
// eslint-disable-next-line no-console
console.log(
`[production-reset] kept admin=${adminRows[0].username} (${adminId}); cleared ${operationalTables.length} operational tables`,
`[production-reset] kept admin=${adminRows[0].username} (${adminId}); preserved ${structuralTables.length} structural tables; cleared ${operationalTables.length} operational tables`,
);
}