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