This commit is contained in:
David Arranz 2026-07-06 14:04:47 +02:00
parent 86bf1870ef
commit b1e9f4bd2d
10 changed files with 703 additions and 755 deletions

View File

@ -1,84 +1,36 @@
# Turborepo starter
# ERP Docs
This Turborepo starter is maintained by the Turborepo core team.
Este directorio reúne la documentación operativa y arquitectónica del ERP.
## Using this example
## Estructura actual
Run the following command:
```sh
npx create-turbo@latest
```txt
docs/
README.md
architecture/
frontend/
dev/
```
## What's inside?
## Documentos clave
This Turborepo includes the following packages/apps:
- [Arquitectura de identity y companies](./architecture/identity-and-companies.md)
- [Contexto tenant y `X-Company-Id`](./architecture/tenant-context.md)
- [API de autenticación identity](./architecture/identity-auth-api.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)
- [Seed local de admin](./dev/seed-local-admin.sql)
### Apps and Packages
## Estado actual resumido
- `docs`: a [Next.js](https://nextjs.org/) app
- `web`: another [Next.js](https://nextjs.org/) app
- `@repo/shadcn-ui`: a stub React component library shared by both `web` and `docs` applications
- `@repo/eslint-config`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`)
- `@repo/typescript-config`: `tsconfig.json`s used throughout the monorepo
- El backend nuevo usa `modules/identity` para autenticación, sesión y control de acceso a companies.
- `modules/companies` es la fuente de verdad de la ficha operativa de empresa.
- El frontend ERP ya usa `@erp/identity/client` como runtime de autenticación.
- El flujo actual resuelve sesión, companies disponibles, auto-selección de empresa única y refresh automático sobre `401`.
Each package/app is 100% [TypeScript](https://www.typescriptlang.org/).
## Reglas de lectura rápida
### Utilities
This Turborepo has some additional tools already setup for you:
- [TypeScript](https://www.typescriptlang.org/) for static type checking
- [ESLint](https://eslint.org/) for code linting
- [Prettier](https://prettier.io) for code formatting
### Build
To build all apps and packages, run the following command:
```
cd my-turborepo
pnpm build
```
### Develop
To develop all apps and packages, run the following command:
```
cd my-turborepo
pnpm dev
```
### Remote Caching
> [!TIP]
> Vercel Remote Cache is free for all plans. Get started today at [vercel.com](https://vercel.com/signup?/signup?utm_source=remote-cache-sdk&utm_campaign=free_remote_cache).
Turborepo can use a technique known as [Remote Caching](https://turbo.build/repo/docs/core-concepts/remote-caching) to share cache artifacts across machines, enabling you to share build caches with your team and CI/CD pipelines.
By default, Turborepo will cache locally. To enable Remote Caching you will need an account with Vercel. If you don't have an account you can [create one](https://vercel.com/signup?utm_source=turborepo-examples), then enter the following commands:
```
cd my-turborepo
npx turbo login
```
This will authenticate the Turborepo CLI with your [Vercel account](https://vercel.com/docs/concepts/personal-accounts/overview).
Next, you can link your Turborepo to your Remote Cache by running the following command from the root of your Turborepo:
```
npx turbo link
```
## Useful Links
Learn more about the power of Turborepo:
- [Tasks](https://turbo.build/repo/docs/core-concepts/monorepos/running-tasks)
- [Caching](https://turbo.build/repo/docs/core-concepts/caching)
- [Remote Caching](https://turbo.build/repo/docs/core-concepts/remote-caching)
- [Filtering](https://turbo.build/repo/docs/core-concepts/monorepos/filtering)
- [Configuration Options](https://turbo.build/repo/docs/reference/configuration)
- [CLI Usage](https://turbo.build/repo/docs/reference/command-line-reference)
- `modules/auth` debe considerarse legacy/deprecated para backend nuevo.
- `GET /companies/available` es el endpoint vigente para cargar companies accesibles.
- `GET /identity/companies` no es un endpoint válido del diseño actual.
- Las rutas tenant-scoped deben usar `Authorization` + `X-Company-Id`.

View File

@ -1,150 +1,18 @@
# ERP Backend · Identity/Auth Migration Notes
# ERP Architecture Docs
## Propósito
## Documentos vigentes
Este documento consolida las decisiones de arquitectura y el estado de migración del backend ERP desde el módulo legacy `auth` hacia el nuevo módulo `identity`.
- [Identity and companies](./identity-and-companies.md)
- [Tenant context](./tenant-context.md)
- [Identity auth API](./identity-auth-api.md)
- [Identity backend architecture](./identity-backend-architecture.md)
- [Identity migration status](./identity-migration-status.md)
- [Identity roadmap](./identity-roadmap.md)
Debe usarse como referencia para futuros incrementos relacionados con:
## Resumen actual
- autenticación
- sesión
- contexto tenant/company
- registro de usuarios
- roles y permisos
- autorización
- login frontend
- retirada progresiva de `modules/auth`
## Estado actual resumido
El backend ya dispone de un módulo `identity` registrado en servidor y con endpoints de autenticación funcionales:
```http
POST /identity/auth/login
POST /identity/auth/refresh
POST /identity/auth/logout
GET /identity/auth/session
```
El módulo `identity` expone builders genéricos desde `@erp/identity/api`:
```ts
requireIdentityAuthenticated(params: StartParams): RequestHandler[];
requireIdentityTenant(params: StartParams): RequestHandler[];
```
Uso esperado en routers tenant-scoped:
```ts
import { requireIdentityTenant } from "@erp/identity/api";
router.use(...requireIdentityTenant(params));
```
Uso esperado en routers que solo requieren usuario autenticado:
```ts
import { requireIdentityAuthenticated } from "@erp/identity/api";
router.use(...requireIdentityAuthenticated(params));
```
## Módulos backend migrados a `identity`
Ya no usan `@erp/auth/api` ni `mockUser`:
- `modules/catalogs`
- `modules/customers`
- `modules/customer-invoices`
- `modules/factuges`
## Consumidores legacy pendientes
Backend pendiente explícitamente:
- `modules/supplier/src/api/infrastructure/express/suppliers.routes.ts`
Frontend todavía usa cliente legacy:
- `apps/web/src/register-modules.tsx`
- `apps/web/src/app.tsx`
Por tanto, `modules/auth` no puede retirarse todavía.
## Regla de arquitectura vigente
Los módulos funcionales no deben importar ni recomponer internals de `identity`.
No usar en módulos consumidores:
```ts
IdentityInternalDeps;
authenticateUserDependencies;
getInternal("identity");
```
No crear helpers de auth por módulo como patrón final:
```txt
catalogs-auth-middlewares.ts
customers-auth-middlewares.ts
...
```
El middleware genérico pertenece a `identity`.
## Separación de responsabilidades
### Authentication
Responde a:
- quién es el usuario
- si el token es válido
- si la cuenta puede autenticarse
### Tenant context
Responde a:
- qué empresa activa se está usando en la request
- actualmente se resuelve por header `X-Company-Id`
### Authorization
Responderá a:
- qué permisos efectivos tiene el usuario dentro de la empresa
Todavía no está implementado en modo completo.
### Document context
Datos como `companySlug`, templates, certificados o metadatos de generación documental no pertenecen al middleware de autenticación.
No añadir a `identity` auth:
- `companySlug`
- `X-Company-Slug`
- `companySlug` en access token
Si un flujo documental necesita `companySlug`, debe resolverse mediante un servicio específico de empresa/documentos.
## Roadmap corto recomendado
1. Crear pantalla de login en Frontend ERP contra `/identity/auth/login`.
2. Mantener `supplier` temporalmente en legacy si así se decide.
3. Implementar refresh automático en frontend.
4. Implementar selección de empresa y uso de `X-Company-Id`.
5. Implementar persistencia y validación real de `CompanyMembership`.
6. Implementar resolución de permisos efectivos.
7. Migrar `supplier` cuando se decida.
8. Retirar backend legacy de `@erp/auth/api`.
9. Migrar o retirar `@erp/auth/client`.
## Documentos relacionados
- [`identity-auth-api.md`](./identity-auth-api.md)
- [`identity-backend-architecture.md`](./identity-backend-architecture.md)
- [`identity-migration-status.md`](./identity-migration-status.md)
- [`identity-roadmap.md`](./identity-roadmap.md)
- `identity` se encarga de autenticación, sesión, refresh y memberships.
- `companies` se encarga de la ficha operativa de empresa.
- `GET /companies/available` es la vía actual para listar companies accesibles.
- El contexto tenant se resuelve con `X-Company-Id` y `requireIdentityTenant(params)`.
- El frontend ERP ya usa el runtime nuevo de `identity` con auto-selección de empresa y refresh automático.

View File

@ -0,0 +1,175 @@
# Identity And Companies
## Objetivo
Documentar la separación actual entre `modules/identity` y `modules/companies`, evitando mezclar autenticación, memberships y ficha operativa de empresa.
## Responsabilidades por módulo
### `modules/identity`
Gestiona:
- `Account`
- login
- refresh
- logout
- session
- `RefreshToken`
- `CompanyMembership`
- roles/permisos futuros
- validación de acceso `accountId + companyId`
Endpoints activos:
```http
POST /identity/auth/login
POST /identity/auth/refresh
POST /identity/auth/logout
GET /identity/auth/session
```
El access token contiene solo:
```ts
{
accountId: string;
email: string;
}
```
No debe contener:
- `companyId`
- `companySlug`
- roles
- permisos
### `modules/companies`
Es la fuente de verdad de la ficha operativa de empresa.
`Company` funcional vive en `modules/companies`.
Campos actuales de `companies`:
```txt
id
legal_name
trade_name
tin
slug
email
phone
website
status
created_at
updated_at
```
Índices actuales:
```txt
UNIQUE(slug)
INDEX(status)
INDEX(tin)
```
`slug` pertenece a `companies`, no a auth ni al token.
## Relación entre ambos módulos
- `CompanyMembership` vive en `identity`.
- `Company` vive en `companies`.
- No mover memberships a `companies`.
- No evolucionar `identity.Company` como ficha operativa ERP.
## Servicios públicos
### `identity:general`
Expone autenticación y acceso por membership.
Uso conceptual:
```ts
companyAccess.canAccessCompany(...)
companyAccess.findAccessibleCompanyIds(...)
```
`identity` devuelve IDs accesibles por membership, no fichas completas de empresa.
### `companies:general`
Expone consulta de empresas y validación de empresa activa.
Uso conceptual:
```ts
ICompanyPublicServices {
finder: ICompanyPublicFinder;
}
```
## Por qué no existe `GET /identity/companies`
No se documenta `GET /identity/companies` como endpoint válido porque produciría acoplamiento circular:
```txt
identity -> companies -> identity
```
Si `identity` devolviera fichas completas de company, tendría que depender de `companies`.
Como `companies` ya depende de `identity` para autenticación, el diseño correcto es:
- `identity` resuelve membership y accesibilidad por ID
- `companies` resuelve la ficha completa de empresa
El endpoint vigente para el frontend es:
```http
GET /companies/available
Authorization: Bearer <access_token>
```
## Dependencias permitidas
Para módulos tenant-scoped que usan `requireIdentityTenant(params)`, la dependencia explícita debe ser hacia:
- `identity`
- `companies`
Porque el middleware tenant compone:
```txt
identity:general.companyAccess
companies:general.finder
```
## Dependencias prohibidas
No introducir:
- `identity -> companies`
- recomposición manual de internals de `identity`
- `IdentityInternalDeps`
- `getInternal("identity")` desde módulos consumidores
- helpers locales de auth por módulo como patrón estable
## `GET /companies/available`
Semántica actual:
- requiere `Authorization`
- no requiere `X-Company-Id`
- usa `requireIdentityAuthenticated(params)`
- devuelve companies activas accesibles para la cuenta autenticada
La disponibilidad se calcula con:
```txt
membership active en identity
+
company active en companies
```

View File

@ -1,12 +1,13 @@
# Identity Auth API
## Endpoints disponibles
## Endpoints activos
```http
POST /identity/auth/login
POST /identity/auth/refresh
POST /identity/auth/logout
GET /identity/auth/session
GET /companies/available
```
## POST `/identity/auth/login`
@ -42,11 +43,11 @@ export type AuthenticatedAccountDTO = {
### Notas
- No requiere `Authorization`.
- No requiere `X-Company-Id`.
- No devuelve empresas accesibles todavía.
- No devuelve roles ni permisos.
- El access token no contiene `companyId`.
- no requiere `Authorization`
- no requiere `X-Company-Id`
- no devuelve companies accesibles
- no devuelve roles ni permisos
- el access token no contiene `companyId`
## POST `/identity/auth/refresh`
@ -69,9 +70,9 @@ export type RefreshSessionResponseDTO = {
### Notas
- El refresh token se rota.
- El cliente debe reemplazar ambos tokens por los nuevos.
- El refresh token anterior no debe reutilizarse tras una renovación correcta.
- el refresh token se rota
- el cliente debe reemplazar ambos tokens por los nuevos
- no debe llevar `X-Company-Id`
## POST `/identity/auth/logout`
@ -100,9 +101,9 @@ export type LogoutResponseDTO = {
### Reglas
- Para logout normal, enviar el `refresh_token` actual.
- Para cerrar todas las sesiones, enviar `all_sessions: true`.
- Si logout falla por expiración de token, el frontend debe limpiar sesión igualmente.
- no debe llevar `X-Company-Id`
- para logout normal, enviar el `refresh_token` actual
- si logout falla por expiración de token, el frontend debe limpiar sesión igualmente
## GET `/identity/auth/session`
@ -120,24 +121,50 @@ export type CurrentSessionResponseDTO = {
};
```
## Errores esperados
### Notas
```txt
400 -> request inválida o header mal formado
401 -> access token ausente/inválido/expirado o credenciales inválidas
403 -> contexto de empresa requerido en rutas tenant-scoped
500 -> error inesperado
```
- no debe llevar `X-Company-Id`
## Rutas tenant-scoped
## GET `/companies/available`
Las rutas de negocio tenant-scoped deben enviar:
### Headers
```http
Authorization: Bearer <access_token>
X-Company-Id: <uuid-company>
```
Actualmente `X-Company-Id` se valida sintácticamente como UUID y se publica como `req.user.companyId`.
### Response
La validación real de membership queda pendiente.
```ts
export type AvailableCompaniesResponseDTO = {
companies: AvailableCompanyDTO[];
};
export type AvailableCompanyDTO = {
id: string;
legal_name: string;
trade_name: string | null;
tin: string | null;
slug: string;
email: string | null;
phone: string | null;
website: string | null;
status: "active";
};
```
### Notas
- usa autenticación, no tenant context
- no requiere `X-Company-Id`
- usa `requireIdentityAuthenticated(params)`
- devuelve solo companies activas accesibles para la cuenta autenticada
## Errores esperados
```txt
400 -> request inválida o UUID de X-Company-Id mal formado
401 -> access token ausente/inválido/expirado o credenciales inválidas
403 -> contexto tenant inválido, company no accesible o company no activa
500 -> error inesperado
```

View File

@ -4,7 +4,7 @@
`identity` sustituye progresivamente al módulo legacy `auth`.
Responsabilidades previstas:
Responsabilidades actuales:
- cuentas autenticables
- login/password
@ -12,48 +12,22 @@ Responsabilidades previstas:
- refresh token rotado
- logout
- sesión actual
- empresas accesibles
- memberships
- roles
- permisos
- autorización futura
- validación de acceso account-company
- base para roles/permisos futuros
## Dominio mínimo creado
## Dominio actual
Agregados principales:
Agregados y conceptos relevantes:
```txt
Account
Company
RefreshToken
CompanyMembership
Role
RefreshToken
```
## Value Objects reutilizados
Desde `@repo/rdx-ddd`:
```txt
UniqueID
LanguageCode
Name
TextValue
UtcDate
EmailAddress
URLAddress
```
VOs específicos de `identity`:
```txt
PasswordHash
RefreshTokenHash
RoleCode
AccountStatus
CompanyStatus
CompanyMembershipStatus
```
`Company` funcional no pertenece a `identity`; vive en `modules/companies`.
## Seguridad
@ -67,81 +41,40 @@ IRefreshTokenGenerator
IRefreshTokenHasher
```
Implementaciones actuales:
Decisiones vigentes:
```txt
BcryptPasswordHasher
JsonWebTokenAccessTokenIssuer
JsonWebTokenAccessTokenVerifier
CryptoRefreshTokenGenerator
Sha256RefreshTokenHasher
```
- password hashing con `bcrypt`
- access token JWT con `accountId` y `email`
- refresh token aleatorio
- refresh token persistido como hash
- no incluir `companyId`, `companySlug`, roles ni permisos en access token
Decisiones:
## Builders públicos
- password hashing con `bcrypt` en V1.
- access token JWT con `accountId` y `email`.
- refresh token aleatorio con `crypto.randomBytes`.
- refresh token persistido solo como hash SHA-256.
- no incluir `companyId`, roles ni permisos en access token.
## Persistencia mínima
Tablas/modelos creados:
```txt
identity_accounts
identity_refresh_tokens
```
Repositorios:
```txt
IAccountRepository
IRefreshTokenRepository
SequelizeAccountRepository
SequelizeRefreshTokenRepository
```
## Servicios públicos del módulo
`identity` registra un servicio público:
```txt
identity:general
```
Incluye:
```ts
auth.authenticatedOnly(): RequestHandler[];
auth.tenantRequired(): RequestHandler[];
```
Pero los consumidores no deberían usar directamente `identity:general` en routers.
Deben usar los builders públicos:
`identity` expone desde `@erp/identity/api`:
```ts
requireIdentityAuthenticated(params: StartParams): RequestHandler[];
requireIdentityTenant(params: StartParams): RequestHandler[];
```
## Uso correcto en routers
Uso correcto:
```ts
import { requireIdentityTenant } from "@erp/identity/api";
export function buildSomeRoutes(params: StartParams) {
const router = Router();
router.use(...requireIdentityTenant(params));
return router;
}
router.use(...requireIdentityTenant(params));
```
## Shape actual de `req.user`
```ts
import { requireIdentityAuthenticated } from "@erp/identity/api";
router.use(...requireIdentityAuthenticated(params));
```
## `req.user`
Shape mínimo esperado:
```ts
{
@ -152,11 +85,35 @@ export function buildSomeRoutes(params: StartParams) {
}
```
Compatibilidad provisional:
`req.user.companyId` solo se debe poblar cuando el middleware tenant valida correctamente la request.
- `roles` puede ser `[]`.
- `companyId` viene de `X-Company-Id`.
- `companySlug` no debe depender de auth.
No añadir:
- `companySlug`
- `company`
- `permissions` enriquecidos
- `companyStatus`
## Tenant validation
`requireIdentityTenant(params)` ya no es solo validación sintáctica.
Valida:
1. token válido
2. account autenticable
3. `X-Company-Id` presente
4. UUID válido
5. membership `active`
6. company `active`
## Relación con `companies`
- `identity` resuelve membership y accesibilidad
- `companies` resuelve la ficha operativa de empresa
- no introducir dependencia `identity -> companies`
Si un consumidor necesita `slug`, debe resolverlo desde `companies`, no desde auth.
## Prohibiciones de diseño
@ -168,54 +125,11 @@ authenticateUserDependencies;
getInternal("identity");
```
No crear:
No crear helpers locales de auth por módulo como patrón estable.
```txt
<module>-auth-middlewares.ts
```
No añadir al access token:
como patrón estable.
No añadir a access token:
```txt
companyId
roles
permissions
companySlug
```
## Permisos
Cada módulo define sus propios permisos.
`core` contiene contratos transversales mínimos:
```ts
export type PermissionCode = string;
export type PermissionDefinition = {
code: PermissionCode;
module: string;
description: string;
};
```
`identity` define solo sus permisos propios:
```txt
identity:companies:update
identity:roles:read
identity:roles:create
identity:roles:update
identity:roles:delete
identity:company-users:read
identity:company-users:update-roles
identity:company-users:disable
identity:company-users:enable
identity:permissions:read
```
`Role.permissionCodes` usa `PermissionCode[]`, no `IdentityPermissionCode[]`.
La existencia real de permisos se valida en Application con `IPermissionCatalog`.
- `companyId`
- `companySlug`
- roles
- permisos

View File

@ -20,130 +20,74 @@ router.use(...requireIdentityAuthenticated(params));
## Migrado a `identity`
### `modules/catalogs`
### Backend tenant-scoped
Routers migrados:
Módulos migrados:
```txt
payment-methods.routes.ts
payment-terms.routes.ts
tax-regimes.routes.ts
tax-definitions.routes.ts
```
- `modules/catalogs`
- `modules/customers`
- `modules/customer-invoices`
- `modules/factuges`
Estado:
```txt
sin @erp/auth/api
sin mockUser
sin helpers locales de auth
usa requireIdentityTenant(params)
- sin `@erp/auth/api`
- sin `mockUser`
- sin helpers locales de auth
- usan `requireIdentityTenant(params)`
### Backend autenticado sin tenant
`modules/companies` usa:
```ts
import { requireIdentityAuthenticated } from "@erp/identity/api";
router.use(...requireIdentityAuthenticated(params));
```
### `modules/customers`
Esto aplica a:
Router migrado:
```txt
customers.routes.ts
```http
POST /companies
GET /companies/available
GET /companies/:company_id
PATCH /companies/:company_id
POST /companies/:company_id/enable
POST /companies/:company_id/disable
```
Estado:
## Frontend actual
El runtime nuevo ya usa:
- `@erp/identity/client`
- `IdentityAuthSessionProvider`
- `DataSourceProvider`
- Axios compartido con refresh automático
Flujo implementado:
```txt
sin @erp/auth/api
sin mockUser
sin helpers locales de auth
usa requireIdentityTenant(params)
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
```
### `modules/customer-invoices`
## Legacy pendiente
Routers migrados:
`modules/auth` queda legacy/deprecated y no debe usarse en backend nuevo.
```txt
proformas.routes.ts
issued-invoices.routes.ts
```
Estado:
```txt
sin @erp/auth/api
sin mockUser
sin helpers locales de auth
usa requireIdentityTenant(params)
```
Notas:
- `companySlug` ya no debe venir de `req.user`.
- La deuda documental de `companySlug: "rodax"` queda fuera del diseño de auth.
### `modules/factuges`
Router migrado:
```txt
factuges.routes.ts
```
Estado:
```txt
sin @erp/auth/api
sin mockUser
usa requireIdentityTenant(params)
```
Nota:
- La carpeta sigue llamándose `infraestructure`; no se ha corregido en esta migración.
## Pendiente backend
Sigue pendiente revisar o retirar consumidores legacy residuales antes de eliminarlo por completo.
### `modules/supplier`
Sigue usando:
```ts
import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth/api";
```
Archivo:
```txt
modules/supplier/src/api/infrastructure/express/suppliers.routes.ts
```
Decisión actual:
```txt
No migrar supplier por ahora.
```
## Legacy documentado
`modules/auth` queda documentado como legacy.
Uso backend nuevo recomendado:
```ts
import { requireIdentityTenant, requireIdentityAuthenticated } from "@erp/identity/api";
```
`@erp/auth/api` se mantiene temporalmente solo por `supplier`.
## Frontend legacy pendiente
Todavía existen imports de `@erp/auth/client` en:
```txt
apps/web/src/register-modules.tsx
apps/web/src/app.tsx
```
Esto impide retirar completamente `modules/auth` aunque el backend acabe limpio.
Queda fuera de esta migración por ahora.
## Búsquedas útiles de control
@ -151,13 +95,7 @@ Esto impide retirar completamente `modules/auth` aunque el backend acabe limpio.
rg -n -F "@erp/auth/api" modules apps packages
rg -n -F "mockUser" modules apps packages
rg -n -F "IdentityInternalDeps" modules apps packages
rg -n -F 'getInternal("identity")' modules apps packages
rg -n -F "getInternal('identity')" modules apps packages
rg -n -F 'getInternal(\"identity\")' modules apps packages
rg -n -F "requireIdentityTenant(params)" modules apps packages
rg -n -F "requireIdentityAuthenticated(params)" modules apps packages
```
Resultado esperado actual:
- `@erp/auth/api`: solo `supplier` y documentación/exports legacy.
- `mockUser`: solo `supplier` y `modules/auth`.
- `IdentityInternalDeps`: no debe aparecer en módulos consumidores.
- `getInternal("identity")`: no debe aparecer en módulos consumidores.

View File

@ -2,216 +2,57 @@
## Objetivo general
Completar la transición desde el módulo legacy `auth` hacia `identity`, sin mezclar autenticación con autorización, tenant context, datos documentales o frontend legacy.
Completar la transición desde el módulo legacy `auth` hacia `identity`, manteniendo separadas autenticación, tenant context, companies y autorización.
## Fase 1 · Backend auth runtime
## Estado consolidado
Estado: prácticamente completado.
Incluye:
Ya está implementado:
```txt
login
refresh rotado
logout
session
access token verifier
refresh token persistence
middlewares genéricos
GET /companies/available
selección de empresa en frontend
auto-selección con una sola empresa
refresh automático sobre 401 en frontend
validación real de membership active + company active en tenant middleware
```
Pendiente técnico recomendado:
## Trabajo todavía pendiente
```txt
- sanear typings de bcrypt/jsonwebtoken en identity
- revisar errores reales de node tsc
```
## Fase 2 · Migración backend desde `@erp/auth/api`
Estado:
```txt
catalogs migrado
customers migrado
customer-invoices migrado
factuges migrado
supplier pendiente intencionado
```
Siguiente decisión:
```txt
migrar supplier o mantenerlo legacy temporalmente
```
## Fase 3 · Frontend login
Objetivo:
Crear pantalla `/login` en Frontend ERP contra:
```http
POST /identity/auth/login
```
Debe implementar:
```txt
email/password
persistencia temporal de tokens
carga de sesión actual
logout básico
preparación para refresh automático
```
No incluir todavía:
```txt
register
forgot password
MFA
OAuth
roles/permisos
selección avanzada de empresa
```
## Fase 4 · Selección de empresa
Backend ya usa `X-Company-Id` para rutas tenant-scoped.
### Roles y permisos
Pendiente:
```txt
GET /identity/companies
GET /identity/companies/current
PATCH /identity/companies/current
```
- resolución de permisos efectivos
- middleware de autorización fina
- endpoints funcionales de roles/permisos si se activan
Frontend deberá:
```txt
listar empresas accesibles
permitir seleccionar empresa activa
enviar X-Company-Id en rutas tenant-scoped
```
## Fase 5 · Membership real
Actualmente `X-Company-Id` se valida sintácticamente pero no contra membership real.
### Migración legacy restante
Pendiente:
```txt
persistencia Company
persistencia CompanyMembership
validación accountId + companyId
bloqueo si membership disabled/invited
```
- revisar consumidores legacy que sigan atados a `modules/auth`
- migrar o retirar `supplier` cuando se decida
- retirar `@erp/auth/client` si aún queda compatibilidad heredada
No validar membership en access token.
La validación debe ocurrir en backend por request o mediante contexto cacheado controlado.
## Fase 6 · Roles y permisos
Ya existe base conceptual:
```txt
Role
PermissionCode
PermissionDefinition
IPermissionCatalog
RolePermissionValidator
```
Pendiente:
```txt
RoleCreator
RoleUpdater
PermissionResolver
AuthorizationService
middleware authorize(permission)
endpoints /identity/roles
endpoints /identity/permissions
```
Reglas:
```txt
cada módulo declara sus propios permisos
identity no define permisos de otros módulos
Role.permissionCodes usa PermissionCode[] transversal
```
## Fase 7 · Registro y onboarding
No implementar registro público sin decisión previa.
Opciones futuras:
```txt
registro público
creación de primera empresa
invitaciones
alta interna por admin
```
Posible secuencia segura:
```txt
1. RegisterAccountUseCase
2. CreateInitialCompanyUseCase
3. CreateOwnerMembershipUseCase
4. Seed owner role
5. Emitir sesión
```
Debe evitarse:
```txt
crear cuentas sin empresa cuando el producto exige tenant
crear empresas sin owner
crear roles sin permisos válidos
```
## Fase 8 · Retirada de `modules/auth`
Solo posible cuando:
```txt
- supplier deje de usar @erp/auth/api
- frontend deje de usar @erp/auth/client
- no queden imports a @erp/auth en runtime
```
Antes de retirar:
```powershell
rg -n -F "@erp/auth" modules apps packages
rg -n -F "mockUser" modules apps packages
```
## Decisiones explícitas
## Decisiones que se mantienen
No hacer:
```txt
- meter companyId en access token
- meter `companyId` en access token
- meter roles/permisos en access token
- meter companySlug en access token
- poblar companySlug desde auth middleware
- crear helpers auth por módulo
- usar getInternal("identity") desde módulos consumidores
- usar IdentityInternalDeps desde módulos consumidores
```
- meter `companySlug` en access token
- documentar `GET /identity/companies` como endpoint vigente
- poblar `companySlug` desde auth middleware
- usar `getInternal("identity")` desde módulos consumidores
- usar `IdentityInternalDeps` desde módulos consumidores
Sí hacer:
```txt
- usar requireIdentityTenant(params) en rutas tenant-scoped
- usar requireIdentityAuthenticated(params) en rutas solo autenticadas
- resolver datos documentales desde servicios específicos
- validar membership en backend cuando exista persistencia real
```
- usar `requireIdentityTenant(params)` en rutas tenant-scoped
- usar `requireIdentityAuthenticated(params)` en rutas solo autenticadas
- usar `GET /companies/available` para companies accesibles en frontend
- resolver datos documentales desde `companies` u otros servicios específicos

View File

@ -0,0 +1,115 @@
# Tenant Context
## Objetivo
Documentar cómo se resuelve el contexto tenant actual del ERP mediante `X-Company-Id` y `requireIdentityTenant(params)`.
## Header tenant
Las rutas tenant-scoped deben recibir:
```http
Authorization: Bearer <access_token>
X-Company-Id: <uuid-company>
```
`X-Company-Id` es el identificador de la company activa en la request.
No usar:
- `X-Company-Slug`
- `companySlug` en token
- `req.user.companySlug`
## Middleware público
Para rutas tenant-scoped:
```ts
import { requireIdentityTenant } from "@erp/identity/api";
router.use(...requireIdentityTenant(params));
```
Para rutas solo autenticadas:
```ts
import { requireIdentityAuthenticated } from "@erp/identity/api";
router.use(...requireIdentityAuthenticated(params));
```
## Validación real de `requireIdentityTenant(params)`
Actualmente ya valida:
1. access token válido
2. account autenticable
3. `X-Company-Id` presente
4. `X-Company-Id` con formato UUID válido
5. membership `active` en `identity_company_memberships`
6. company `active` en `companies`
7. solo entonces rellena `req.user.companyId`
## Semántica esperada de errores
```txt
sin Authorization -> 401
token inválido -> 401
cuenta inexistente/no autenticable -> 401
sin X-Company-Id -> 403
X-Company-Id inválido -> 400
company inexistente -> 403
company disabled -> 403
membership inexistente -> 403
membership disabled/invited -> 403
membership active + company active -> entra al controller
```
## Shape mínimo de `req.user`
```ts
{
userId: UniqueID;
email?: EmailAddress;
companyId?: UniqueID;
roles?: string[];
}
```
No añadir:
- `companySlug`
- `company`
- `permissions` enriquecidos
- `companyStatus`
## Módulos tenant-scoped migrados
Actualmente usan `requireIdentityTenant(params)`:
- `modules/catalogs`
- `modules/customers`
- `modules/customer-invoices`
- `modules/factuges`
`modules/supplier` queda fuera de esta migración por ahora.
## Qué no debe ir en el token
El access token no debe incluir:
- `companyId`
- `companySlug`
- roles
- permisos
El contexto tenant se resuelve por request con `X-Company-Id`.
## Document context
Si un flujo documental necesita `companySlug`, debe resolverlo desde `companies`, por ejemplo:
```txt
companyId -> companies:general.finder.findById(...) -> slug
```

View File

@ -12,59 +12,28 @@
--
-- Credentials:
-- email: admin@local.test
-- password: Passw0rd123
--
-- Important:
-- Replace <BCRYPT_HASH_GENERADO> before running this script.
-- password: Admin123!
--
-- Generate bcrypt hash from the workspace:
--
-- node -e "const bcrypt=require('bcrypt'); bcrypt.hash('Admin123!', 10).then(console.log)"
--
-- Replace the hash below if you want to regenerate it.
-- This script is idempotent:
-- - It does not duplicate the admin account if the email exists.
-- - It does not duplicate the company if the slug exists.
-- - It does not duplicate the membership if it already exists.
--
-- If you need to force password reset for the existing admin,
-- uncomment the UPDATE block near the end.
-- ============================================================
SET @admin_id = UUID();
SET @company_id = UUID();
SET @admin_email = 'admin@local.test';
SET @admin_password_hash = '$2b$10$6m6Nh2OpDy9MlQF18KOueOzLSCHybcg8yu1JyG0XxjgxB5Qx2dMdG';
SET
@admin_password_hash = '$2a$10$qgmmm34tJug4HydlKwcZxOVA5u5zoDTLE5lkH//sp55tl5au2wNQm';
SET
@company_id1 = "5e4dc5b3-96b9-4968-9490-14bd032fec5f" -- UUID();
SET
@company_slug1 = 'rodax';
SET @company_legal_name1 = 'Rodax Software S.L.';
SET @company_trade_name1 = 'Rodax';
SET @company_id2 = UUID();
SET @company_slug2 = 'company2';
SET @company_legal_name2 = 'Empresa 2 S.L.';
SET @company_trade_name2 = 'Empresa 2';
SET @company_id3 = UUID();
SET @company_slug3 = 'company3';
SET @company_legal_name3 = 'Empresa 2 S.L.';
SET @company_trade_name3 = 'Empresa 3';
-- ------------------------------------------------------------
-- Account admin
-- ------------------------------------------------------------
SET @company_slug = 'rodax';
SET @company_legal_name = 'Rodax Software S.L.';
SET @company_trade_name = 'Rodax';
INSERT INTO
identity_accounts (
@ -96,17 +65,12 @@ WHERE
email = @admin_email
);
-- Recover the real account id if the account already existed.
SELECT id INTO @admin_id
FROM identity_accounts
WHERE
email = @admin_email
LIMIT 1;
-- ------------------------------------------------------------
-- Company
-- ------------------------------------------------------------
INSERT INTO
companies (
id,
@ -141,17 +105,12 @@ WHERE
slug = @company_slug
);
-- Recover the real company id if the company already existed.
SELECT id INTO @company_id
FROM companies
WHERE
slug = @company_slug
LIMIT 1;
-- ------------------------------------------------------------
-- Account-company membership
-- ------------------------------------------------------------
INSERT INTO
identity_company_memberships (
id,
@ -171,21 +130,6 @@ WHERE
AND company_id = @company_id
);
-- ------------------------------------------------------------
-- Optional: force admin password/status update in local/dev
-- ------------------------------------------------------------
--
-- UPDATE identity_accounts
-- SET
-- password_hash = @admin_password_hash,
-- status = 'active',
-- updated_at = CURRENT_TIMESTAMP
-- WHERE email = @admin_email;
-- ------------------------------------------------------------
-- Final check
-- ------------------------------------------------------------
SELECT
a.id AS account_id,
a.email,
@ -202,4 +146,4 @@ FROM
JOIN companies c ON c.id = m.company_id
WHERE
a.email = @admin_email
AND c.slug = @company_slug;
AND c.slug = @company_slug;

View File

@ -0,0 +1,174 @@
# Frontend Auth And Company Selection
## Runtime actual
El frontend ERP usa el runtime nuevo:
```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
```
## Login
El login usa:
```http
POST /identity/auth/login
```
Tras login correcto:
- se guardan `accessToken` y `refreshToken`
- se limpia cualquier `activeCompanyId` previo
- se carga `GET /companies/available`
- se resuelve el redirect autenticado según companies disponibles
## Session restore
Si existe `accessToken` persistido:
1. se llama `GET /identity/auth/session`
2. se hidrata la sesión autenticada
3. se llama `GET /companies/available`
4. se resuelve `activeCompanyId` persistido o auto-selección
Si la restauración falla:
- se limpia la sesión
- se limpia `activeCompanyId`
## Selección de empresa
### Cero empresas
Si `companies.length === 0`:
- se muestra `/no-companies`
- no existe `activeCompanyId`
- no se envía `X-Company-Id`
- sigue disponible el logout
### Una empresa
Si `companies.length === 1`:
- se selecciona automáticamente
- se guarda `activeCompanyId`
- no se muestra `/company-selection`
- se continúa al área privada
### Varias empresas
Si `companies.length > 1`:
- se muestra `/company-selection`
- no existe `activeCompanyId` hasta elegir
- el usuario selecciona company
- se guarda `activeCompanyId`
- se continúa al área privada
### `activeCompanyId` persistido ya no disponible
Si el `activeCompanyId` persistido ya no aparece en `GET /companies/available`:
- con `0` companies -> `/no-companies`
- con `1` company -> auto-selección de la disponible
- con varias companies -> `/company-selection`
## `GET /companies/available`
Endpoint actual:
```http
GET /companies/available
Authorization: Bearer <access_token>
```
No requiere:
- `X-Company-Id`
No usa:
- `requireIdentityTenant(params)`
Devuelve:
```ts
export type AvailableCompaniesResponseDTO = {
companies: AvailableCompanyDTO[];
};
```
## Headers
### No deben llevar `X-Company-Id`
```txt
POST /identity/auth/login
POST /identity/auth/refresh
POST /identity/auth/logout
GET /identity/auth/session
GET /companies/available
```
### Sí deben llevar `X-Company-Id`
Rutas tenant-scoped como:
- `/customers`
- `/catalogs/*`
- `/customer-invoices/*`
- `/factuges`
siempre que exista `activeCompanyId`.
## Refresh automático sobre `401`
El frontend implementa refresh automático global sobre Axios.
Comportamiento:
1. una request autenticada recibe `401`
2. si existe `refreshToken`, se llama `POST /identity/auth/refresh`
3. se guardan los nuevos tokens
4. se reintenta una única vez la request original
5. si refresh falla, se limpia la sesión y se redirige a `/login`
## Reglas implementadas
- retry único por request con `_retry`
- refresh concurrente único con `refreshPromise` compartida
- varias requests `401` esperan el mismo refresh
- login `401` no dispara refresh
- refresh `401` no intenta refrescarse a sí mismo
- `/identity/auth/refresh` no lleva `X-Company-Id`
- la request tenant-scoped reintentada conserva `X-Company-Id`
## Endpoints excluidos de tenant header y refresh
```txt
/identity/auth/login
/identity/auth/refresh
/identity/auth/logout
/identity/auth/session
/companies/available
```