Files
mercadodevida/spec/tech.md
2026-08-22 08:35:46 +02:00

3.1 KiB

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=adminrole <> 'customer' AND role = $1; respuesta items internos.
  • users.itest.ts AC2/AC3: actualizar aserción — /users devuelve customer (ben) no admin (ana).