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