feat(F-154): completed feature
This commit is contained in:
54
work/artifacts/F-154/architect.md
Normal file
54
work/artifacts/F-154/architect.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user