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:
|
||||
|
||||
```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`.
|
||||
|
||||
@ -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.
|
||||
|
||||
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
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
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:
|
||||
-- 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;
|
||||
|
||||
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