feat(recycle-bin): soft delete + restore for master data
Accidental admin deletes of users/roles/subscriptions/plans/tariffs were hard and unrecoverable. Now they soft-delete into a recycle bin. Schema (migration 0012): nullable deleted_at + deleted_by on users, roles, subscriptions, subscription_plans, tariffs. Additive ADD COLUMN; verified against a copy of the live DB. Backend: each resource's DELETE route STAMPS instead of removing; every catalog list filters deleted_at IS NULL. New recycle-bin module + routes (GET /api/recycle-bin, POST .../restore, DELETE .../:id purge) gated on a new recyclebin:read/update/delete permission. A 6-hourly + startup sweep auto-purges items older than RECYCLE_BIN_RETENTION_DAYS (default 30; 0 = forever). Invariants: soft-deleted users can't log in (login rejects deleted_at; no-lockout counts live admins only); a soft-deleted subscription doesn't open the barrier; plans are versioned so a delete stamps all versions of the plan_id (bin shows one item); username/role-name UNIQUE spans deleted rows so reuse returns a clear 409 pointing at the bin; restore doesn't auto-cascade a dangling role (guard resolves missing role to empty perms). The signed append-only ledger is OUT of scope (no delete path). Web: a Recycle bin tab under Setup (RecycleBin.tsx) with Restore/Purge + purge confirm; api client + i18n (sq + en parity). Tests: recycle-bin.test.ts (9 unit) + recycle-bin-routes.test.ts (4 integration: delete -> can't-login -> restore -> login, purge, gating, 409 reuse). server 103/103; build+lint+test 19/19. Wiki: new concepts/soft-delete.md; local-jwt-auth + index + log updated. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -56,6 +56,7 @@ export const en: Catalog = {
|
||||
roles: "Roles",
|
||||
shifts: "Shifts",
|
||||
reports: "Reports",
|
||||
recycleBin: "Recycle bin",
|
||||
logs: "Logs",
|
||||
},
|
||||
status: {
|
||||
@@ -712,6 +713,24 @@ export const en: Catalog = {
|
||||
subCars: "Cars covered",
|
||||
},
|
||||
},
|
||||
recycleBin: {
|
||||
title: "Recycle bin",
|
||||
retentionNote: "Deleted items are kept for {{days}} days, then permanently removed.",
|
||||
empty: "Nothing deleted. Items you delete appear here, recoverable until they expire.",
|
||||
col: { type: "Type", item: "Item", deleted: "Deleted", actions: "" },
|
||||
kind: {
|
||||
user: "User",
|
||||
role: "Role",
|
||||
subscription: "Subscription",
|
||||
plan: "Plan",
|
||||
tariff: "Tariff",
|
||||
},
|
||||
restore: "Restore",
|
||||
purge: "Purge",
|
||||
purgeConfirmTitle: "Purge permanently?",
|
||||
purgeConfirmBody: "Permanently delete “{{label}}”? It cannot be restored after this.",
|
||||
purgeIrreversible: "This is irreversible.",
|
||||
},
|
||||
logs: {
|
||||
title: "System logs",
|
||||
refresh: "Refresh",
|
||||
|
||||
@@ -58,6 +58,7 @@ export const sq = {
|
||||
roles: "Rolet",
|
||||
shifts: "Turnet",
|
||||
reports: "Raportet",
|
||||
recycleBin: "Koshi",
|
||||
logs: "Loget",
|
||||
},
|
||||
status: {
|
||||
@@ -726,6 +727,24 @@ export const sq = {
|
||||
subCars: "Makina të mbuluara",
|
||||
},
|
||||
},
|
||||
recycleBin: {
|
||||
title: "Koshi",
|
||||
retentionNote: "Artikujt e fshirë mbahen për {{days}} ditë, pastaj hiqen përgjithmonë.",
|
||||
empty: "Asgjë e fshirë. Artikujt që fshini shfaqen këtu, të rikuperueshëm derisa të skadojnë.",
|
||||
col: { type: "Lloji", item: "Artikulli", deleted: "Fshirë", actions: "" },
|
||||
kind: {
|
||||
user: "Përdorues",
|
||||
role: "Rol",
|
||||
subscription: "Abonim",
|
||||
plan: "Plan",
|
||||
tariff: "Tarifë",
|
||||
},
|
||||
restore: "Rikthe",
|
||||
purge: "Fshi përfundimisht",
|
||||
purgeConfirmTitle: "Të fshihet përfundimisht?",
|
||||
purgeConfirmBody: "Të fshihet përgjithmonë “{{label}}”? Nuk mund të rikthehet pas kësaj.",
|
||||
purgeIrreversible: "Ky veprim është i pakthyeshëm.",
|
||||
},
|
||||
logs: {
|
||||
title: "Loget e sistemit",
|
||||
refresh: "Rifresko",
|
||||
|
||||
Reference in New Issue
Block a user