Commit Graph

13 Commits

Author SHA1 Message Date
riyadhafraa c0cf2115dd Task #96: Show dependency counts in admin app/service/user lists
Goal: make the admin delete dialog show its dependency warning on
the FIRST click (matching the existing Groups UX), instead of
needing a 409 round-trip from the DELETE endpoint to populate the
warning.

Changes:
- lib/api-spec/openapi.yaml: added optional count fields to App
  (groupCount, restrictionCount, openCount), Service (orderCount),
  and UserProfile (noteCount, orderCount, conversationCount,
  messageCount).
- artifacts/api-server/src/routes/apps.ts: GET /admin/apps now
  batch-loads groupCount/restrictionCount/openCount via grouped
  COUNT queries on group_apps, app_permissions, and app_opens.
- artifacts/api-server/src/routes/services.ts: GET /services now
  batch-loads orderCount from service_orders.
- artifacts/api-server/src/routes/users.ts: GET /users now batch-
  loads noteCount/orderCount/conversationCount/messageCount from
  notes, service_orders, conversations.created_by, and
  messages.sender_id.
- artifacts/tx-os/src/pages/admin.tsx: Apps, Services, and Users
  delete buttons pre-populate their conflict-state from the row's
  count fields so the DeletionWarningDialog shows the warning on
  the first click. The lazy 409 fallback still works as a safety
  net for any caller without counts.
- replit.md: documented the new admin-panel delete UX.

Tests:
- Added artifacts/api-server/tests/list-dependency-counts.test.mjs
  with 6 tests verifying each list endpoint exposes the new count
  fields with the expected non-zero values when dependents exist
  AND zero values when they do not (apps, services, users).
- Existing artifacts/api-server/tests/delete-force-warnings.test.mjs
  (9 tests) still passes — DELETE behavior unchanged.
- Verified end-to-end with a Playwright browser test: clicking the
  service delete icon ONCE opens the confirmation modal with the
  dependency warning ("orders", count >= 1) immediately.

Notes / drift:
- Count fields were added to the shared App/Service/UserProfile
  schemas (not list-only sub-schemas) so the same shape is returned
  from list and detail endpoints. Code review flagged this as
  acceptable but broader than strictly necessary; left as is to
  keep the API consistent.

Out of scope (already pre-existing on main, tracked as follow-ups):
- artifacts/api-server/tests/apps-open.test.mjs has a syntax error.
- artifacts/api-server/tests/app-permissions-unique.test.mjs has
  two failing assertions about the composite primary key.
- artifacts/api-server/src/routes/executive-meetings.ts has 3 pre-
  existing typecheck errors around the font-settings scope column.
- The `test` workflow exits with ECONNREFUSED when the api-server
  isn't already up — needs a readiness check.

Replit-Task-Id: bd14fe73-9961-431b-ab5a-ab70f116e8c7
2026-04-29 08:48:29 +00:00
riyadhafraa 675a4a418a Record more sensitive admin actions in the audit log (Task #93)
Expanded audit_logs coverage from "force deletions only" to a wider set
of sensitive admin actions. The Audit Log view's action filter dropdown
picks them up automatically (it queries distinct actions from the table).

New audit actions written:
- user.delete (every delete; metadata includes username, email, force,
  and dependent counts when applicable). Replaces user.force_delete for
  new entries; old rows remain.
- role.create / role.update / role.delete (in artifacts/api-server/src/
  routes/roles.ts). role.update only fires when fields actually change
  and records previousName + changes.
- role.permissions.replace / role.permission.add / role.permission.remove
  for the PUT /roles/:id/permissions, POST /roles/:id/permissions, and
  DELETE /roles/:id/permissions/:permissionId endpoints. Replace records
  added[] and removed[] diffs; add/remove records permissionId and
  permissionName so the affected permission stays identifiable even if
  later renamed/deleted. No-op writes (same permission re-added, replace
  with identical set) skip the audit.
- group.create / group.update / group.delete (group.delete replaces
  group.force_delete for new entries; metadata always includes force +
  counts). group.update records changed fields and added/removed lists
  for members, apps, and roles when those collections were touched.
- group.user.add / group.user.remove / group.app.add / group.app.remove
  / group.role.add / group.role.remove for the sub-resource
  POST/DELETE /groups/:id/:kind/:targetId endpoints (only when something
  actually changed, via .returning() guard).
- auth.issue_reset_link in routes/auth.ts admin issue-reset-link.
- app.create / app.update / app.delete (app.delete replaces
  app.force_delete for new entries with force flag in metadata).
  app.update only fires when something actually changed and records a
  per-field {from, to} diff.
- settings.update with per-field {from, to} diff (covers
  registrationOpen and any other UpdateAppSettingsBody field).

Out of scope / deviations:
- "App-permission changes" is in the task description, but no admin
  endpoints exist yet to write app_permissions. A separate task ("Let
  admins manage which permission an app requires from the UI") will add
  them. Filed follow-up #113 to add the audit hooks once those endpoints
  exist. Also filed #114 (readable summaries for new actions) and #115
  (automated tests for the expanded coverage).
- The pre-existing standalone task "Record an audit trail when a role's
  permissions change" was folded into this task at code-review request,
  since the same task description called out role permissions. That
  separate task is now obsolete.

Verified end-to-end via curl against the running API: every new action
appears in /api/admin/audit-logs and in the actions[] dropdown list,
including all three role.permission* actions.

Replit-Task-Id: f078e4fd-afdf-4c71-b29c-c0cae026af1b
2026-04-28 20:47:24 +00:00
riyadhafraa 7c9edf6cb6 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
2026-04-27 12:07:07 +00:00
riyadhafraa 595ca06a57 Task #83: Refresh role and permission changes live across user sessions
## Socket.IO broadcasts (core task):
- POST /users/:id/roles: uses .returning() to emit apps_changed only when row inserted
- DELETE /users/:id/roles/:roleName: uses .returning() to emit only when row deleted
- Added emitAppsChangedToRoleHolders(roleId) helper to realtime.ts
- New role-permission endpoints in roles.ts, emit only on effective changes:
  - GET /roles/:id/permissions — list permissions for a role
  - POST /roles/:id/permissions — assign; emits to role holders on actual insert
  - DELETE /roles/:id/permissions/:permId — remove; emits only if row was deleted

## Bug fix: admin panel not showing disabled apps
- Added GET /api/admin/apps (requireAdmin) returning ALL apps from DB
- Updated admin.tsx: useQuery → /api/admin/apps with ADMIN_APPS_KEY constant
- All admin.tsx mutation handlers invalidate ADMIN_APPS_KEY

## UI fix: disabled app cards nearly invisible (opacity-60 on glass)
- Gray background + grayscale icon + muted text + stronger Disabled badge

Replit-Task-Id: f439ec75-bcd5-4030-8ee1-83a5d976f1a1

## DB: removed 11 duplicate rows from app_permissions table
2026-04-27 11:11:43 +00:00
riyadhafraa 5051b1b905 fix: tell receivers when an order is no longer available + fix disabled apps disappearing from admin
## Task #80 — Tell receivers when an order is no longer available to claim

### Problem
When a receiver tried to act on an order that had already been cancelled, completed,
or claimed by someone else, the UI showed a generic "Could not complete the action"
toast. The stale card also stayed in the list, requiring a manual refresh.

### Changes

**artifacts/api-server/src/routes/service-orders.ts**
- PATCH /orders/:id/confirm-receipt: after a failed atomic claim, query the current
  order status; return 409 `order_unavailable` if it's cancelled/completed (vs the
  existing 409 `already_claimed` for when it was grabbed by someone else first)
- PATCH /orders/:id/status (preparing/completed branch): terminal-state check runs
  BEFORE permission gating — returns 409 `order_unavailable` when order is already
  cancelled/completed instead of ever hitting 403 Forbidden
- PATCH /orders/:id/status (cancel branch): same — terminal-state check moved to
  the top of the branch so non-admin receivers trying to cancel an already-terminal
  order get 409 `order_unavailable` (not 403 Forbidden which would never let the UI
  remove the stale card)

**artifacts/tx-os/src/pages/orders-incoming.tsx**
- handleConfirmReceipt onError: distinguish 409 `order_unavailable` from
  `already_claimed`, show specific toast, remove stale card via invalidate()
- handleSetStatus onError: same pattern — specific toast + invalidate() on
  `order_unavailable`
- handleCancel onError: same pattern

**artifacts/tx-os/src/locales/en.json + ar.json**
- Added `incomingOrders.orderUnavailable` ("This order is no longer available" /
  "هذا الطلب لم يعد متاحاً")

## Bonus fix — Disabled app disappears from admin panel (user-reported)

### Problem
When an admin disabled an app via the toggle, the app disappeared from the admin
panel too (isActive filter applied to everyone), making it impossible to re-enable
without direct DB access.

### Changes

**artifacts/api-server/src/routes/apps.ts**
- getVisibleAppsForUser: admins now receive ALL apps including inactive ones via
  conditional $dynamic() Drizzle query; non-admins still only see active apps

**artifacts/tx-os/src/pages/home.tsx**
- Filter orderedApps to only active apps before rendering so inactive apps don't
  appear on the home screen even for admins

**artifacts/tx-os/src/pages/admin.tsx**
- Inactive app cards show reduced opacity + a "Disabled/معطّل" badge

**artifacts/tx-os/src/locales/en.json + ar.json**
- Added `admin.appDisabled` ("Disabled" / "معطّل")

Replit-Task-Id: 8d6fade8-e76c-4d3c-864b-59007688c12c
2026-04-27 09:06:23 +00:00
riyadhafraa 72e1f8d9ed Groups CRUD: address validator round 2 + 3 blockers
Round 2 fixes:
- apps.ts getVisibleAppsForUser uses getEffectiveRoleIds() so admin
  detection honors group_roles inheritance (closes group-only-admin
  bypass). Exported helper from middlewares/auth.ts.
- POST /users accepts optional groupIds[] via new CreateUserBody
  schema. Validates ids exist; user create + auto-Everyone + requested
  groups happen in one transaction. OpenAPI extended; api-zod and
  api-client-react regenerated.
- DELETE /users/:id 400s on self-delete.
- scripts/src/seed.ts: removed `void inArray;` slop and unused import.
- Locales (ar+en): added admin.users.col.{displayNameAr,displayNameEn,
  language} and admin.groups.searchPlaceholder.

Round 3 fixes:
- Admin Users "Add User" dialog gets a groupIds checkbox multi-select
  populated from useListGroups; createUser sends groupIds when set.
- PATCH /users/:id validates ALL groupIds before deleting memberships
  (returns 400 on any invalid id); replacement wrapped in transaction
  to avoid partial-loss windows.
- Apps admin SQL bug: `${arr} = ANY(...)` produced bad params; now uses
  `and(inArray(rolesTable.id, ids), eq(name,'admin'))`.
- New tests (artifacts/api-server/tests/apps-group-visibility.test.mjs):
  group-derived admin sees all; group member sees group-granted +
  unrestricted; outsider sees only unrestricted. All 3 pass.

Typecheck clean; full api test suite green except pre-existing
admin-app-opens-pagination flake (unrelated). Architect: PASS.
2026-04-22 08:54:08 +00:00
riyadhafraa 4595e792e4 Fix 5 validator blockers in groups/users admin work
1. apps.ts getVisibleAppsForUser: use getEffectiveRoleIds() so admin
   gate honors group_roles inheritance (was direct user_roles only —
   security bypass for group-only admins). Exported helper from
   middlewares/auth.ts.

2. POST /users: switch from RegisterBody to new CreateUserBody schema
   that accepts optional groupIds[]. Validates ids exist; user creation
   + auto-Everyone + requested groups happen in single transaction.
   OpenAPI spec extended; api-zod and api-client-react regenerated.

3. DELETE /users/🆔 400 if req.session.userId === target (no
   self-delete).

4. scripts/src/seed.ts: removed `void inArray;` AI-slop and dropped
   unused inArray import.

5. Locales (ar.json + en.json): added admin.users.col.displayNameAr,
   displayNameEn, language and admin.groups.searchPlaceholder.

Typecheck clean. groups-crud + service-orders + users tests pass.
Pre-existing admin-app-opens-pagination flake unrelated.
Architect re-review: PASS.
2026-04-22 08:47:35 +00:00
riyadhafraa f5273af19f Task #74: Add groups system + admin User Management UI
Backend:
- New schema: groups, user_groups, group_apps, group_roles (lib/db/src/schema/groups.ts)
- Seeds Admins, TeaBoy, Everyone system groups idempotently and maps existing users
- /api/groups CRUD with admin guard, batch counts, system-group delete protection
- Validates appIds/roleIds/userIds (400 on missing) and wraps assignment writes in
  a single DB transaction (no partial state on failure)
- /api/users gains q/groupId/status filters, batch role+group loading, groupIds
  replacement on PATCH, auto-assigns Everyone on admin-create
- /auth/register also auto-assigns Everyone for consistent default linkage
- buildAuthUser now returns groups (matches updated AuthUser OpenAPI schema)
- App visibility (getVisibleAppsForUser) unions group-granted apps via
  group_apps + user_groups in addition to existing permission gating

Frontend (admin.tsx):
- Nav restructured: User Management section with Users + Groups children
- Section deep-linked via #section=… URL hash
- Users page rebuilt: search, group filter, status filter, sortable table,
  groups column, edit-groups dialog, mobile cards
- New Groups page: cards with member/app/role counts, create dialog,
  detail editor with Info/Apps/Users tabs and system-group guard
- ar/en translations added for all new keys

Testing:
- pnpm typecheck clean (api + web)
- 25/26 api tests pass; the only failure is pre-existing flaky pagination test
  (admin-app-opens-pagination) — left as-is per scratchpad note
- Code review feedback addressed (validation, transactions, register auto-assign)
2026-04-22 08:28:31 +00:00
riyadhafraa a5484283f4 Add app opens & services activity charts to admin dashboard
- New `app_opens` table (id, user_id, app_id, created_at) and Drizzle
  schema; exported from lib/db schema index.
- New `POST /api/apps/{id}/open` endpoint logs an open for the current
  user (auth required, 204 on success, 404 for unknown app id).
- Extended `GET /api/stats/admin` with appOpensByDay/appOpensLast7Days/
  appOpensPrev7Days and servicesCreatedByDay/servicesCreatedLast7Days,
  refactored around a shared buildSeries helper.
- Regenerated api-client/zod and added two new bar charts (App opens,
  Services added) to the admin Dashboard alongside the existing
  Sign-ups chart, with EN/AR translations.
- Home page fires bare `logAppOpen(id, { keepalive: true })` before
  navigating so the request survives client-side route changes; the
  bottom dock buttons also use this helper.
- Restructured SortableAppIcon so the click target and dnd-kit
  listeners live on the same <button>, fixing a click-vs-drag
  interaction that prevented the open event from firing in tests.

Rebase notes:
- home.tsx: incoming main reworked AppIcon styling, the apps grid
  header (with count and empty state), and the dock button. Kept all
  incoming visual changes while preserving this task's
  AppIconContent fragment, single-button SortableAppIcon, and
  openApp() wiring (so dock + grid both log opens).
- opengraph.jpg: kept incoming binary version.

E2E verified: clicking app icons increments app_opens; admin dashboard
renders all three charts.

Replit-Task-Id: 776c14f7-4e5a-4bf6-80e2-a6c7586c1fcb
2026-04-20 15:16:19 +00:00
riyadhafraa 3df6eb7c54 Task #16: Per-user drag-and-drop reorder for Home apps
- New user_app_orders composite-PK table (user_id, app_id) -> sort_order
- Added GET helper getVisibleAppsForUser that LEFT JOINs user order
  and sorts by COALESCE(user_sort, app.sort_order, name)
- New PUT /api/me/app-order endpoint validates payload, filters to
  visible apps, dedupes, replaces row set in a transaction
- Frontend: wrapped home apps grid in @dnd-kit DndContext + SortableContext
  with PointerSensor (distance:8) and TouchSensor (delay:250 / tolerance:5)
  so taps still navigate while a press-and-drag reorders
- Optimistic local state with rollback on error; useEffect skips sync
  while a save mutation is in flight to avoid stomping on user changes
- Bottom dock intentionally NOT sortable (per user choice)
- DB schema pushed manually via SQL (drizzle-kit push prompted for
  rename ambiguity); regenerated api-zod / api-client-react
- Verified end-to-end: PUT /api/me/app-order returns reordered list
  and subsequent GET /api/apps reflects the new per-user order
2026-04-20 12:12:06 +00:00
riyadhafraa abcc32fb66 Add ability to reorder apps on the home screen
Implements drag-and-drop functionality for reordering apps using @dnd-kit, adds a new `userAppOrdersTable` to the database schema to store user-specific app order preferences, and introduces a new API endpoint `/api/me/app-order` for updating these preferences.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: e782e35b-00f5-4b9b-8931-63051a25df80
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/PrQkd7G
Replit-Helium-Checkpoint-Created: true
2026-04-20 12:09:14 +00:00
riyadhafraa 5debea9e61 fix: address all code review rejections — security, RBAC, i18n, types
Security:
- Fix CORS origin validation to use exact match (includes) instead of
  startsWith, preventing domain-prefix bypass attacks with credentials: true

RBAC — App Visibility:
- /api/apps now filters apps per user's permission set via app_permissions
  and role_permissions tables: admin users see all active apps; regular users
  see apps that either have no permission restriction or have a matching
  permission assigned through their roles

Admin User Create:
- Add POST /api/users (requireAdmin) endpoint to create users via the admin
  dashboard; uses bcryptjs hashing, checks for duplicate username, returns
  safe profile payload

i18n — Remove Hardcoded Strings:
- Add "common.appName" key to en.json ("TeaBoy OS") and ar.json ("نظام TeaBoy")
- Replace hardcoded "TeaBoy OS" literal in login.tsx and register.tsx
  with t("common.appName") call

Code Cleanliness:
- Remove all // @replit scaffold comments from badge.tsx and button.tsx
  (UI component files that had Replit-specific style commentary)

Admin Redirect Fix:
- Move non-admin redirect from render body to useEffect to comply with
  React rules of hooks; add early null return after all hooks for clean
  access control

Socket.IO Security:
- Server-side auth: session middleware applied via io.engine.use
- io.use middleware rejects unauthenticated socket connections
- Auto-join user room from session; verify membership before conv room join
- Remove client-sent "join" userId event (server derives userId from session)

Other Fixes:
- Add GET /api/users/directory (auth-only) for chat non-admin user lookup
- Wire language toggle in home.tsx to persist via PUT /api/auth/language
- Fix admin.tsx nullable field coercions with ?? defaults
- All TypeScript checks pass; full e2e regression passes
2026-04-20 09:42:37 +00:00
riyadhafraa 001e9023b2 feat: build TeaBoy OS — bilingual Arabic/English internal OS platform
Completed full-stack TeaBoy OS build:

Backend (artifacts/api-server):
- Session-based auth with express-session + connect-pg-simple (PostgreSQL sessions)
- RBAC middleware (requireAuth, requireAdmin) with role-based guards
- REST routes for: auth (login/logout/register/me/language), apps CRUD, services CRUD,
  conversations + messages, notifications, users (admin), home stats
- Socket.IO mounted on same HTTP server at /api/socket.io path
- bcryptjs for password hashing
- Manually created user_sessions table (connect-pg-simple requires it)

Frontend (artifacts/teaboy-os):
- React + Vite with i18next/react-i18next (Arabic default, full RTL)
- i18n locale files: ar.json + en.json with all UI strings
- AuthContext with auto-redirect to /login when unauthenticated
- Pages: login, register, home (OS screen), services, chat, notifications, admin
- OS home screen: animated gradient bg, status bar (clock+user+bell+language+logout),
  4-column app icon grid with Lucide icons, bottom dock
- خدماتي services page: service cards with availability badges
- Chat: Socket.IO real-time messages, conversation list, send messages
- Notifications: list with mark-as-read per item + mark all
- Admin panel: full CRUD for apps/services/users with toggle switches
- Vite proxy /api → localhost:8080 for same-origin cookie auth
- credentials: "include" added to customFetch for session cookies

Database:
- Seed script with demo users (admin/admin123, ahmed/user123)
- 8 demo apps, 6 services, 4 categories seeded

All e2e tests pass: login, home screen, services, language toggle, admin, logout
2026-04-20 09:20:50 +00:00