Plan: Milestone 1 — Acceso y Administración¶
Source PRD: docs/prds/qaudit.prd.md
Blueprint: docs/rfcs/RFC-0001-qaudit-arquitectura.md
Selected Milestone: 1 — Acceso y administración
Complexity: Large
Outcome (PRD): Un auditor inicia sesión con su cuenta corporativa y solo entra si está habilitado; el admin gestiona tiendas y usuarios.
Summary¶
Primer milestone de un proyecto greenfield. Entrega el outcome de acceso + administración y establece toda la base técnica que los milestones 2–7 reutilizan: scaffolding Next.js + TypeScript estricto, Prisma + Postgres, Docker Compose, arnés de pruebas (Vitest + Playwright, cobertura ≥95% per RFC §11), SSO con Microsoft Entra ID con whitelist y roles, y dos CRUDs de administración (tiendas y usuarios). La app vive en la raíz del repo (no en subcarpeta). No se toca QUICK POS ni el flujo de auditoría todavía.
Naturaleza dual del milestone¶
| Eje | Qué incluye |
|---|---|
| Fundación (se hace una vez, fija convenciones) | Scaffolding, tooling, Prisma singleton, capa lib/domain vacía pero estructurada, Docker Compose, arnés de tests |
| Feature (el outcome de M1) | Auth SSO + whitelist + roles, guards de ruta, CRUD de stores, CRUD de users |
Patterns to Mirror¶
No existe código previo — proyecto greenfield. Este milestone crea los patrones. Fuentes de convención a respetar (no inventar otras):
| Categoría | Fuente | Patrón a establecer |
|---|---|---|
| Capas | RFC §4.2 | src/app/ (UI) · src/app/api/ (borde HTTP, valida y delega) · src/lib/domain/ (puro, sin I/O) · src/lib/db.ts (Prisma singleton) |
| Estilo / naming | reglas web globales (coding-style) | Componentes PascalCase, hooks use*, organización por feature, archivos <800 líneas |
| Tests | reglas globales (testing) + RFC §11 | AAA, nombres descriptivos, TDD (RED→GREEN→REFACTOR), cobertura ≥95% |
| Errores | RFC §9 + global | Validación con schema (Zod) en el borde; nunca confiar en input; authz revalidada server-side |
| Datos | RFC §5 | Prisma como fuente de verdad; constraints en la BD; migración incremental por milestone |
Files to Change¶
Convención de paquete: pnpm (alineado con las reglas de hooks web). La app vive en la raíz del repo (per RFC §4.2):
src/app/es el App Router,prisma/y la config quedan arriba. La carpeta de documentación (docs/) convive en la raíz sin interferir con el build de Next.
| File | Action | Why |
|---|---|---|
package.json, tsconfig.json, next.config.ts |
CREATE | Scaffolding Next.js App Router + TS estricto |
eslint.config.mjs, .prettierrc, .stylelintrc |
CREATE | Tooling de calidad (zero-warning policy) |
vitest.config.ts, vitest.setup.ts |
CREATE | Unit/integration + cobertura ≥95% |
playwright.config.ts |
CREATE | E2E |
prisma/schema.prisma |
CREATE | Datasource Postgres + modelos stores y users (solo M1) |
prisma/migrations/** |
CREATE | Primera migración |
prisma/seed.ts |
CREATE | Bootstrap del primer admin (Diana) desde env |
src/lib/db.ts |
CREATE | Prisma client singleton |
src/lib/domain/authorization.ts |
CREATE | Decisión pura de autorización (whitelist + rol), testeable sin I/O |
src/lib/validation/store.ts, src/lib/validation/user.ts |
CREATE | Schemas Zod (dominio email, rango puerto, formato codsuc) |
auth.ts |
CREATE | Auth.js: provider Entra ID single-tenant, callbacks signIn/jwt/session |
src/middleware.ts |
CREATE | Guard de rutas a nivel edge (autenticado / rol) |
src/app/layout.tsx, src/app/page.tsx |
CREATE | Shell + redirect por rol |
src/app/api/health/route.ts |
CREATE | Endpoint de health para el healthcheck de Docker (Task 10) |
src/app/(auth)/login/page.tsx, .../denied/page.tsx |
CREATE | Login y pantalla de "no habilitado" |
src/app/(admin)/layout.tsx |
CREATE | Guard server-side rol=admin |
src/app/(admin)/stores/page.tsx + componentes |
CREATE | UI CRUD de tiendas |
src/app/(admin)/users/page.tsx + componentes |
CREATE | UI CRUD de whitelist |
src/app/api/stores/route.ts, .../[id]/route.ts |
CREATE | API tiendas (admin-only) |
src/app/api/users/route.ts, .../[id]/route.ts |
CREATE | API usuarios (admin-only) |
Dockerfile, docker-compose.yml, docker-compose.prod.yml, .env.example, .gitignore |
CREATE | Postgres de dev (Task 1) + despliegue on-premise (RFC §10) |
docs/rfcs/RFC-0001-qaudit-arquitectura.md |
UPDATE | ✅ Hecho: §5 users.ms_oid → nullable (se vincula en primer login) |
Tasks¶
Orden TDD: por cada tarea con lógica, el test va primero (RED), luego implementación (GREEN), luego refactor.
Task 1: Scaffolding, tooling y Postgres de desarrollo¶
- Action: Inicializar Next.js (App Router, TS estricto) en la raíz del repo; configurar ESLint/Prettier/stylelint, Vitest + RTL, Playwright; scripts pnpm (
lint,typecheck,test,test:e2e,build). Levantar undocker-compose.ymlcon el serviciopostgres:16-alpine(volumen + healthcheck) para que la Task 2 tenga contra qué migrar. ElDockerfilede la app y los overrides de prod se completan en la Task 10. - Mirror: reglas globales web (coding-style, testing).
- Validate:
pnpm install && pnpm typecheck && pnpm lint && pnpm test --run(suite vacía pasa, cero warnings);docker compose up -d postgresdeja la BD healthy.
Task 2: Base de datos y Prisma¶
- Action:
prisma/schema.prismacon datasource Postgres y modelosstoresyuserscon los constraints del RFC §5 — exceptoms_oidque vanullable @unique. Generar primera migración. Implementarsrc/lib/db.tssingleton. - Mirror: RFC §5 (constraints en BD); migración incremental (solo tablas de M1).
- Validate:
pnpm prisma migrate devaplica limpio contra el Postgres levantado en la Task 1;pnpm prisma validate.
Task 3: Lógica de autorización (pura) — TDD¶
- Action:
lib/domain/authorization.ts: función pura que dado un usuario (o ausencia) decideallow/denyy rol, y la lógica de vinculación dems_oiden primer login (match por email normalizado, case-insensitive). Tests primero. - Mirror:
lib/domainsin I/O; AAA. - Validate: casos — email habilitado+activo → allow; inactivo → deny; inexistente → deny; primer login vincula oid; logins siguientes matchean por oid.
Task 4: Schemas de validación — TDD¶
- Action: Zod para store (
puerto1–65535,codsuc2 dígitos,codempint, hostname no vacío) y user (email LIKE %@zavidoro.com.py,rol ∈ {admin,auditor}). Tests primero. - Validate: rechaza email de otro dominio, puerto fuera de rango, codsuc inválido; acepta válidos.
Task 5: Auth.js + Entra ID + whitelist¶
- Action:
auth.tscon provider Microsoft Entra ID single-tenant. CallbacksignInconsultausersvíalib/domain/authorization→ si no habilitado, deniega y redirige a/denied. Callbackjwt/sessionexponerolyuserId. Primer login vinculams_oid. - Mirror: RFC §6; secretos solo en env, validados al arranque.
- Validate: integración con DB de test + provider mockeado: usuario habilitado entra con rol correcto; no habilitado → denegado.
Task 6: Guards de ruta¶
- Action:
middleware.ts(autenticado) +(admin)/layout.tsx(revalidarol=adminserver-side). Cada Route Handler revalida authz, no confía en la UI. - Mirror: RFC §6 ("nunca confiar solo en el guard de UI").
- Validate: E2E — auditor no alcanza
(admin); no autenticado → login.
Task 7: API + UI de Tiendas (CRUD) — TDD¶
- Action:
/api/stores(list/create/update/deactivate), admin-only, valida con Zod. UI lista + formulario. Baja lógica (activa=false), no borrado físico. - Validate: integración handlers contra DB de test (201/200/403/422); E2E admin crea/edita/desactiva tienda.
Task 8: API + UI de Usuarios/whitelist (CRUD) — TDD¶
- Action:
/api/users(list/create/update/deactivate), admin-only, valida dominio de email. UI lista + formulario (email, nombre, rol, activo).ms_oidno editable (lo llena el login). - Validate: integración (rechaza dominio ajeno con 422; admin-only 403); E2E alta de auditor habilitado.
Task 9: Seed de bootstrap del primer admin¶
- Action:
prisma/seed.tsque inserta el admin inicial (email desdeBOOTSTRAP_ADMIN_EMAIL) idempotente. - Validate: correr seed dos veces no duplica; el admin sembrado puede entrar y ver el panel.
Task 10: Dockerfile de la app y despliegue completo¶
- Action:
Dockerfile(build Next.js), agregar el servicioappaldocker-compose.ymlya existente (condepends_onpostgres healthy y healthcheck contra/api/health),.env.example,docker-compose.prod.ymlpara overrides de producción. El.gitignoreya se creó al inicializar git. - Mirror: RFC §10.
- Validate:
docker compose up --buildlevanta app + postgres; healthcheck verde vía/api/health; la app responde y conecta a la BD.
Validation¶
# desde la raíz del repo
pnpm install
docker compose up -d postgres # BD de dev healthy (Task 1)
pnpm prisma validate && pnpm prisma migrate dev
pnpm typecheck # tsc --noEmit, cero errores
pnpm lint # ESLint + stylelint, cero warnings (zero-warning policy)
pnpm test --run --coverage # Vitest, ≥95% (RFC §11)
pnpm test:e2e # Playwright (provider Entra mockeado)
docker compose up --build -d && docker compose ps # app + postgres healthy
curl -fsS localhost:3000/api/health # endpoint de health responde
Risks¶
| Risk | Likelihood | Mitigation |
|---|---|---|
ms_oid NOT NULL (RFC §5) incompatible con whitelist previa al login |
Alta (ya detectado) | ✅ Resuelto: ms_oid nullable, se vincula en primer login; RFC §5 ya actualizado |
| Bootstrap del primer admin (gallina-huevo del whitelist) | Alta | Seed idempotente desde BOOTSTRAP_ADMIN_EMAIL |
| E2E del flujo OIDC real requiere tenant Entra | Media | Mockear el provider en E2E; testear la decisión de authz en unit/integration |
| Matching email vs oid (cambios de email, mayúsculas) | Media | Normalizar email (lower/trim); preferir ms_oid una vez vinculado |
| Callback URL de Entra apuntando a host on-premise expuesto | Media | Documentar redirect URI en .env.example; validar AUTH_URL al arranque |
| Scope creep: meter tablas de auditoría que son de M2+ | Media | Migración de M1 solo incluye stores y users |
Acceptance¶
- [ ] Tareas 1–10 completas
- [ ] Auditor habilitado entra con SSO; no habilitado ve
/denied; auditor no accede a(admin) - [ ] Admin (sembrado) gestiona altas/bajas de tiendas y usuarios desde el panel
- [ ]
pnpm typecheck,pnpm lint(cero warnings),pnpm test --coverage(≥95%),pnpm test:e2epasan - [ ]
docker compose updeja app + postgres healthy;/api/healthresponde - [x] RFC §5 actualizado (
ms_oidnullable) - [ ] Patrones establecidos y documentados para que M2 los espeje (no reinventar)
Status: DRAFT — esperando confirmación antes de escribir código.