ops: blindar reset preservando configuración estructural
This commit is contained in:
@@ -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`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user