Files
mercadodevida/work/artifacts/F-154/architect.md
2026-08-22 08:35:46 +02:00

3.2 KiB

F-154 — Architect Evidence

Feature

F-154 — Admin: separate customers from internal users (fix, high/high).

Problem

Two admin list endpoints both returned all identity_users, so the Customers and Users pages showed storefront customers and backoffice staff mixed together:

  • GET /users (module users, PgProfileRepository.listCustomers) → all users, no role filter. Consumed by the Customers page (customersApi.list/api/users).
  • GET /admin/users (module security) → all users unless ?role= supplied. Consumed by the Users page (adminUsersApi.list/api/admin/users).

Roles (DB identity_users.role, citext NOT NULL DEFAULT 'customer')

  • customer → storefront client.
  • admin / editor / pos_cashier / pos_manager → internal / backoffice staff.

Decision

Force the separation in the backend (single source of truth), not client-side:

  1. listCustomers (→ GET /users) now always appends AND iu.role = 'customer' (code literal, no user input → no injection). The ?q search still applies on email. Single-user findCustomerById (/users/:id) is untouched (owner-or-admin, role-agnostic).
  2. GET /admin/users: base condition role <> 'customer' (literal) so the Users list NEVER returns storefront customers; ?role=admin|editor still narrows within internal staff. ?role=customer resolves to an empty intersection (still no leak).
  3. Frontend: remove the customer option from the Users page role dropdown (UX polish — backend already enforces the boundary). Customers page unchanged (already calls /users).

Alternatives considered

  • Client-side filtering only: rejected — backend is the trust boundary; the admin API must not leak customers through /admin/users.
  • New /customers endpoint: rejected — the frontend already binds Customers↔/users and Users↔/admin/users; splitting now duplicates effort with no behavioral gain.

Boundary / security

  • users module references identity_users only as a SQL table name (existing pattern in search); no TypeScript import crosses the identity↔users↔security boundary. lint:boundaries unaffected.
  • No migration (identity_users.role already exists, NOT NULL DEFAULT 'customer').
  • User input (q, role) stays parameterized ($N); the role constants are code literals.

Test plan (no DB required → runs in npm test)

  • users/infrastructure/pg-profile-repository.test.ts: mock pg.Pool, assert listCustomers emits iu.role = 'customer', respects q, count === select filter, returns only customer rows.
  • security/api/security.routes.test.ts: register routes on a mock Fastify with a mock pool + mocked authenticate returning admin; assert GET /admin/users queries role <> 'customer' and that ?role=admin adds AND role = $1; response excludes customer rows.
  • app/tests/users.itest.ts AC2/AC3: update assertion — /users returns the customer (ben), not the promoted admin (ana).

Risk

Medium-high: changes admin list semantics (/users now customer-only). Mitigations: /users/:id (single) unchanged; only the LIST contract changes; existing itest updated; no migration; boundary/lint verified.