From 343c889250147c01c7ec2faacce999696ecb68fa Mon Sep 17 00:00:00 2001 From: enlineawork Date: Sat, 5 Sep 2026 22:12:18 -0300 Subject: [PATCH] ops: preparar reset limpio de datos operativos --- ...00000-reset-production-operational-data.ts | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 api-v3/src/database/migrations/1788652800000-reset-production-operational-data.ts diff --git a/api-v3/src/database/migrations/1788652800000-reset-production-operational-data.ts b/api-v3/src/database/migrations/1788652800000-reset-production-operational-data.ts new file mode 100644 index 0000000..a57c6a2 --- /dev/null +++ b/api-v3/src/database/migrations/1788652800000-reset-production-operational-data.ts @@ -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 { + 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 { + throw new Error( + 'ResetProductionOperationalData is irreversible by migration; restore the deploy PRE database backup instead.', + ); + } +}