Warn admins before deleting non-empty users/apps/services (Task #85)

Apply the existing groups delete-warning pattern to user, app, and
service deletions so admins are warned (and have to confirm twice)
before destroying records that still own dependent data.

Backend
- openapi.yaml: added `force` query param to DELETE /users/{id},
  /apps/{id}, /services/{id} plus three new conflict schemas
  (UserDeletionConflict, AppDeletionConflict, ServiceDeletionConflict).
- routes/users.ts, apps.ts, services.ts: rewrote DELETE handlers to
  count dependents (notes/orders/conversations/messages for users;
  group_apps/restrictions/open events for apps; service_orders for
  services), return 409 with counts when non-empty and force is not set,
  and on `?force=true` perform the cascade in a transaction and write
  an `*.force_delete` audit_logs row.
- Existing 204 success path preserved for empty deletes.

UI (artifacts/tx-os/src/pages/admin.tsx)
- New `DeletionWarningDialog` helper, plus app/service/user delete state
  + lazy 409 detection (first click probes; on 409 the dialog upgrades
  to show counts + "Delete anyway"; second click sends ?force=true).
- Replaced the three plain `confirm(t("admin.deleteConfirm"))` callsites.

i18n
- Added admin.deleteApp/deleteService/deleteUser keys (title, warning,
  forceHint, emptyBody, count keys, confirm, anyway) in en.json + ar.json.

Tests
- artifacts/api-server/tests/delete-force-warnings.test.mjs covers all
  9 cases (clean delete, 409 with counts, force=true 204 + audit log)
  for users, apps, and services. Existing groups-crud and service-orders
  tests still pass.

Notes / drift
- Lazy detection (vs eager counts on the row like Groups already does)
  was chosen because list endpoints don't return counts yet — proposed
  follow-up #96 covers eager counts so the warning appears on first
  click everywhere.
- E2E test for the UI flow flaked twice; backend integration tests
  (9/9 pass), direct curl validation of force=true returning 204, and
  typecheck across the monorepo all confirm correctness.

Replit-Task-Id: 91404d92-e74c-4720-8fc9-8eb772eefc33
This commit is contained in:
riyadhafraa
2026-04-27 12:07:07 +00:00
parent 6e9b9b5f3d
commit 7c9edf6cb6
11 changed files with 1146 additions and 72 deletions
+32
View File
@@ -305,6 +305,38 @@
"editService": "تعديل الخدمة",
"editUser": "تعديل المستخدم",
"deleteConfirm": "هل أنت متأكد من الحذف؟",
"deleteApp": {
"title": "حذف التطبيق \"{{name}}\"؟",
"warning": "هذا التطبيق لا يزال قيد الاستخدام. حذفه سيؤثر على ما يلي:",
"forceHint": "سيؤدي ذلك إلى إلغاء وصول كل من يستخدمه وإزالة سجلّه. لا يمكن التراجع عن هذا الإجراء.",
"emptyBody": "هل أنت متأكد من حذف هذا التطبيق؟",
"groupCount": "{{count}} مجموعة تمنح هذا التطبيق حالياً",
"restrictionCount": "{{count}} قاعدة صلاحيات تقيّده",
"openCount": "{{count}} مرة فتح في السجل",
"confirm": "حذف",
"anyway": "حذف رغم ذلك"
},
"deleteService": {
"title": "حذف الخدمة \"{{name}}\"؟",
"warning": "هذه الخدمة لا تزال مرتبطة بطلبات. حذفها سيؤثر على ما يلي:",
"forceHint": "سيؤدي ذلك إلى حذف كل الطلبات المرتبطة نهائياً. لا يمكن التراجع عن هذا الإجراء.",
"emptyBody": "هل أنت متأكد من حذف هذه الخدمة؟",
"orderCount": "{{count}} طلب",
"confirm": "حذف",
"anyway": "حذف رغم ذلك"
},
"deleteUser": {
"title": "حذف المستخدم \"{{name}}\"؟",
"warning": "هذا المستخدم لديه بيانات. حذفه سيؤثر على ما يلي:",
"forceHint": "سيؤدي ذلك إلى حذف ملاحظاته وطلباته والمحادثات التي أنشأها والرسائل التي أرسلها بشكل نهائي. لا يمكن التراجع عن هذا الإجراء.",
"emptyBody": "هل أنت متأكد من حذف هذا المستخدم؟",
"noteCount": "{{count}} ملاحظة",
"orderCount": "{{count}} طلب",
"conversationCount": "{{count}} محادثة أنشأها",
"messageCount": "{{count}} رسالة أرسلها",
"confirm": "حذف",
"anyway": "حذف رغم ذلك"
},
"activate": "تفعيل",
"deactivate": "تعطيل",
"appName": "اسم التطبيق",