6.7 KiB
6.7 KiB
Implementation Plan: Job 918 — Admin Area System Database Backup/Restore and Danger Zone 1.01
1. Architectural Reconnaissance
- Codebase style & conventions: Node.js monorepo using strict TypeScript, NestJS controllers/services on the backend, Angular standalone components and signals on the frontend, async/await, dependency injection, Zod request/archive validation, and stable
ApiErrorcodes. The requested UI and API flow already exists; the defect is isolated to the backend restore rebuild.RestoreService.cloneAndOverwriteDatabase()clones the live SQLite file and executesDELETEstatements while the live schema'strg_user_last_admin_deletetrigger remains active.PRAGMA foreign_keys = OFFdoes not disable triggers, so deleting the sole live admin raisesLAST_ADMINbefore archived users can be inserted. - Data Layer: SQLite through TypeORM's
better-sqlite3driver. Runtime migrations create a database-level invariant withtrg_user_last_admin_updateandtrg_user_last_admin_delete. Backups dynamically include application tables but excludeadmin_operation_token,migrations, and SQLite internals. Restore uses a cloned database, direct parameterizedbetter-sqlite3statements, and a database/uploads filesystem swap with rollback handles. - Test Framework & Structure: Jest 29 with
ts-jest; backend tests live under/repo/tests/backendand are selected by/repo/tests/jest.config.js. Rootnpm testruns backend and frontend projects, whilenpm run test:backendruns the focused backend suite. Tests must remain CLI-only and use isolated temporary SQLite/upload/staging paths rather than shared mutable resources. - Required Tools & Dependencies: No new package, system tool, global CLI, or persistent
/dataasset is required. Existing Node.js/npm, TypeScript, Jest, Nest testing utilities, TypeORM, andbetter-sqlite3are sufficient.setup.shalready installs dependencies and rebuilds the native SQLite binding, so it should not be changed.
2. Impacted Files
- To Modify:
backend/src/modules/admin/system/restore.service.ts— make the offline clone rebuild temporarily suspend the last-admin triggers, restore all archived rows, validate invariants, and reinstate the triggers before the file is eligible for swapping.tests/backend/admin-system-restore-commit.spec.ts— add a focused real-file restore regression covering a backup that contains the same sole admin as the live database, restored table content, uploads, and trigger preservation; retain the existing controller/DTO contract coverage.
- To Create: None.
3. Proposed Changes
- Database / Schema Migration:
- Do not add or alter a migration: the
LAST_ADMINtriggers are correct for ordinary runtime user mutations. - In the private staged database clone only, read the exact SQL definitions of
trg_user_last_admin_updateandtrg_user_last_admin_deletefromsqlite_master, fail closed if the expected trigger definitions cannot be captured, and drop those triggers before clearing application tables. - Keep foreign keys disabled during the bulk replacement, clear all known application tables, and insert archived rows in the existing dependency-safe order with parameterized values.
- Before closing the staged clone, verify the archive produced at least one admin user so a restore cannot bypass the deployment-wide invariant. Recreate both captured triggers exactly, re-enable foreign keys, run
PRAGMA foreign_key_check, and treat any violations or trigger-restoration failure as a restore failure. This confines the temporary invariant suspension to an offline candidate file and ensures the candidate has full protections before swap.
- Do not add or alter a migration: the
- Backend Logic & APIs:
- Preserve the existing
POST /api/v1/admin/system/restore/validate, confirmation-token, andPOST /restore/commitcontracts;stagingIdalready survives Zod validation and reachesRestoreService.commitRestore(). - Refactor the staged rebuild cleanup so database closure and pragma/trigger restoration are deterministic and original failures are not masked by cleanup. Any rebuild, integrity, swap, or verification error must continue through the existing rollback path and return
SYSTEM_RESTORE_ROLLED_BACKwithout changing live data/uploads. - Keep the existing filesystem transaction: build and validate the candidate first, copy staged uploads beside the live upload root, swap database and uploads together, verify the swapped database, then commit rollback artifacts.
- Keep post-success session revocation in
AdminSystemController.commitRestore(). After the restored database is live, revoke refresh sessions for the authenticated admin ID; the frontend's already-wiredforceServerInvalidation()then clears the access session and redirects all tabs to login.
- Preserve the existing
- Frontend UI Integration:
- No frontend changes are planned. The file picker, staging summary, reauthentication modal, confirmation phrase, restore commit request, rollback alert, data-cache invalidation, and forced-login behavior are already wired in
frontend/src/app/features/admin/system/system.component.ts,system.service.ts, and the auth/system data-change services.
- No frontend changes are planned. The file picker, staging summary, reauthentication modal, confirmation phrase, restore commit request, rollback alert, data-cache invalidation, and forced-login behavior are already wired in
4. Test Strategy
- Target Unit Test File:
tests/backend/admin-system-restore-commit.spec.ts. - Mocking Strategy: Keep existing controller dependencies mocked for request forwarding/token/session-revocation assertions. Add one minimal service-level integration regression using a unique temporary directory, a real file-backed
better-sqlite3database, realRestoreService, and realFilesystemTransactionService; mock onlyConfigServiceand the liveDataSource.queryverification boundary as needed. Arrange a live database with the migrated last-admin triggers, one sole admin, pre-restore challenge/blog/setting rows, and live uploads; stage a valid archive containing that same admin plus different table values and replacement challenge/icon files; commit; then open the swapped file independently and assert archived counts/settings, exact upload replacement, and both last-admin triggers still exist and reject deleting the sole restored admin. Add one focused invalid-archive invariant case with no admin and assertSYSTEM_RESTORE_ROLLED_BACKplus unchanged live database/uploads. Clean all temporary files inafterEach/afterAll; no UI, network, visual checks, shared/data, or large mock environment. - Execution: Follow red-green-refactor: first run the focused backend spec to confirm the
LAST_ADMINfailure, implement the smallest restore-service correction, rerun the focused spec, then runnpm run test:backend, rootnpm test,npm --workspace backend run build, and the repository lint/typecheck commands if present (the current package scripts expose build but no dedicated lint/typecheck command).