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:
@@ -37,9 +37,15 @@ Authentication and authorization, kept **fully local** — a direct consequence
|
||||
**last user holding admin** — administration can never be locked out of the appliance.
|
||||
- `event:void` is a permission, NOT a ledger delete: the append-only signed chain is untouched; the
|
||||
permission only gates who may APPEND a void event (there is no void API route yet — forward seam).
|
||||
- The grid is **extensible** — adding a feature adds its `resource:action` rows. Latest: **`log:read`**
|
||||
(a new `log` resource) gates the diagnostic-log viewer (`GET /api/logs`); admin holds it, and it's
|
||||
grantable to a diagnostic role. See [[app-logs]].
|
||||
- The grid is **extensible** — adding a feature adds its `resource:action` rows. Recent additions:
|
||||
**`log:read`** (gates the diagnostic-log viewer, `GET /api/logs`; see [[app-logs]]); **`report:read`**
|
||||
(the admin Reports dashboard; see [[reporting-analytics]]); and **`recyclebin:read/update/delete`**
|
||||
(view / restore / purge soft-deleted master data; see [[soft-delete]]). Admin holds them all; each is
|
||||
grantable to a scoped role.
|
||||
- **Soft-deleted users can't authenticate.** The login route rejects a user whose `deleted_at` is set
|
||||
(with the same generic "invalid credentials" so a deleted account isn't enumerable). The no-lockout
|
||||
"last admin" check counts only LIVE admins, so soft-deleting can't strand administration. See
|
||||
[[soft-delete]].
|
||||
- **No privilege escalation through the RBAC system itself.** `role:create`/`role:update` and
|
||||
`user:create`/`user:update` are themselves grantable, so a non-admin could otherwise self-escalate.
|
||||
Guards (`routes/roles.ts`, `routes/users.ts`): a caller may only put permissions on a role that
|
||||
|
||||
Reference in New Issue
Block a user