3.1 KiB
3.1 KiB
F-154 — Technical Design
Context
identity_userstienerole: citext NOT NULL DEFAULT 'customer'(valores:customer,admin,editor,pos_cashier,pos_manager).customer= storefront; el resto = backoffice.GET /users(módulousers):PgProfileRepository.listCustomershaceSELECT ... FROM identity_users iu LEFT JOIN users_profiles up ... WHERE ($1::text IS NULL OR iu.email ILIKE $1). Devuelve TODO. Usado porclientsApi.list(página Customers) →/api/users.GET /admin/users(módulosecurity): query inline con condiciones opcionalesroleyq. Sin?role=devuelve TODO. Usado poradminUsersApi.list(página Users) →/api/admin/users.listCustomersse consume SOLO enusers.routes.ts(/users).findCustomerById(single,/users/:id) es role-agnostic (owner-or-admin) → no cambia.- No existe test de
users/securityroutes;users.itest.tsAC2/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):
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.GET /admin/users→ condición baserole <> 'customer'(literal).?role=admin|editorse andaña conAND role = $1. Así/admin/usersNUNCA devuelve customers, incluso con?role=customer(devuelve vacío). Parámetro base es literal → índices de$Nde los filtros opcionales inalterados.- 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/customersduplicaría y obligaría cambios frontend sin valor.
Boundary / Security
usersmódulo referenciaidentity_usersSOLO como nombre de tabla SQL (patrón ya usado ensearch); sin import TS users↔security.lint:boundariessin cambios nuevos.roleproviene 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 literalesrole = '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):listCustomersemiteiu.role = 'customer',qfiltra sobre email, COUNT y SELECT coinciden, returns solo filas customer.security.routes.test.ts(mock app+deps):/admin/usersdefault →role <> 'customer';?role=admin→role <> 'customer' AND role = $1; respuesta items internos.users.itest.tsAC2/AC3: actualizar aserción —/usersdevuelve customer (ben) no admin (ana).