ops: preparar reset limpio de datos operativos

This commit is contained in:
2026-09-05 22:12:18 -03:00
parent 1b57dcc850
commit 343c889250
@@ -0,0 +1,115 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* One-time production reset requested before the first clean Android rollout.
*
* Keeps only structural product configuration plus the single `admin` account.
* Operational/business data is removed. Recovery is intentionally performed
* from the deploy PRE backup, not through a synthetic down migration.
*/
export class ResetProductionOperationalData1788652800000 implements MigrationInterface {
name = 'ResetProductionOperationalData1788652800000';
public async up(queryRunner: QueryRunner): Promise<void> {
const adminRows: Array<{ id: string; username: string }> = await queryRunner.query(`
SELECT id, username
FROM users
WHERE lower(trim(username)) = 'admin'
ORDER BY id
`);
if (adminRows.length !== 1) {
throw new Error(
`Production reset aborted: expected exactly one username admin, found ${adminRows.length}`,
);
}
const adminId = adminRows[0].id;
// Product configuration that must survive a clean operational start.
const preservedTables = new Set([
'typeorm_migrations',
'users',
'user_roles',
'roles',
'permissions',
'role_permissions',
'asset_types',
'asset_attribute_definitions',
'asset_type_parent_rules',
'finding_categories',
'finding_catalog_items',
'finding_catalog_item_asset_types',
'finding_catalog_asset_type_profiles',
]);
const tableRows: Array<{ table_name: string }> = await queryRunner.query(`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
ORDER BY table_name
`);
const operationalTables = tableRows
.map((row) => row.table_name)
.filter((table) => !preservedTables.has(table));
if (operationalTables.length > 0) {
const quoted = operationalTables
.map((table) => `"${table.replace(/"/g, '""')}"`)
.join(', ');
await queryRunner.query(`TRUNCATE TABLE ${quoted} RESTART IDENTITY CASCADE`);
}
// Remove every user except the explicitly validated administrator.
// user_roles for removed users follow their FK cascade.
await queryRunner.query(`DELETE FROM users WHERE id <> $1`, [adminId]);
// 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(`
UPDATE users
SET failed_login_attempts = 0,
locked_until = NULL,
last_login_at = NULL,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`, [adminId]);
const finalUsers: Array<{ total: string; admins: string }> = await queryRunner.query(`
SELECT
count(*)::text AS total,
count(*) FILTER (WHERE lower(trim(username)) = 'admin')::text AS admins
FROM users
`);
if (finalUsers[0]?.total !== '1' || finalUsers[0]?.admins !== '1') {
throw new Error('Production reset verification failed: users table is not admin-only');
}
// 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) {
const safeTable = `"${table.replace(/"/g, '""')}"`;
const rows: Array<{ total: string }> = await queryRunner.query(
`SELECT count(*)::text AS total FROM ${safeTable}`,
);
if (rows[0]?.total !== '0') {
throw new Error(`Production reset verification failed: ${table} is not empty`);
}
}
// 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`,
);
}
public async down(): Promise<void> {
throw new Error(
'ResetProductionOperationalData is irreversible by migration; restore the deploy PRE database backup instead.',
);
}
}