# 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.