50 lines
3.1 KiB
Markdown
50 lines
3.1 KiB
Markdown
# F-154 — Technical Design
|
|
|
|
## Context
|
|
- `identity_users` tiene `role: citext NOT NULL DEFAULT 'customer'` (valores: `customer`,
|
|
`admin`, `editor`, `pos_cashier`, `pos_manager`). `customer` = storefront; el resto = backoffice.
|
|
- `GET /users` (módulo `users`): `PgProfileRepository.listCustomers` hace
|
|
`SELECT ... FROM identity_users iu LEFT JOIN users_profiles up ... WHERE ($1::text IS NULL OR iu.email ILIKE $1)`.
|
|
Devuelve TODO. Usado por `clientsApi.list` (página Customers) → `/api/users`.
|
|
- `GET /admin/users` (módulo `security`): query inline con condiciones opcionales `role` y `q`.
|
|
Sin `?role=` devuelve TODO. Usado por `adminUsersApi.list` (página Users) → `/api/admin/users`.
|
|
- `listCustomers` se consume SOLO en `users.routes.ts` (`/users`). `findCustomerById`
|
|
(single, `/users/:id`) es role-agnostic (owner-or-admin) → no cambia.
|
|
- No existe test de `users`/`security` routes; `users.itest.ts` AC2/AC3 asocia al admin (ana)
|
|
al listado `/users` (true hoy porque /users devuelve todo; romperá si /users es customer-only).
|
|
|
|
## Decision
|
|
Forzar la separación en el backend (no cliente):
|
|
1. `listCustomers` → siempre `... AND iu.role = 'customer'` (literal, no user input → sin inyección).
|
|
Parámetros inalterados: `[searchFilter, limit, offset]`; COUNT también filtra por rol.
|
|
2. `GET /admin/users` → condición base `role <> 'customer'` (literal). `?role=admin|editor`
|
|
se andaña con `AND role = $1`. Así `/admin/users` NUNCA devuelve customers, incluso con
|
|
`?role=customer` (devuelve vacío). Parámetro base es literal → índices de `$N` de los
|
|
filtros opcionales inalterados.
|
|
3. Frontend: dropdown de Users quita `<option value="customer">`.
|
|
|
|
## Alternatives
|
|
- Filtrado cliente-only: rechazado. El backend es la fuente única de verdad; el cliente no
|
|
debe poder ver customers vía `/admin/users`.
|
|
- Nuevo endpoint `/customers`: rechazado. El cliente ya consume `/users` (customers) y
|
|
`/admin/users` (internos); crear `/customers` duplicaría y obligaría cambios frontend
|
|
sin valor.
|
|
|
|
## Boundary / Security
|
|
- `users` módulo referencia `identity_users` SOLO como nombre de tabla SQL (patrón ya usado en
|
|
`search`); sin import TS users↔security. `lint:boundaries` sin cambios nuevos.
|
|
- `role` proviene de la DB (no user input directo en el filtro de roles; el literal `'customer'`/`'customer'`
|
|
está en código). En `/admin/users`, `?role=` validado por zod enum `['customer','editor','admin']`.
|
|
- Sin inyección: los valores user input (`q`, `role`) siguen parametrizados (`$N`); los literales
|
|
`role = 'customer'` / `role <> 'customer'` son constantes de código.
|
|
|
|
## Migration
|
|
Ninguna. `identity_users.role` ya existe (NOT NULL DEFAULT 'customer').
|
|
|
|
## Tests
|
|
- `pg-profile-repository.test.ts` (mock pool): `listCustomers` emite `iu.role = 'customer'`,
|
|
`q` filtra sobre email, COUNT y SELECT coinciden, returns solo filas customer.
|
|
- `security.routes.test.ts` (mock app+deps): `/admin/users` default → `role <> 'customer'`;
|
|
`?role=admin` → `role <> 'customer' AND role = $1`; respuesta items internos.
|
|
- `users.itest.ts` AC2/AC3: actualizar aserción — `/users` devuelve customer (ben) no admin (ana).
|