This commit is contained in:
David Arranz 2026-07-06 17:47:05 +02:00
parent 0b07382919
commit cc14cdacf1
11 changed files with 372 additions and 13 deletions

View File

@ -1 +1,28 @@
Monorepo con cliente, servidor y packages modulares. # ERP Monorepo
Monorepo con aplicaciones, servidor y paquetes modulares para el ERP.
## Estado actual
- El runtime nuevo de autenticación y sesión vive en `modules/identity`.
- `modules/companies` es la fuente de verdad de la ficha operativa de empresa.
- `modules/auth` debe considerarse legacy/deprecated para backend nuevo.
- El frontend ERP ya usa `@erp/identity/client` como runtime actual.
## Documentación clave
- [Docs index](./docs/README.md)
- [Identity and companies](./docs/architecture/identity-and-companies.md)
- [Tenant context y `X-Company-Id`](./docs/architecture/tenant-context.md)
- [Identity auth API](./docs/architecture/identity-auth-api.md)
- [Frontend auth and company selection](./docs/frontend/auth-and-company-selection.md)
- [Seed local admin](./docs/dev/seed-local-admin.sql)
## Reglas rápidas
- Backend nuevo: usar `@erp/identity/api`.
- Rutas solo autenticadas: `requireIdentityAuthenticated(params)`.
- Rutas tenant-scoped: `requireIdentityTenant(params)`.
- `GET /companies/available` es el endpoint vigente para companies accesibles.
- No documentar ni introducir `GET /identity/companies`.
- No meter `companyId` ni `companySlug` dentro del access token.

74
apps/web/README.md Normal file
View File

@ -0,0 +1,74 @@
# `apps/web`
Aplicación frontend principal del ERP.
## Runtime actual de autenticación
La app usa:
```txt
@erp/identity/client
IdentityAuthSessionProvider
DataSourceProvider
Axios compartido
```
No debe usarse `@erp/auth/client` como runtime nuevo.
## Flujo actual
```txt
login
-> GET /identity/auth/session
-> GET /companies/available
-> resolver empresa activa
-> auto-selección si hay una sola empresa
-> selector si hay varias
-> /no-companies si no hay empresas
-> X-Company-Id en rutas tenant-scoped
```
## Headers
No enviar `X-Company-Id` a:
```txt
POST /identity/auth/login
POST /identity/auth/refresh
POST /identity/auth/logout
GET /identity/auth/session
GET /companies/available
```
Sí enviarlo a rutas tenant-scoped cuando exista `activeCompanyId`.
## App company switcher
Componente:
```txt
apps/web/src/layout/app-company-switcher.tsx
```
Propósito:
- mostrar la empresa activa real en el layout autenticado
- permitir cambiar entre `availableCompanies` de la sesión actual
Reglas:
- usar el contexto de `identity`
- no manejar tokens
- no llamar directamente a `GET /companies/available`
- no usar `companySlug` para auth
- no modificar el access token
## Refresh automático
La app implementa refresh automático global sobre `401` con retry único por request y `refreshPromise` compartida.
Si el refresh falla:
- limpiar sesión
- limpiar empresa activa persistida
- redirigir a `/login`

View File

@ -20,6 +20,10 @@ docs/
- [Estado de migración a identity](./architecture/identity-migration-status.md) - [Estado de migración a identity](./architecture/identity-migration-status.md)
- [Flujo frontend de auth y selección de empresa](./frontend/auth-and-company-selection.md) - [Flujo frontend de auth y selección de empresa](./frontend/auth-and-company-selection.md)
- [Seed local de admin](./dev/seed-local-admin.sql) - [Seed local de admin](./dev/seed-local-admin.sql)
- [README de `modules/auth`](../modules/auth/README.md)
- [README de `modules/identity`](../modules/identity/README.md)
- [README de `modules/companies`](../modules/companies/README.md)
- [README de `apps/web`](../apps/web/README.md)
## Estado actual resumido ## Estado actual resumido
@ -33,4 +37,5 @@ docs/
- `modules/auth` debe considerarse legacy/deprecated para backend nuevo. - `modules/auth` debe considerarse legacy/deprecated para backend nuevo.
- `GET /companies/available` es el endpoint vigente para cargar companies accesibles. - `GET /companies/available` es el endpoint vigente para cargar companies accesibles.
- `GET /identity/companies` no es un endpoint válido del diseño actual. - `GET /identity/companies` no es un endpoint válido del diseño actual.
- `GET /account/companies` tampoco es un endpoint válido del diseño actual.
- Las rutas tenant-scoped deben usar `Authorization` + `X-Company-Id`. - Las rutas tenant-scoped deben usar `Authorization` + `X-Company-Id`.

View File

@ -17,8 +17,8 @@ Gestiona:
- session - session
- `RefreshToken` - `RefreshToken`
- `CompanyMembership` - `CompanyMembership`
- acceso `account-company`
- roles/permisos futuros - roles/permisos futuros
- validación de acceso `accountId + companyId`
Endpoints activos: Endpoints activos:
@ -83,6 +83,7 @@ INDEX(tin)
- `Company` vive en `companies`. - `Company` vive en `companies`.
- No mover memberships a `companies`. - No mover memberships a `companies`.
- No evolucionar `identity.Company` como ficha operativa ERP. - No evolucionar `identity.Company` como ficha operativa ERP.
- No usar `customers` para representar la empresa tenant.
## Servicios públicos ## Servicios públicos
@ -133,6 +134,8 @@ GET /companies/available
Authorization: Bearer <access_token> Authorization: Bearer <access_token>
``` ```
Tampoco debe documentarse ni introducirse `GET /account/companies` como variante válida.
## Dependencias permitidas ## Dependencias permitidas
Para módulos tenant-scoped que usan `requireIdentityTenant(params)`, la dependencia explícita debe ser hacia: Para módulos tenant-scoped que usan `requireIdentityTenant(params)`, la dependencia explícita debe ser hacia:

View File

@ -10,6 +10,21 @@ GET /identity/auth/session
GET /companies/available GET /companies/available
``` ```
## Endpoints que no forman parte del diseño vigente
No documentar ni crear como ruta válida:
```http
GET /identity/companies
GET /account/companies
```
Motivo:
- `identity` no debe devolver fichas completas de empresa.
- `companies` ya depende de `identity` para autenticación.
- Un endpoint de ese tipo introduciría el ciclo `identity -> companies -> identity`.
## POST `/identity/auth/login` ## POST `/identity/auth/login`
### Request ### Request
@ -159,6 +174,7 @@ export type AvailableCompanyDTO = {
- no requiere `X-Company-Id` - no requiere `X-Company-Id`
- usa `requireIdentityAuthenticated(params)` - usa `requireIdentityAuthenticated(params)`
- devuelve solo companies activas accesibles para la cuenta autenticada - devuelve solo companies activas accesibles para la cuenta autenticada
- no modifica sesión ni access token
## Errores esperados ## Errores esperados

View File

@ -6,6 +6,10 @@
-- Purpose: -- Purpose:
-- Creates a local admin account, one active company and the -- Creates a local admin account, one active company and the
-- active membership between them. -- active membership between them.
-- Tables touched:
-- - identity_accounts
-- - companies
-- - identity_company_memberships
-- --
-- Intended use: -- Intended use:
-- Local/dev environments only. -- Local/dev environments only.

View File

@ -118,6 +118,36 @@ export type AvailableCompaniesResponseDTO = {
}; };
``` ```
## App company switcher
Componente documentado:
```txt
apps/web/src/layout/app-company-switcher.tsx
```
Propósito:
- cambiar de empresa activa una vez iniciada la sesión
- mostrar la empresa activa actual dentro del layout autenticado
Reglas:
- consume `availableCompanies` y `activeCompany` desde `useIdentityAuthSession()`
- usa `changeActiveCompany()` o `selectActiveCompany()` del contexto, no requests directas
- no llama directamente a `GET /companies/available` si el contexto ya está hidratado
- no maneja tokens
- no usa `@erp/auth/client`
- no usa `companySlug` para autenticación
- no modifica el access token
- al cambiar empresa, las siguientes requests tenant-scoped deben salir con el nuevo `X-Company-Id`
Comportamiento esperado:
- `0` empresas: no rompe; render deshabilitado o equivalente
- `1` empresa: muestra la empresa activa; no hace falta selector operativo
- `N` empresas: permite cambiar entre las permitidas y marca la activa
## Headers ## Headers
### No deben llevar `X-Company-Id` ### No deben llevar `X-Company-Id`
@ -172,3 +202,14 @@ Comportamiento:
/identity/auth/session /identity/auth/session
/companies/available /companies/available
``` ```
## Pruebas manuales recomendadas
1. Login con usuario sin empresas: debe ir a `/no-companies`.
2. Login con usuario con una empresa: debe auto-seleccionarse y entrar al área privada.
3. Login con usuario con varias empresas: debe mostrarse `/company-selection`.
4. Cambio de empresa desde `app-company-switcher`: la request tenant-scoped siguiente debe llevar el nuevo `X-Company-Id`.
5. Access token expirado con refresh válido: debe ejecutarse refresh + retry único.
6. Refresh token inválido: debe limpiarse la sesión y redirigir a `/login`.
7. Endpoints auth y `GET /companies/available`: no deben llevar `X-Company-Id`.
8. Rutas tenant-scoped: deben llevar `X-Company-Id` cuando exista `activeCompanyId`.

View File

@ -14,3 +14,5 @@ Importante:
- Este paquete sigue exponiendo `@erp/auth/client`, por lo que no debe retirarse todavía. - Este paquete sigue exponiendo `@erp/auth/client`, por lo que no debe retirarse todavía.
- No cambiar ni borrar exports legacy mientras `supplier` siga sin migrarse. - No cambiar ni borrar exports legacy mientras `supplier` siga sin migrarse.
- No usar `mockUser` ni middlewares legacy de auth en backend nuevo.
- No borrar `modules/auth` hasta retirar consumidores legacy reales.

View File

@ -0,0 +1,78 @@
# `modules/companies`
`companies` es el módulo independiente y fuente de verdad de la ficha operativa de empresa.
## Responsabilidades
Gestiona la entidad `Company` funcional del ERP:
- datos legales
- datos operativos básicos
- `slug`
- `status`
Separación arquitectónica:
- `modules/identity`: autenticación, cuentas, memberships, acceso account-company
- `modules/companies`: ficha real de empresa y validación de company activa
## Tabla `companies`
Campos actuales:
```txt
id
legal_name
trade_name
tin
slug
email
phone
website
status
created_at
updated_at
```
Índices esperados:
```txt
UNIQUE(slug)
INDEX(status)
INDEX(tin)
```
## Servicios públicos
El módulo expone:
```txt
companies:general
```
Responsabilidad:
- validar existencia y estado activo de la company
- devolver datos públicos/operativos de empresa
No debe asumir:
- memberships
- autenticación por cuenta
- roles/permisos de identity
## Dependencias
`modules/companies` depende de `identity`.
Uso actual:
- sus endpoints usan `requireIdentityAuthenticated(params)`
- `GET /companies/available` no usa `requireIdentityTenant(params)`
## Reglas
- `CompanyMembership` vive en `identity`.
- No mover memberships a `companies`.
- No usar `customers` para representar la empresa tenant.
- `companySlug` no debe venir de token, `X-Company-Slug` ni `req.user.companySlug`.

View File

@ -1,6 +1,6 @@
import type { IDocumentProperties, IDocumentPropertiesFactory } from "@erp/core/api"; import type { IDocumentProperties, IDocumentPropertiesFactory } from "@erp/core/api";
import type { IssuedInvoiceReportSnapshot } from "../application-models"; import type { IssuedInvoiceReportSnapshot } from "../models";
/** /**
* Construye los metadatos del documento PDF de una factura emitida. * Construye los metadatos del documento PDF de una factura emitida.

View File

@ -1,14 +1,123 @@
# Identity Module # `modules/identity`
`identity` convivirá temporalmente con `@erp/auth`. `identity` es el runtime nuevo de autenticación, sesión y acceso por memberships del ERP.
`@erp/auth` sigue siendo el runtime de autenticación actual. ## Responsabilidades
`identity` incorporará progresivamente la implementación V1 aprobada: Gestiona:
- `email/password`
- `access token`
- `refresh token` persistido hasheado
- tenant activo mediante `X-Company-Id`
- roles y permisos company-scoped
No se deben registrar middlewares ni reemplazar dependencias de `@erp/auth` hasta que `identity` tenga implementación funcional validada. - `Account`
- login
- refresh
- logout
- session
- `RefreshToken`
- `CompanyMembership`
- acceso `account-company`
- roles/permisos futuros
No gestiona:
- la ficha operativa completa de empresa
- `companySlug` en auth
- el tenant dentro del access token
## Endpoints activos
```http
POST /identity/auth/login
POST /identity/auth/refresh
POST /identity/auth/logout
GET /identity/auth/session
```
El endpoint para companies accesibles es:
```http
GET /companies/available
```
Ese endpoint pertenece al flujo actual, pero no implica que `identity` deba exponer `GET /identity/companies`.
## Access token
El access token contiene solo:
```ts
{
accountId: string;
email: string;
}
```
No debe contener:
- `companyId`
- `companySlug`
- roles
- permisos
## Middlewares públicos
Para rutas autenticadas sin tenant:
```ts
router.use(...requireIdentityAuthenticated(params));
```
Para rutas tenant-scoped:
```ts
router.use(...requireIdentityTenant(params));
```
Código backend nuevo no debe usar:
- `@erp/auth/api`
- `mockUser`
- `requireAuthenticated` legacy
- `requireCompanyContext` legacy
## Servicios públicos
`identity` expone el servicio:
```txt
identity:general
```
Con acceso a `companyAccess` para casos como:
```ts
canAccessCompany(...)
findAccessibleCompanyIds(...)
```
Responsabilidad:
- `identity` decide qué `companyId` puede usar una cuenta según memberships activas.
- no devuelve la ficha completa de empresa.
## Tenant context
`requireIdentityTenant(params)` compone:
- `identity:general.companyAccess`
- `companies:general.finder`
Validación esperada:
1. access token válido
2. cuenta autenticable
3. `X-Company-Id` presente
4. `X-Company-Id` UUID válido
5. membership activa
6. company activa
7. `req.user.companyId` poblado
## Reglas de mantenimiento
- Backend nuevo: usar `@erp/identity/api`.
- No documentar ni crear `GET /identity/companies`.
- No borrar `modules/auth` mientras existan consumidores legacy reales.
- `modules/supplier` sigue pendiente de migración si continúa usando el runtime legacy.