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;
|
const adminId = adminRows[0].id;
|
||||||
|
|
||||||
// Product configuration that must survive a clean operational start.
|
const structuralTables = [
|
||||||
const preservedTables = new Set([
|
|
||||||
'typeorm_migrations',
|
|
||||||
'users',
|
|
||||||
'user_roles',
|
|
||||||
'roles',
|
'roles',
|
||||||
'permissions',
|
'permissions',
|
||||||
'role_permissions',
|
'role_permissions',
|
||||||
@@ -41,6 +37,34 @@ export class ResetProductionOperationalData1788652800000 implements MigrationInt
|
|||||||
'finding_catalog_items',
|
'finding_catalog_items',
|
||||||
'finding_catalog_item_asset_types',
|
'finding_catalog_item_asset_types',
|
||||||
'finding_catalog_asset_type_profiles',
|
'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(`
|
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.
|
// A reset must invalidate every prior login token, including admin's.
|
||||||
// auth_sessions is operational and was truncated above; admin simply logs in again.
|
// auth_sessions is operational and was truncated above; admin simply logs in again.
|
||||||
await queryRunner.query(`
|
await queryRunner.query(
|
||||||
|
`
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET failed_login_attempts = 0,
|
SET failed_login_attempts = 0,
|
||||||
locked_until = NULL,
|
locked_until = NULL,
|
||||||
last_login_at = NULL,
|
last_login_at = NULL,
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`, [adminId]);
|
`,
|
||||||
|
[adminId],
|
||||||
|
);
|
||||||
|
|
||||||
const finalUsers: Array<{ total: string; admins: string }> = await queryRunner.query(`
|
const finalUsers: Array<{ total: string; admins: string }> = await queryRunner.query(`
|
||||||
SELECT
|
SELECT
|
||||||
@@ -88,6 +115,37 @@ export class ResetProductionOperationalData1788652800000 implements MigrationInt
|
|||||||
throw new Error('Production reset verification failed: users table is not admin-only');
|
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
|
// Assert that every operational table is empty. This makes the migration
|
||||||
// fail atomically if a table was repopulated during the reset transaction.
|
// fail atomically if a table was repopulated during the reset transaction.
|
||||||
for (const table of operationalTables) {
|
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.
|
// Keep a concise server-side record in the migration log for deploy diagnostics.
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(
|
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