Doc
This commit is contained in:
parent
86bf1870ef
commit
b1e9f4bd2d
100
docs/README.md
100
docs/README.md
@ -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:
|
```txt
|
||||||
|
docs/
|
||||||
```sh
|
README.md
|
||||||
npx create-turbo@latest
|
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
|
- El backend nuevo usa `modules/identity` para autenticación, sesión y control de acceso a companies.
|
||||||
- `web`: another [Next.js](https://nextjs.org/) app
|
- `modules/companies` es la fuente de verdad de la ficha operativa de empresa.
|
||||||
- `@repo/shadcn-ui`: a stub React component library shared by both `web` and `docs` applications
|
- El frontend ERP ya usa `@erp/identity/client` como runtime de autenticación.
|
||||||
- `@repo/eslint-config`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`)
|
- El flujo actual resuelve sesión, companies disponibles, auto-selección de empresa única y refresh automático sobre `401`.
|
||||||
- `@repo/typescript-config`: `tsconfig.json`s used throughout the monorepo
|
|
||||||
|
|
||||||
Each package/app is 100% [TypeScript](https://www.typescriptlang.org/).
|
## Reglas de lectura rápida
|
||||||
|
|
||||||
### Utilities
|
- `modules/auth` debe considerarse legacy/deprecated para backend nuevo.
|
||||||
|
- `GET /companies/available` es el endpoint vigente para cargar companies accesibles.
|
||||||
This Turborepo has some additional tools already setup for you:
|
- `GET /identity/companies` no es un endpoint válido del diseño actual.
|
||||||
|
- Las rutas tenant-scoped deben usar `Authorization` + `X-Company-Id`.
|
||||||
- [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)
|
|
||||||
|
|||||||
@ -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
|
- `identity` se encarga de autenticación, sesión, refresh y memberships.
|
||||||
- sesión
|
- `companies` se encarga de la ficha operativa de empresa.
|
||||||
- contexto tenant/company
|
- `GET /companies/available` es la vía actual para listar companies accesibles.
|
||||||
- registro de usuarios
|
- El contexto tenant se resuelve con `X-Company-Id` y `requireIdentityTenant(params)`.
|
||||||
- roles y permisos
|
- El frontend ERP ya usa el runtime nuevo de `identity` con auto-selección de empresa y refresh automático.
|
||||||
- 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)
|
|
||||||
|
|||||||
175
docs/architecture/identity-and-companies.md
Normal file
175
docs/architecture/identity-and-companies.md
Normal 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
|
||||||
|
```
|
||||||
@ -1,12 +1,13 @@
|
|||||||
# Identity Auth API
|
# Identity Auth API
|
||||||
|
|
||||||
## Endpoints disponibles
|
## Endpoints activos
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /identity/auth/login
|
POST /identity/auth/login
|
||||||
POST /identity/auth/refresh
|
POST /identity/auth/refresh
|
||||||
POST /identity/auth/logout
|
POST /identity/auth/logout
|
||||||
GET /identity/auth/session
|
GET /identity/auth/session
|
||||||
|
GET /companies/available
|
||||||
```
|
```
|
||||||
|
|
||||||
## POST `/identity/auth/login`
|
## POST `/identity/auth/login`
|
||||||
@ -42,11 +43,11 @@ export type AuthenticatedAccountDTO = {
|
|||||||
|
|
||||||
### Notas
|
### Notas
|
||||||
|
|
||||||
- No requiere `Authorization`.
|
- no requiere `Authorization`
|
||||||
- No requiere `X-Company-Id`.
|
- no requiere `X-Company-Id`
|
||||||
- No devuelve empresas accesibles todavía.
|
- no devuelve companies accesibles
|
||||||
- No devuelve roles ni permisos.
|
- no devuelve roles ni permisos
|
||||||
- El access token no contiene `companyId`.
|
- el access token no contiene `companyId`
|
||||||
|
|
||||||
## POST `/identity/auth/refresh`
|
## POST `/identity/auth/refresh`
|
||||||
|
|
||||||
@ -69,9 +70,9 @@ export type RefreshSessionResponseDTO = {
|
|||||||
|
|
||||||
### Notas
|
### Notas
|
||||||
|
|
||||||
- El refresh token se rota.
|
- el refresh token se rota
|
||||||
- El cliente debe reemplazar ambos tokens por los nuevos.
|
- el cliente debe reemplazar ambos tokens por los nuevos
|
||||||
- El refresh token anterior no debe reutilizarse tras una renovación correcta.
|
- no debe llevar `X-Company-Id`
|
||||||
|
|
||||||
## POST `/identity/auth/logout`
|
## POST `/identity/auth/logout`
|
||||||
|
|
||||||
@ -100,9 +101,9 @@ export type LogoutResponseDTO = {
|
|||||||
|
|
||||||
### Reglas
|
### Reglas
|
||||||
|
|
||||||
- Para logout normal, enviar el `refresh_token` actual.
|
- no debe llevar `X-Company-Id`
|
||||||
- Para cerrar todas las sesiones, enviar `all_sessions: true`.
|
- para logout normal, enviar el `refresh_token` actual
|
||||||
- Si logout falla por expiración de token, el frontend debe limpiar sesión igualmente.
|
- si logout falla por expiración de token, el frontend debe limpiar sesión igualmente
|
||||||
|
|
||||||
## GET `/identity/auth/session`
|
## GET `/identity/auth/session`
|
||||||
|
|
||||||
@ -120,24 +121,50 @@ export type CurrentSessionResponseDTO = {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
## Errores esperados
|
### Notas
|
||||||
|
|
||||||
```txt
|
- no debe llevar `X-Company-Id`
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
## Rutas tenant-scoped
|
## GET `/companies/available`
|
||||||
|
|
||||||
Las rutas de negocio tenant-scoped deben enviar:
|
### Headers
|
||||||
|
|
||||||
```http
|
```http
|
||||||
Authorization: Bearer <access_token>
|
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
|
||||||
|
```
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
`identity` sustituye progresivamente al módulo legacy `auth`.
|
`identity` sustituye progresivamente al módulo legacy `auth`.
|
||||||
|
|
||||||
Responsabilidades previstas:
|
Responsabilidades actuales:
|
||||||
|
|
||||||
- cuentas autenticables
|
- cuentas autenticables
|
||||||
- login/password
|
- login/password
|
||||||
@ -12,48 +12,22 @@ Responsabilidades previstas:
|
|||||||
- refresh token rotado
|
- refresh token rotado
|
||||||
- logout
|
- logout
|
||||||
- sesión actual
|
- sesión actual
|
||||||
- empresas accesibles
|
|
||||||
- memberships
|
- memberships
|
||||||
- roles
|
- validación de acceso account-company
|
||||||
- permisos
|
- base para roles/permisos futuros
|
||||||
- autorización futura
|
|
||||||
|
|
||||||
## Dominio mínimo creado
|
## Dominio actual
|
||||||
|
|
||||||
Agregados principales:
|
Agregados y conceptos relevantes:
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
Account
|
Account
|
||||||
Company
|
RefreshToken
|
||||||
CompanyMembership
|
CompanyMembership
|
||||||
Role
|
Role
|
||||||
RefreshToken
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Value Objects reutilizados
|
`Company` funcional no pertenece a `identity`; vive en `modules/companies`.
|
||||||
|
|
||||||
Desde `@repo/rdx-ddd`:
|
|
||||||
|
|
||||||
```txt
|
|
||||||
UniqueID
|
|
||||||
LanguageCode
|
|
||||||
Name
|
|
||||||
TextValue
|
|
||||||
UtcDate
|
|
||||||
EmailAddress
|
|
||||||
URLAddress
|
|
||||||
```
|
|
||||||
|
|
||||||
VOs específicos de `identity`:
|
|
||||||
|
|
||||||
```txt
|
|
||||||
PasswordHash
|
|
||||||
RefreshTokenHash
|
|
||||||
RoleCode
|
|
||||||
AccountStatus
|
|
||||||
CompanyStatus
|
|
||||||
CompanyMembershipStatus
|
|
||||||
```
|
|
||||||
|
|
||||||
## Seguridad
|
## Seguridad
|
||||||
|
|
||||||
@ -67,81 +41,40 @@ IRefreshTokenGenerator
|
|||||||
IRefreshTokenHasher
|
IRefreshTokenHasher
|
||||||
```
|
```
|
||||||
|
|
||||||
Implementaciones actuales:
|
Decisiones vigentes:
|
||||||
|
|
||||||
```txt
|
- password hashing con `bcrypt`
|
||||||
BcryptPasswordHasher
|
- access token JWT con `accountId` y `email`
|
||||||
JsonWebTokenAccessTokenIssuer
|
- refresh token aleatorio
|
||||||
JsonWebTokenAccessTokenVerifier
|
- refresh token persistido como hash
|
||||||
CryptoRefreshTokenGenerator
|
- no incluir `companyId`, `companySlug`, roles ni permisos en access token
|
||||||
Sha256RefreshTokenHasher
|
|
||||||
```
|
|
||||||
|
|
||||||
Decisiones:
|
## Builders públicos
|
||||||
|
|
||||||
- password hashing con `bcrypt` en V1.
|
`identity` expone desde `@erp/identity/api`:
|
||||||
- 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:
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
requireIdentityAuthenticated(params: StartParams): RequestHandler[];
|
requireIdentityAuthenticated(params: StartParams): RequestHandler[];
|
||||||
requireIdentityTenant(params: StartParams): RequestHandler[];
|
requireIdentityTenant(params: StartParams): RequestHandler[];
|
||||||
```
|
```
|
||||||
|
|
||||||
## Uso correcto en routers
|
Uso correcto:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { requireIdentityTenant } from "@erp/identity/api";
|
import { requireIdentityTenant } from "@erp/identity/api";
|
||||||
|
|
||||||
export function buildSomeRoutes(params: StartParams) {
|
router.use(...requireIdentityTenant(params));
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
router.use(...requireIdentityTenant(params));
|
|
||||||
|
|
||||||
return router;
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Shape actual de `req.user`
|
```ts
|
||||||
|
import { requireIdentityAuthenticated } from "@erp/identity/api";
|
||||||
|
|
||||||
|
router.use(...requireIdentityAuthenticated(params));
|
||||||
|
```
|
||||||
|
|
||||||
|
## `req.user`
|
||||||
|
|
||||||
|
Shape mínimo esperado:
|
||||||
|
|
||||||
```ts
|
```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 `[]`.
|
No añadir:
|
||||||
- `companyId` viene de `X-Company-Id`.
|
|
||||||
- `companySlug` no debe depender de auth.
|
- `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
|
## Prohibiciones de diseño
|
||||||
|
|
||||||
@ -168,54 +125,11 @@ authenticateUserDependencies;
|
|||||||
getInternal("identity");
|
getInternal("identity");
|
||||||
```
|
```
|
||||||
|
|
||||||
No crear:
|
No crear helpers locales de auth por módulo como patrón estable.
|
||||||
|
|
||||||
```txt
|
No añadir al access token:
|
||||||
<module>-auth-middlewares.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
como patrón estable.
|
- `companyId`
|
||||||
|
- `companySlug`
|
||||||
No añadir a access token:
|
- roles
|
||||||
|
- permisos
|
||||||
```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`.
|
|
||||||
|
|||||||
@ -20,130 +20,74 @@ router.use(...requireIdentityAuthenticated(params));
|
|||||||
|
|
||||||
## Migrado a `identity`
|
## Migrado a `identity`
|
||||||
|
|
||||||
### `modules/catalogs`
|
### Backend tenant-scoped
|
||||||
|
|
||||||
Routers migrados:
|
Módulos migrados:
|
||||||
|
|
||||||
```txt
|
- `modules/catalogs`
|
||||||
payment-methods.routes.ts
|
- `modules/customers`
|
||||||
payment-terms.routes.ts
|
- `modules/customer-invoices`
|
||||||
tax-regimes.routes.ts
|
- `modules/factuges`
|
||||||
tax-definitions.routes.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
Estado:
|
Estado:
|
||||||
|
|
||||||
```txt
|
- sin `@erp/auth/api`
|
||||||
sin @erp/auth/api
|
- sin `mockUser`
|
||||||
sin mockUser
|
- sin helpers locales de auth
|
||||||
sin helpers locales de auth
|
- usan `requireIdentityTenant(params)`
|
||||||
usa 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:
|
```http
|
||||||
|
POST /companies
|
||||||
```txt
|
GET /companies/available
|
||||||
customers.routes.ts
|
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
|
```txt
|
||||||
sin @erp/auth/api
|
login
|
||||||
sin mockUser
|
-> GET /identity/auth/session
|
||||||
sin helpers locales de auth
|
-> GET /companies/available
|
||||||
usa requireIdentityTenant(params)
|
-> 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
|
Sigue pendiente revisar o retirar consumidores legacy residuales antes de eliminarlo por completo.
|
||||||
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
|
|
||||||
|
|
||||||
### `modules/supplier`
|
### `modules/supplier`
|
||||||
|
|
||||||
Sigue usando:
|
Queda fuera de esta migración por ahora.
|
||||||
|
|
||||||
```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.
|
|
||||||
|
|
||||||
## Búsquedas útiles de control
|
## 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 "@erp/auth/api" modules apps packages
|
||||||
rg -n -F "mockUser" modules apps packages
|
rg -n -F "mockUser" modules apps packages
|
||||||
rg -n -F "IdentityInternalDeps" 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.
|
|
||||||
|
|||||||
@ -2,216 +2,57 @@
|
|||||||
|
|
||||||
## Objetivo general
|
## 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.
|
Ya está implementado:
|
||||||
|
|
||||||
Incluye:
|
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
login
|
login
|
||||||
refresh rotado
|
refresh rotado
|
||||||
logout
|
logout
|
||||||
session
|
session
|
||||||
access token verifier
|
GET /companies/available
|
||||||
refresh token persistence
|
selección de empresa en frontend
|
||||||
middlewares genéricos
|
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
|
### Roles y permisos
|
||||||
- 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.
|
|
||||||
|
|
||||||
Pendiente:
|
Pendiente:
|
||||||
|
|
||||||
```txt
|
- resolución de permisos efectivos
|
||||||
GET /identity/companies
|
- middleware de autorización fina
|
||||||
GET /identity/companies/current
|
- endpoints funcionales de roles/permisos si se activan
|
||||||
PATCH /identity/companies/current
|
|
||||||
```
|
|
||||||
|
|
||||||
Frontend deberá:
|
### Migración legacy restante
|
||||||
|
|
||||||
```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.
|
|
||||||
|
|
||||||
Pendiente:
|
Pendiente:
|
||||||
|
|
||||||
```txt
|
- revisar consumidores legacy que sigan atados a `modules/auth`
|
||||||
persistencia Company
|
- migrar o retirar `supplier` cuando se decida
|
||||||
persistencia CompanyMembership
|
- retirar `@erp/auth/client` si aún queda compatibilidad heredada
|
||||||
validación accountId + companyId
|
|
||||||
bloqueo si membership disabled/invited
|
|
||||||
```
|
|
||||||
|
|
||||||
No validar membership en access token.
|
## Decisiones que se mantienen
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
No hacer:
|
No hacer:
|
||||||
|
|
||||||
```txt
|
- meter `companyId` en access token
|
||||||
- meter companyId en access token
|
|
||||||
- meter roles/permisos en access token
|
- meter roles/permisos en access token
|
||||||
- meter companySlug en access token
|
- meter `companySlug` en access token
|
||||||
- poblar companySlug desde auth middleware
|
- documentar `GET /identity/companies` como endpoint vigente
|
||||||
- crear helpers auth por módulo
|
- poblar `companySlug` desde auth middleware
|
||||||
- usar getInternal("identity") desde módulos consumidores
|
- usar `getInternal("identity")` desde módulos consumidores
|
||||||
- usar IdentityInternalDeps desde módulos consumidores
|
- usar `IdentityInternalDeps` desde módulos consumidores
|
||||||
```
|
|
||||||
|
|
||||||
Sí hacer:
|
Sí hacer:
|
||||||
|
|
||||||
```txt
|
- usar `requireIdentityTenant(params)` en rutas tenant-scoped
|
||||||
- usar requireIdentityTenant(params) en rutas tenant-scoped
|
- usar `requireIdentityAuthenticated(params)` en rutas solo autenticadas
|
||||||
- usar requireIdentityAuthenticated(params) en rutas solo autenticadas
|
- usar `GET /companies/available` para companies accesibles en frontend
|
||||||
- resolver datos documentales desde servicios específicos
|
- resolver datos documentales desde `companies` u otros servicios específicos
|
||||||
- validar membership en backend cuando exista persistencia real
|
|
||||||
```
|
|
||||||
|
|||||||
115
docs/architecture/tenant-context.md
Normal file
115
docs/architecture/tenant-context.md
Normal 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
|
||||||
|
```
|
||||||
@ -12,59 +12,28 @@
|
|||||||
--
|
--
|
||||||
-- Credentials:
|
-- Credentials:
|
||||||
-- email: admin@local.test
|
-- email: admin@local.test
|
||||||
-- password: Passw0rd123
|
-- password: Admin123!
|
||||||
--
|
|
||||||
-- Important:
|
|
||||||
-- Replace <BCRYPT_HASH_GENERADO> before running this script.
|
|
||||||
--
|
--
|
||||||
-- Generate bcrypt hash from the workspace:
|
-- Generate bcrypt hash from the workspace:
|
||||||
--
|
--
|
||||||
-- node -e "const bcrypt=require('bcrypt'); bcrypt.hash('Admin123!', 10).then(console.log)"
|
-- 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:
|
-- This script is idempotent:
|
||||||
-- - It does not duplicate the admin account if the email exists.
|
-- - 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 company if the slug exists.
|
||||||
-- - It does not duplicate the membership if it already 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 @admin_id = UUID();
|
||||||
|
SET @company_id = UUID();
|
||||||
|
|
||||||
SET @admin_email = 'admin@local.test';
|
SET @admin_email = 'admin@local.test';
|
||||||
|
SET @admin_password_hash = '$2b$10$6m6Nh2OpDy9MlQF18KOueOzLSCHybcg8yu1JyG0XxjgxB5Qx2dMdG';
|
||||||
|
|
||||||
SET
|
SET @company_slug = 'rodax';
|
||||||
@admin_password_hash = '$2a$10$qgmmm34tJug4HydlKwcZxOVA5u5zoDTLE5lkH//sp55tl5au2wNQm';
|
SET @company_legal_name = 'Rodax Software S.L.';
|
||||||
|
SET @company_trade_name = 'Rodax';
|
||||||
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
|
|
||||||
-- ------------------------------------------------------------
|
|
||||||
|
|
||||||
INSERT INTO
|
INSERT INTO
|
||||||
identity_accounts (
|
identity_accounts (
|
||||||
@ -96,17 +65,12 @@ WHERE
|
|||||||
email = @admin_email
|
email = @admin_email
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Recover the real account id if the account already existed.
|
|
||||||
SELECT id INTO @admin_id
|
SELECT id INTO @admin_id
|
||||||
FROM identity_accounts
|
FROM identity_accounts
|
||||||
WHERE
|
WHERE
|
||||||
email = @admin_email
|
email = @admin_email
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
|
|
||||||
-- ------------------------------------------------------------
|
|
||||||
-- Company
|
|
||||||
-- ------------------------------------------------------------
|
|
||||||
|
|
||||||
INSERT INTO
|
INSERT INTO
|
||||||
companies (
|
companies (
|
||||||
id,
|
id,
|
||||||
@ -141,17 +105,12 @@ WHERE
|
|||||||
slug = @company_slug
|
slug = @company_slug
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Recover the real company id if the company already existed.
|
|
||||||
SELECT id INTO @company_id
|
SELECT id INTO @company_id
|
||||||
FROM companies
|
FROM companies
|
||||||
WHERE
|
WHERE
|
||||||
slug = @company_slug
|
slug = @company_slug
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
|
|
||||||
-- ------------------------------------------------------------
|
|
||||||
-- Account-company membership
|
|
||||||
-- ------------------------------------------------------------
|
|
||||||
|
|
||||||
INSERT INTO
|
INSERT INTO
|
||||||
identity_company_memberships (
|
identity_company_memberships (
|
||||||
id,
|
id,
|
||||||
@ -171,21 +130,6 @@ WHERE
|
|||||||
AND company_id = @company_id
|
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
|
SELECT
|
||||||
a.id AS account_id,
|
a.id AS account_id,
|
||||||
a.email,
|
a.email,
|
||||||
|
|||||||
174
docs/frontend/auth-and-company-selection.md
Normal file
174
docs/frontend/auth-and-company-selection.md
Normal 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
|
||||||
|
```
|
||||||
Loading…
Reference in New Issue
Block a user