feat: plantilla base workspace SaaS
This commit is contained in:
599
docs/ARCHITECTURE.md
Normal file
599
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,599 @@
|
||||
# Anexo76 - Resumen de Arquitectura Técnica
|
||||
|
||||
## 📋 Índice
|
||||
1. [Visión General](#visión-general)
|
||||
2. [Stack Tecnológico](#stack-tecnológico)
|
||||
3. [Arquitectura del Sistema](#arquitectura-del-sistema)
|
||||
4. [Arquitectura de Schemas y Módulos](#arquitectura-de-schemas-y-módulos)
|
||||
5. [Estructura del Proyecto](#estructura-del-proyecto)
|
||||
6. [Flujos Principales](#flujos-principales)
|
||||
7. [Seguridad](#seguridad)
|
||||
8. [Base de Datos](#base-de-datos)
|
||||
9. [API Reference](#api-reference)
|
||||
|
||||
---
|
||||
|
||||
## Visión General
|
||||
|
||||
Anexo76 es una aplicación SaaS multi-tenant para gestión de comercio exterior en México, enfocada en cumplir con los Anexos 24, 31 y 22 del SAT.
|
||||
|
||||
### Objetivos de Negocio
|
||||
- Gestión de inventarios para maquilas e IMMEX
|
||||
- Control de pedimentos aduanales
|
||||
- Manejo de facturas de importación/exportación
|
||||
- Cumplimiento normativo SAT
|
||||
- Licenciamiento flexible por planes
|
||||
|
||||
---
|
||||
|
||||
## Stack Tecnológico
|
||||
|
||||
### Backend
|
||||
- **Framework**: FastAPI 0.110+ (Python 3.11+)
|
||||
- **ORM**: SQLAlchemy 2.0
|
||||
- **Autenticación**: Keycloak (OpenID Connect)
|
||||
- **Base de Datos**: PostgreSQL 15+
|
||||
- **Validación**: Pydantic 2.6+
|
||||
- **Testing**: Pytest
|
||||
|
||||
### Frontend
|
||||
- **Framework**: SvelteKit 2.0+ (Svelte 5)
|
||||
- **Lenguaje**: TypeScript
|
||||
- **Auth Client**: keycloak-js
|
||||
- **Estilos**: TailwindCSS 4.1+
|
||||
- **Build**: Vite 7+
|
||||
|
||||
### Infraestructura
|
||||
- **Containerización**: Docker / Docker Compose
|
||||
- **Orquestación**: Kubernetes (futuro)
|
||||
- **CI/CD**: GitHub Actions / GitLab CI
|
||||
- **Monitoreo**: Prometheus + Grafana
|
||||
|
||||
---
|
||||
|
||||
## Arquitectura del Sistema
|
||||
|
||||
### Patrón Arquitectónico: Modular Layered (estilo NestJS)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ FRONTEND │
|
||||
│ SvelteKit + Keycloak-js + TailwindCSS │
|
||||
└────────────────┬────────────────────────────────────────┘
|
||||
│ HTTP/REST + JWT
|
||||
┌────────────────▼────────────────────────────────────────┐
|
||||
│ API GATEWAY (FastAPI) │
|
||||
│ Middleware: Tenant | License | Logging | CORS │
|
||||
└────────────────┬────────────────────────────────────────┘
|
||||
│
|
||||
┌────────┴────────┐
|
||||
│ │
|
||||
┌───────▼──────┐ ┌──────▼────────┐
|
||||
│ MODULES │ │ CORE LAYER │
|
||||
│ │ │ │
|
||||
│ • auth │ │ • config.py │
|
||||
│ • tenants │ │ • database.py │
|
||||
│ • licenses │ │ • security.py │
|
||||
│ • ... │ │ • middleware │
|
||||
└───────┬──────┘ └───────────────┘
|
||||
│
|
||||
┌───────▼──────────────────────────┐
|
||||
│ DATABASE LAYER (Multi-tenant) │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────────┐ │
|
||||
│ │ Core DB │ │ Tenant 1 DB │ │
|
||||
│ │ (shared) │ │ (dedicated) │ │
|
||||
│ └──────────┘ └──────────────┘ │
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Estructura Modular (por módulo)
|
||||
|
||||
Cada módulo sigue el patrón:
|
||||
|
||||
```
|
||||
modules/{module_name}/
|
||||
├── models.py # ORM Models (SQLAlchemy)
|
||||
├── dto.py # Data Transfer Objects (Pydantic)
|
||||
├── service.py # Business Logic Layer
|
||||
├── routes.py # API Endpoints (FastAPI)
|
||||
└── __init__.py # Module exports
|
||||
```
|
||||
|
||||
#### Responsabilidades por Capa
|
||||
|
||||
1. **models.py**: Representación de entidades en BD
|
||||
- Define tablas con SQLAlchemy
|
||||
- Relaciones entre entidades
|
||||
- Constraints y validaciones a nivel DB
|
||||
|
||||
2. **dto.py**: Contratos de entrada/salida de datos
|
||||
- DTOs de request (CreateDTO, UpdateDTO)
|
||||
- DTOs de response (ResponseDTO)
|
||||
- Validaciones de Pydantic
|
||||
|
||||
3. **service.py**: Lógica de negocio
|
||||
- Operaciones CRUD
|
||||
- Validaciones de negocio
|
||||
- Orquestación de operaciones complejas
|
||||
|
||||
4. **routes.py**: Exposición HTTP
|
||||
- Definición de endpoints
|
||||
- Documentación OpenAPI automática
|
||||
- Manejo de dependencias (auth, db)
|
||||
|
||||
---
|
||||
|
||||
## Arquitectura de Schemas y Módulos
|
||||
|
||||
### Estructura de Schemas en Base de Datos
|
||||
|
||||
La aplicación utiliza una arquitectura de schemas para organizar lógicamente las tablas según su funcionalidad y alcance:
|
||||
|
||||
#### **Schema `a24` (Anexo 24)**
|
||||
Contiene todas las tablas relacionadas con el **Anexo 24 del SAT** (control de inventarios para empresas IMMEX):
|
||||
- Gestión de inventarios
|
||||
- Control de entradas y salidas de mercancías
|
||||
- Reportes de existencias
|
||||
- Cumplimiento de obligaciones fiscales del Anexo 24
|
||||
|
||||
#### **Schema `a76` (Anexo 76)**
|
||||
Contiene todas las tablas relacionadas con el **Anexo 76 del SAT** (comercio exterior):
|
||||
- Pedimentos aduanales
|
||||
- Facturas de importación/exportación
|
||||
- Documentación de comercio exterior
|
||||
- Cumplimiento normativo de comercio exterior
|
||||
|
||||
#### **Schema `public` (Catálogos Fijos)**
|
||||
Contiene **catálogos compartidos** y datos de referencia que no cambian frecuentemente:
|
||||
- Catálogos del SAT (tipos de material, unidades de medida, etc.)
|
||||
- Códigos de país
|
||||
- Catálogos de aduanas
|
||||
- Tipos de documento
|
||||
- Datos maestros compartidos entre módulos
|
||||
|
||||
### Convención de Prefijos de Tablas
|
||||
|
||||
Para mantener claridad y trazabilidad, las tablas utilizan prefijos que identifican su módulo funcional:
|
||||
|
||||
#### **Prefijo `inv_` (Inventarios)**
|
||||
Tablas relacionadas con el **control de inventarios**:
|
||||
- `inv_products`: Productos en inventario
|
||||
- `inv_movements`: Movimientos de entrada/salida
|
||||
- `inv_warehouses`: Almacenes
|
||||
- `inv_balances`: Saldos de inventario
|
||||
|
||||
**Nota histórica**: Anteriormente se utilizaba el prefijo `s` (SCAII - Sistema de aduanas e Inventarios).
|
||||
|
||||
#### **Prefijo `fa_` (Fixed Assets / Activos Fijos)**
|
||||
Tablas relacionadas con la **gestión de activos fijos**:
|
||||
- `fa_assets`: Registro de activos fijos
|
||||
- `fa_depreciation`: Depreciación de activos
|
||||
- `fa_maintenance`: Mantenimiento de activos
|
||||
- `fa_transfers`: Transferencias de activos
|
||||
|
||||
**Nota histórica**: Anteriormente se utilizaba el prefijo `q` (SCAF - Sistema de Control de Activos Fijos).
|
||||
|
||||
### Diagrama de Arquitectura de Schemas
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ DATABASE: anexo76_db │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌────────────────┐ ┌────────────────┐ ┌───────────────┐ │
|
||||
│ │ Schema: a24 │ │ Schema: a76 │ │Schema: public │ │
|
||||
│ │ (Anexo 24) │ │ (Anexo 76) │ │ (Catálogos) │ │
|
||||
│ ├────────────────┤ ├────────────────┤ ├───────────────┤ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ inv_products │ │ pedimentos │ │ material_types│ │
|
||||
│ │ inv_movements │ │ facturas │ │ uom_codes │ │
|
||||
│ │ inv_warehouses │ │ customs_docs │ │ countries │ │
|
||||
│ │ inv_balances │ │ export_ops │ │ customs_list │ │
|
||||
│ │ │ │ │ │ document_types│ │
|
||||
│ │ fa_assets │ │ │ │ │ │
|
||||
│ │ fa_depreciation│ │ │ │ │ │
|
||||
│ │ fa_maintenance │ │ │ │ │ │
|
||||
│ │ fa_transfers │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ └────────────────┘ └────────────────┘ └───────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Ventajas de esta Arquitectura
|
||||
|
||||
1. **Separación Lógica**: Cada schema representa un dominio específico del negocio
|
||||
2. **Escalabilidad**: Facilita la adición de nuevos módulos sin afectar los existentes
|
||||
3. **Seguridad**: Permite aplicar permisos a nivel de schema
|
||||
4. **Mantenibilidad**: Código y migraciones organizados por dominio
|
||||
5. **Claridad**: Los prefijos hacen evidente la funcionalidad de cada tabla
|
||||
6. **Migración Gradual**: Permite actualizar sistemas legados (SCAII/SCAF) sin interrupciones
|
||||
|
||||
### Mapeo de Sistemas Legados
|
||||
|
||||
| Sistema Legacy | Prefijo Antiguo | Sistema Nuevo | Prefijo Nuevo | Schema |
|
||||
|----------------------|-----------------|-------------------|---------------|----------|
|
||||
| SCAII (Inventarios) | `s` | Inventarios | `inv_` | `a24` |
|
||||
| SCAF (Activos Fijos) | `q` | Fixed Assets | `fa_` | `a24` |
|
||||
| Winsaii (Pedimentos) | `w` | - | - | `a22` |
|
||||
| - | `g` | Comercio Exterior | - | `a76` |
|
||||
| - | `g` | Catálogos SAT | - | `public` |
|
||||
|
||||
---
|
||||
|
||||
## Estructura del Proyecto
|
||||
|
||||
```
|
||||
anexo76/
|
||||
├── backend/
|
||||
│ ├── main.py # Aplicación FastAPI principal
|
||||
│ ├── requirements.txt # Dependencias
|
||||
│ ├── init_db.py # Script de inicialización
|
||||
│ ├── Dockerfile
|
||||
│ │
|
||||
│ ├── core/ # Capa core (shared)
|
||||
│ │ ├── config.py # Configuración (Pydantic Settings)
|
||||
│ │ ├── database.py # Gestión de BD multi-tenant
|
||||
│ │ ├── security.py # Auth Keycloak + JWT
|
||||
│ │ ├── middleware.py # Middlewares personalizados
|
||||
│ │ └── __init__.py
|
||||
│ │
|
||||
│ └── api/
|
||||
│ └── v1/
|
||||
│ ├── router.py # Router principal v1
|
||||
│ ├── common/ # Utilidades compartidas
|
||||
│ │ ├── base_models.py
|
||||
│ │ ├── crud_routes.py
|
||||
│ │ ├── dto_mixins.py
|
||||
│ │ └── tenant_crud_routes.py
|
||||
│ │
|
||||
│ └── modules/ # Módulos de negocio por schema
|
||||
│ ├── a24/ # Módulo Anexo 24 (Inventarios)
|
||||
│ │ ├── inventarios/
|
||||
│ │ └── activos_fijos/
|
||||
│ │
|
||||
│ ├── a76/ # Módulo Anexo 76 (Comercio Exterior)
|
||||
│ │ ├── pedimentos/
|
||||
│ │ └── facturas/
|
||||
│ │
|
||||
│ └── public/ # Catálogos compartidos
|
||||
│ ├── material_types/
|
||||
│ ├── uom_codes/
|
||||
│ └── countries/
|
||||
│
|
||||
├── frontend/
|
||||
│ ├── src/
|
||||
│ │ ├── routes/ # Páginas SvelteKit
|
||||
│ │ │ ├── +layout.svelte # Layout global con Keycloak
|
||||
│ │ │ ├── +page.svelte # Dashboard principal
|
||||
│ │ │ └── callback/ # OAuth callback
|
||||
│ │ │
|
||||
│ │ └── lib/
|
||||
│ │ ├── auth.ts # Servicio de autenticación
|
||||
│ │ └── api.ts # Cliente API
|
||||
│ │
|
||||
│ ├── static/
|
||||
│ │ └── silent-check-sso.html
|
||||
│ ├── package.json
|
||||
│ └── Dockerfile
|
||||
│
|
||||
├── docs/
|
||||
│ ├── KEYCLOAK_SETUP.md # Guía de configuración
|
||||
│ └── ARCHITECTURE.md # Este documento
|
||||
│
|
||||
├── docker-compose.yml # Orquestación completa
|
||||
├── start.sh # Script de inicio rápido
|
||||
├── README.md # Documentación principal
|
||||
└── .gitignore
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Flujos Principales
|
||||
|
||||
### 1. Flujo de Autenticación
|
||||
|
||||
```
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ Frontend │ │ Keycloak │ │ Backend │
|
||||
└────┬─────┘ └────┬─────┘ └────┬─────┘
|
||||
│ │ │
|
||||
│ 1. Clic "Login" │ │
|
||||
├────────────────────────────>│ │
|
||||
│ │ │
|
||||
│ 2. Formulario de login │ │
|
||||
│<────────────────────────────┤ │
|
||||
│ │ │
|
||||
│ 3. Credenciales │ │
|
||||
├────────────────────────────>│ │
|
||||
│ │ │
|
||||
│ 4. Redirigir + auth code │ │
|
||||
│<────────────────────────────┤ │
|
||||
│ │ │
|
||||
│ 5. Intercambiar code x token│ │
|
||||
├────────────────────────────>│ │
|
||||
│ │ │
|
||||
│ 6. JWT (access + refresh) │ │
|
||||
│<────────────────────────────┤ │
|
||||
│ │ │
|
||||
│ 7. Request con Bearer token │ │
|
||||
├─────────────────────────────┼──────────────────────────>│
|
||||
│ │ │
|
||||
│ │ 8. Validar token │
|
||||
│ │<──────────────────────────┤
|
||||
│ │ │
|
||||
│ │ 9. Public key │
|
||||
│ ├──────────────────────────>│
|
||||
│ │ │
|
||||
│ 10. Respuesta con datos │ │
|
||||
│<─────────────────────────────┼───────────────────────────┤
|
||||
│ │ │
|
||||
```
|
||||
|
||||
### 2. Flujo de Request Multi-tenant
|
||||
|
||||
```
|
||||
Request con JWT
|
||||
↓
|
||||
TenantMiddleware
|
||||
├─ Extrae tenant_id del token
|
||||
├─ Valida tenant existe y está activo
|
||||
└─ Agrega tenant_id a request.state
|
||||
↓
|
||||
LicenseValidationMiddleware
|
||||
├─ Consulta licencia del tenant
|
||||
├─ Valida estado (active/expired)
|
||||
├─ Valida fecha de vigencia
|
||||
└─ Agrega license_info a request.state
|
||||
↓
|
||||
Endpoint Handler
|
||||
├─ Obtiene tenant_id de request.state
|
||||
├─ Selecciona BD (shared o dedicated)
|
||||
└─ Procesa request
|
||||
↓
|
||||
Response
|
||||
```
|
||||
|
||||
### 3. Flujo de Selección de Base de Datos
|
||||
|
||||
```python
|
||||
# Pseudocódigo
|
||||
tenant_id = request.state.tenant_id
|
||||
|
||||
tenant = db.query(Tenant).filter(Tenant.id == tenant_id).first()
|
||||
|
||||
if tenant.type == "SHARED":
|
||||
# Usar BD compartida (core_db)
|
||||
db_session = CoreSessionLocal()
|
||||
# Queries incluyen tenant_id en WHERE
|
||||
|
||||
elif tenant.type == "DEDICATED":
|
||||
# Usar BD dedicada del tenant
|
||||
db_config = json.loads(tenant.db_config)
|
||||
db_session = get_tenant_db(tenant_id, db_config)
|
||||
# No necesita filtrar por tenant_id
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Seguridad
|
||||
|
||||
### Autenticación
|
||||
- **Keycloak** como Identity Provider
|
||||
- **OpenID Connect** (OIDC)
|
||||
- **JWT** con RS256 (firma asimétrica)
|
||||
- **Refresh tokens** para renovación
|
||||
|
||||
### Autorización
|
||||
- **RBAC** (Role-Based Access Control)
|
||||
- Roles: `admin`, `user`, `auditor`, `system`
|
||||
- Middleware `has_role()` para proteger endpoints
|
||||
|
||||
### Multi-tenancy
|
||||
- **Aislamiento por tenant_id** en JWT
|
||||
- **Row-level security** en BD compartida
|
||||
- **BD dedicada** para mayor aislamiento (enterprise)
|
||||
|
||||
### Row-Level Security (RLS) en BD compartida
|
||||
|
||||
> Convención alineada al skill `aduanasoft-dev-standards` (sección 10).
|
||||
> La capa API sigue siendo responsable del control fino (roles/permisos
|
||||
> con Keycloak + `PermissionService`); RLS añade **defensa en profundidad**
|
||||
> a nivel de BD para que un bug en un `WHERE` no permita salirse del tenant.
|
||||
|
||||
#### Variables de sesión (`SET LOCAL`)
|
||||
|
||||
| GUC | Origen | Comportamiento RLS |
|
||||
|-----|--------|--------------------|
|
||||
| `app.tenant_id` | JWT (`TenantMiddleware`) → `request.state.tenant_id` | Obligatoria. Si está vacía, `app.current_tenant_id()` retorna `NULL` y las políticas devuelven `0` filas (fail-closed). |
|
||||
| `app.company_id` | Header `X-Company-Id` o cookie `active_company_id` | Opcional. Si está vacía, el tenant ve **todas sus compañías** (útil para selectores de compañía y bootstrap). |
|
||||
|
||||
Ambas se fijan con `SET LOCAL` al inicio de cada transacción —
|
||||
**nunca** con `SET` global, para no contaminar conexiones del pool.
|
||||
|
||||
Helpers SQL definidos por la migración `d1a2b3c4e5f6_enable_rls_tenant_company`:
|
||||
|
||||
```sql
|
||||
CREATE FUNCTION app.current_tenant_id() RETURNS INTEGER LANGUAGE sql STABLE AS
|
||||
$$ SELECT NULLIF(current_setting('app.tenant_id', true), '')::INTEGER $$;
|
||||
CREATE FUNCTION app.current_company_id() RETURNS INTEGER LANGUAGE sql STABLE AS
|
||||
$$ SELECT NULLIF(current_setting('app.company_id', true), '')::INTEGER $$;
|
||||
```
|
||||
|
||||
#### Tipos de política
|
||||
|
||||
1. **Solo `tenant_id`** (p.ej. `a76.company`, `core.licenses`):
|
||||
`tenant_id = app.current_tenant_id()`.
|
||||
2. **`tenant_id` + `company_id`** (`TenantScopedMixin`, mayoría de tablas
|
||||
`a24/`a76/`core`): además exige `company_id = app.current_company_id()`
|
||||
cuando esa GUC está fijada.
|
||||
3. **Solo `company_id`** (algunas tablas `a76.company_*`): valida el
|
||||
`tenant_id` indirectamente vía `EXISTS` contra `a76.company`.
|
||||
|
||||
Todas las tablas usan `FORCE ROW LEVEL SECURITY` para que la política
|
||||
aplique también al owner. Las únicas tablas core **excluidas** son
|
||||
`core.tenants` y `core.user_tenants` — necesarias para el bootstrap del
|
||||
selector de tenant antes de tener contexto fijado.
|
||||
|
||||
#### Propagación del contexto
|
||||
|
||||
| Camino | Cómo se fija el contexto |
|
||||
|--------|--------------------------|
|
||||
| HTTP request | `TenantMiddleware` rellena `request.state.tenant_id`/`company_id`; `get_core_db` / `get_async_core_db` leen esos valores y los guardan en `Session.info`. Un listener `after_begin` ejecuta `SET LOCAL` por transacción. |
|
||||
| `LicenseValidationMiddleware` | Usa `scoped_core_db(tenant_id=...)` para que la consulta de licencia entre con contexto RLS válido. |
|
||||
| Tareas Celery | `track_and_dispatch` inyecta `rls_tenant_id` / `rls_company_id` en los headers del task; los signals `task_prerun`/`task_postrun` los copian a `ContextVar`s del worker, que el listener `after_begin` consume como fallback. Tareas críticas (imports/exports de invoices, expediente) abren la sesión con `scoped_core_db(tenant_id=..., company_id=...)`. |
|
||||
| Tests | Las suites de pytest pueden usar `scoped_core_db(...)` o emular el flujo con `set_config('app.tenant_id', ...)` antes del query. Hay un set de tests en `backend/tests/integration/test_rls_tenant_company.py` que valida aislamiento A vs B usando un rol sin `BYPASSRLS`. |
|
||||
|
||||
#### Reparto de responsabilidades
|
||||
|
||||
| Capa | Decide |
|
||||
|------|--------|
|
||||
| **API (FastAPI + Keycloak + `PermissionService`)** | Roles, permisos por compañía, accesos a recursos concretos (`validate_access_to_resource`), reglas de negocio. |
|
||||
| **RLS (PostgreSQL)** | Límite estructural duro: `tenant_id` y `company_id`. **No** modela roles/permisos para evitar duplicar lógica fina con la API. |
|
||||
|
||||
#### Operación / DevOps
|
||||
|
||||
- En **producción** la API debe conectar con un rol **sin** `BYPASSRLS`
|
||||
(`postgres` superusuario lo bypassea por diseño). El `docker-compose.yml`
|
||||
de desarrollo usa `postgres` deliberadamente para no romper migraciones;
|
||||
los tests crean un rol `anexo76_rls_test` para ejercitar las políticas.
|
||||
- Los jobs/ETL/migraciones que necesiten ver todos los tenants deben usar
|
||||
un rol técnico explícito con `BYPASSRLS` o fijar `app.tenant_id` por
|
||||
iteración — nunca asumir que la sesión global "ve todo".
|
||||
- La migración `d1a2b3c4e5f6_enable_rls_tenant_company` tiene `downgrade()`
|
||||
completo (drop policies + `DISABLE ROW LEVEL SECURITY`) para revertir.
|
||||
|
||||
### Validación de Licencias
|
||||
- Middleware verifica en cada request:
|
||||
- ✓ Licencia activa
|
||||
- ✓ No expirada
|
||||
- ✓ Límites no excedidos
|
||||
|
||||
---
|
||||
|
||||
## Base de Datos
|
||||
|
||||
### Modelo Híbrido Multi-tenant
|
||||
|
||||
#### BD Core (Compartida)
|
||||
Tablas principales:
|
||||
- `tenants`: Información de clientes
|
||||
- `licenses`: Control de licencias por tenant
|
||||
- `license_usage`: Métricas de uso
|
||||
- `users` (futuro): Usuarios por tenant
|
||||
|
||||
Todas las tablas operacionales incluyen `tenant_id` para segmentación.
|
||||
|
||||
#### BD Dedicadas (Enterprise)
|
||||
- Una BD PostgreSQL por tenant
|
||||
- Configuración almacenada en `tenants.db_config`
|
||||
- Migración automática desde BD compartida
|
||||
|
||||
### Ejemplo de Tabla Multi-tenant
|
||||
|
||||
```sql
|
||||
CREATE TABLE inventories (
|
||||
id SERIAL PRIMARY KEY,
|
||||
tenant_id INTEGER NOT NULL REFERENCES tenants(id),
|
||||
product_code VARCHAR(50) NOT NULL,
|
||||
quantity INTEGER NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
|
||||
-- Índice compuesto para queries eficientes
|
||||
INDEX idx_tenant_product (tenant_id, product_code)
|
||||
);
|
||||
```
|
||||
|
||||
### Migración y Upgrade
|
||||
|
||||
```python
|
||||
# Tenant en BD compartida → BD dedicada
|
||||
tenant_service.upgrade_to_dedicated(
|
||||
tenant_id=123,
|
||||
db_config={
|
||||
"host": "dedicated-postgres.example.com",
|
||||
"port": 5432,
|
||||
"name": "tenant_123_db",
|
||||
"user": "tenant_123_user",
|
||||
"password": "secure_password"
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### Módulo: Authentication (`/v1/auth`)
|
||||
|
||||
| Endpoint | Método | Descripción | Auth |
|
||||
|----------|--------|-------------|------|
|
||||
| `/auth/login` | POST | Login con Keycloak | Público |
|
||||
| `/auth/refresh` | POST | Renovar access token | Público |
|
||||
| `/auth/me` | GET | Info del usuario actual | Bearer |
|
||||
| `/auth/logout` | POST | Cerrar sesión | Bearer |
|
||||
| `/auth/health` | GET | Health check | Público |
|
||||
|
||||
### Módulo: Tenants (`/v1/tenants`)
|
||||
|
||||
| Endpoint | Método | Descripción | Rol Requerido |
|
||||
|----------|--------|-------------|---------------|
|
||||
| `/tenants` | POST | Crear tenant | admin |
|
||||
| `/tenants` | GET | Listar tenants | admin |
|
||||
| `/tenants/{id}` | GET | Obtener tenant | user |
|
||||
| `/tenants/{id}` | PUT | Actualizar tenant | admin |
|
||||
| `/tenants/{id}` | DELETE | Eliminar tenant | admin |
|
||||
| `/tenants/slug/{slug}` | GET | Obtener por slug | user |
|
||||
|
||||
### Módulo: Licenses (`/v1/licenses`)
|
||||
|
||||
| Endpoint | Método | Descripción | Rol Requerido |
|
||||
|----------|--------|-------------|---------------|
|
||||
| `/licenses` | POST | Crear licencia | admin |
|
||||
| `/licenses/tenant/{id}` | GET | Obtener licencia | user |
|
||||
| `/licenses/tenant/{id}` | PUT | Actualizar licencia | admin |
|
||||
| `/licenses/validate/{id}` | GET | Validar licencia | user |
|
||||
| `/licenses/usage/{id}` | GET | Uso de licencia | user |
|
||||
| `/licenses/my-license` | GET | Mi licencia | user |
|
||||
|
||||
### Planes de Licencia
|
||||
|
||||
| Plan | Usuarios | Storage | Operaciones/mes | Features |
|
||||
|------|----------|---------|-----------------|----------|
|
||||
| Free | 5 | 10 GB | 1,000 | API básica |
|
||||
| Basic | 20 | 50 GB | 10,000 | + Reportes |
|
||||
| Professional | 100 | 200 GB | 50,000 | + Integraciones |
|
||||
| Enterprise | ∞ | ∞ | ∞ | + Soporte + BD dedicada |
|
||||
|
||||
---
|
||||
|
||||
## Próximas Implementaciones
|
||||
|
||||
### Backend
|
||||
- [ ] Módulo de inventarios
|
||||
- [ ] Módulo de pedimentos
|
||||
- [ ] Módulo de facturas
|
||||
- [ ] Webhooks para integraciones
|
||||
- [ ] Reportes avanzados
|
||||
- [ ] Export/Import de datos
|
||||
|
||||
### Frontend
|
||||
- [ ] Dashboard con gráficas
|
||||
- [ ] Gestión de inventarios UI
|
||||
- [ ] Formularios de pedimentos
|
||||
- [ ] Panel de administración
|
||||
- [ ] Reportes interactivos
|
||||
|
||||
### DevOps
|
||||
- [ ] CI/CD pipeline
|
||||
- [ ] Tests automatizados
|
||||
- [ ] Monitoreo con Prometheus
|
||||
- [ ] Dashboards de Grafana
|
||||
- [ ] Deploy a Kubernetes
|
||||
- [ ] Backup automatizado
|
||||
|
||||
---
|
||||
|
||||
**Última actualización**: Octubre 2025
|
||||
**Versión del documento**: 1.0
|
||||
227
docs/KEYCLOAK_SETUP.md
Normal file
227
docs/KEYCLOAK_SETUP.md
Normal file
@@ -0,0 +1,227 @@
|
||||
# Guía de Configuración de Keycloak para Anexo76
|
||||
|
||||
Esta guía te ayudará a configurar Keycloak para usar con Anexo76.
|
||||
|
||||
# Script auto initialize
|
||||
|
||||
Te genera toda la configruracion inicial de keycloack que se ve en este documento,
|
||||
aparte de esto te genera un primer usuario configurado con su tenant y una company
|
||||
|
||||
```
|
||||
scripts/init_first_time.sh
|
||||
```
|
||||
|
||||
## 1. Acceder a Keycloak Admin Console
|
||||
|
||||
1. Abrir http://localhost:8080
|
||||
2. Hacer clic en "Administration Console"
|
||||
3. Login con: `admin` / `admin`
|
||||
|
||||
## 2. Configurar Cliente Backend
|
||||
|
||||
### Crear Cliente Backend
|
||||
|
||||
1. En el menú izquierdo, ir a **Clients**
|
||||
2. Clic en **Create client**
|
||||
3. Configurar:
|
||||
- **Client ID**: `anexo76-backend`
|
||||
- **Client Protocol**: `openid-connect`
|
||||
- Clic en **Next**
|
||||
4. En la siguiente pantalla:
|
||||
- **Client authentication**: ON (Confidential)
|
||||
- **Authorization**: OFF
|
||||
- **Authentication flow**: Marcar solo "Standard flow" y "Direct access grants"
|
||||
- Clic en **Next**
|
||||
5. En "Login settings":
|
||||
- **Root URL**: `http://localhost:8000`
|
||||
- **Valid redirect URIs**: `http://localhost:8000/*`
|
||||
- **Web origins**: `http://localhost:8000`
|
||||
- Clic en **Save**
|
||||
|
||||
### Obtener Client Secret
|
||||
|
||||
1. Ir a la pestaña **Credentials**
|
||||
2. Copiar el **Client secret**
|
||||
3. Agregar al archivo `backend/.env`:
|
||||
```
|
||||
KEYCLOAK_CLIENT_SECRET=tu-client-secret-aqui
|
||||
```
|
||||
|
||||
## 3. Configurar Cliente Frontend
|
||||
|
||||
### Crear Cliente Frontend
|
||||
|
||||
1. En **Clients**, clic en **Create client**
|
||||
2. Configurar:
|
||||
- **Client ID**: `anexo76-frontend`
|
||||
- **Client Protocol**: `openid-connect`
|
||||
- Clic en **Next**
|
||||
3. En la siguiente pantalla:
|
||||
- **Client authentication**: OFF (Public)
|
||||
- **Authorization**: OFF
|
||||
- **Authentication flow**: Marcar "Standard flow"
|
||||
- Clic en **Next**
|
||||
4. En "Login settings":
|
||||
- **Root URL**: `http://localhost:5173`
|
||||
- **Valid redirect URIs**:
|
||||
- `http://localhost:5173/*`
|
||||
- `http://localhost:3000/*`
|
||||
- **Valid post logout redirect URIs**:
|
||||
- `http://localhost:5173/*`
|
||||
- `http://localhost:3000/*`
|
||||
- **Web origins**:
|
||||
- `http://localhost:5173`
|
||||
- `http://localhost:3000`
|
||||
- Clic en **Save**
|
||||
|
||||
## 4. Crear Usuario de Prueba
|
||||
|
||||
### Crear Usuario
|
||||
|
||||
1. En el menú izquierdo, ir a **Users**
|
||||
2. Clic en **Add user**
|
||||
3. Configurar:
|
||||
- **Username**: `demo`
|
||||
- **Email**: `demo@empresa-demo.com`
|
||||
- **First name**: `Usuario`
|
||||
- **Last name**: `Demo`
|
||||
- **Email verified**: ON
|
||||
- Clic en **Create**
|
||||
|
||||
### Establecer Contraseña
|
||||
|
||||
1. Ir a la pestaña **Credentials**
|
||||
2. Clic en **Set password**
|
||||
3. Configurar:
|
||||
- **Password**: `demo123`
|
||||
- **Password confirmation**: `demo123`
|
||||
- **Temporary**: OFF (para no tener que cambiar la contraseña)
|
||||
4. Clic en **Save**
|
||||
|
||||
### Agregar Atributo tenant_id
|
||||
|
||||
1. En el mismo usuario, ir a la pestaña **Attributes**
|
||||
2. Clic en **Add an attribute**
|
||||
3. Configurar:
|
||||
- **Key**: `tenant_id`
|
||||
- **Value**: `1`
|
||||
4. Clic en **Save**
|
||||
|
||||
### Asignar Roles
|
||||
|
||||
1. Ir a la pestaña **Role mappings**
|
||||
2. En "Available roles", buscar y asignar:
|
||||
- `admin` (si existe)
|
||||
- `user` (si existe)
|
||||
3. Si no existen estos roles, crearlos primero:
|
||||
- Ir a **Realm roles** en el menú izquierdo
|
||||
- Crear roles: `admin`, `user`, `auditor`, `system`
|
||||
- Regresar al usuario y asignar roles
|
||||
|
||||
## 5. Configurar Mapper para tenant_id (Opcional pero recomendado)
|
||||
|
||||
Para que el `tenant_id` se incluya automáticamente en el token:
|
||||
|
||||
1. Ir a **Clients** → `anexo76-backend`
|
||||
2. Ir a la pestaña **Client scopes**
|
||||
3. Clic en `anexo76-backend-dedicated`
|
||||
4. Ir a la pestaña **Mappers**
|
||||
5. Clic en **Add mapper** → **By configuration** → **User Attribute**
|
||||
6. Configurar:
|
||||
- **Name**: `tenant-id-mapper`
|
||||
- **User Attribute**: `tenant_id`
|
||||
- **Token Claim Name**: `tenant_id`
|
||||
- **Claim JSON Type**: `String`
|
||||
- **Add to ID token**: ON
|
||||
- **Add to access token**: ON
|
||||
- **Add to userinfo**: ON
|
||||
7. Clic en **Save**
|
||||
|
||||
Repetir para el cliente `anexo76-frontend` si es necesario.
|
||||
|
||||
## 6. Verificar Configuración
|
||||
|
||||
### Probar desde el Frontend
|
||||
|
||||
1. Abrir http://localhost:5173
|
||||
2. Hacer clic en "Iniciar Sesión"
|
||||
3. Ingresar credenciales:
|
||||
- Usuario: `demo`
|
||||
- Contraseña: `demo123`
|
||||
4. Deberías ver el dashboard con información del usuario y licencia
|
||||
|
||||
### Probar desde el API
|
||||
|
||||
```bash
|
||||
# Obtener token
|
||||
curl -X POST http://localhost:8080/realms/master/protocol/openid-connect/token \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d "client_id=anexo76-backend" \
|
||||
-d "client_secret=TU_CLIENT_SECRET" \
|
||||
-d "username=demo" \
|
||||
-d "password=demo123" \
|
||||
-d "grant_type=password"
|
||||
|
||||
# Usar el token para llamar al API
|
||||
curl -X GET http://localhost:8000/v1/auth/me \
|
||||
-H "Authorization: Bearer TU_ACCESS_TOKEN"
|
||||
```
|
||||
|
||||
## 7. Configuración Adicional (Opcional)
|
||||
|
||||
### Personalizar Tema de Login
|
||||
|
||||
1. Ir a **Realm settings** → **Themes**
|
||||
2. Seleccionar tema de login deseado
|
||||
3. Guardar cambios
|
||||
|
||||
### Configurar Timeout de Sesión
|
||||
|
||||
1. Ir a **Realm settings** → **Sessions**
|
||||
2. Ajustar:
|
||||
- **SSO Session Idle**: Tiempo de inactividad antes de expirar (ej: 30 minutos)
|
||||
- **SSO Session Max**: Tiempo máximo de sesión (ej: 10 horas)
|
||||
3. Guardar cambios
|
||||
|
||||
### Habilitar Registro de Usuarios (Opcional)
|
||||
|
||||
1. Ir a **Realm settings** → **Login**
|
||||
2. Activar **User registration**
|
||||
3. Guardar cambios
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Error: "Invalid redirect URI"
|
||||
|
||||
- Verificar que las URIs en el cliente coincidan exactamente
|
||||
- Incluir el protocolo (http:// o https://)
|
||||
- Incluir el puerto si es necesario
|
||||
|
||||
### Error: "Client not found"
|
||||
|
||||
- Verificar que el Client ID sea exacto
|
||||
- Verificar que el realm sea correcto
|
||||
|
||||
### Token no incluye tenant_id
|
||||
|
||||
- Verificar que el usuario tenga el atributo configurado
|
||||
- Verificar que el mapper esté configurado correctamente
|
||||
- Probar obteniendo un nuevo token
|
||||
|
||||
### Usuario no puede hacer login
|
||||
|
||||
- Verificar que el usuario esté habilitado (User enabled: ON)
|
||||
- Verificar que el email esté verificado (Email verified: ON)
|
||||
- Verificar que la contraseña no sea temporal
|
||||
|
||||
## Próximos Pasos
|
||||
|
||||
1. Para producción, cambiar el realm de `master` a uno dedicado
|
||||
2. Configurar HTTPS/TLS en Keycloak
|
||||
3. Configurar backup de la base de datos de Keycloak
|
||||
4. Implementar políticas de contraseña más estrictas
|
||||
5. Configurar MFA (Multi-Factor Authentication)
|
||||
|
||||
---
|
||||
|
||||
**¡Listo!** Tu configuración de Keycloak está completa para desarrollo.
|
||||
214
docs/MICROSOFT_SSO_SETUP.md
Normal file
214
docs/MICROSOFT_SSO_SETUP.md
Normal file
@@ -0,0 +1,214 @@
|
||||
# Configuración de Login con Microsoft (Azure AD)
|
||||
|
||||
Esta guía te ayudará a configurar el login con Microsoft junto con el login tradicional.
|
||||
|
||||
## Parte 1: Configurar Aplicación en Azure AD
|
||||
|
||||
### 1.1 Crear App Registration en Azure Portal
|
||||
|
||||
1. Ve a [Azure Portal](https://portal.azure.com)
|
||||
2. Busca "Azure Active Directory" o "Microsoft Entra ID"
|
||||
3. En el menú lateral, selecciona **App registrations**
|
||||
4. Clic en **New registration**
|
||||
5. Configura:
|
||||
- **Name**: `Anexo76`
|
||||
- **Supported account types**:
|
||||
- "Accounts in any organizational directory (Any Azure AD directory - Multitenant)"
|
||||
- O "Accounts in any organizational directory and personal Microsoft accounts" si quieres permitir cuentas @outlook.com, @hotmail.com
|
||||
- **Redirect URI**:
|
||||
- Platform: `Web`
|
||||
- URI: `http://localhost:8080/realms/master/broker/microsoft/endpoint`
|
||||
- Clic en **Register**
|
||||
|
||||
### 1.2 Obtener Client ID y crear Client Secret
|
||||
|
||||
1. En la página de tu aplicación, copia el **Application (client) ID**
|
||||
2. Ve a **Certificates & secrets** en el menú lateral
|
||||
3. Clic en **New client secret**
|
||||
4. Descripción: `keycloak-integration`
|
||||
5. Expires: Selecciona el tiempo que prefieras (ej: 24 months)
|
||||
6. Clic en **Add**
|
||||
7. **IMPORTANTE**: Copia el **Value** del secret inmediatamente (solo se muestra una vez)
|
||||
|
||||
### 1.3 Configurar API Permissions (Opcional pero recomendado)
|
||||
|
||||
1. Ve a **API permissions**
|
||||
2. Deberías ver `Microsoft Graph` > `User.Read` (Delegated) - esto es suficiente
|
||||
3. Si quieres más información del usuario, agrega:
|
||||
- `email`
|
||||
- `profile`
|
||||
- `openid`
|
||||
|
||||
## Parte 2: Configurar Identity Provider en Keycloak
|
||||
|
||||
### 2.1 Agregar Microsoft como Identity Provider
|
||||
|
||||
1. Abre Keycloak Admin Console: http://localhost:8080
|
||||
2. Login como admin
|
||||
3. Asegúrate de estar en el realm correcto (probablemente `master`)
|
||||
4. En el menú lateral, ve a **Identity providers**
|
||||
5. En el dropdown "Add provider", selecciona **Microsoft**
|
||||
6. Configura:
|
||||
- **Alias**: `microsoft` (o cualquier nombre que prefieras)
|
||||
- **Display name**: `Microsoft` (esto es lo que verá el usuario)
|
||||
- **Enabled**: ON
|
||||
- **Store tokens**: ON (opcional, para poder usar tokens de Microsoft después)
|
||||
- **Stored tokens readable**: OFF
|
||||
- **Trust email**: ON
|
||||
- **First login flow**: `first broker login`
|
||||
- **Client ID**: Pega el Application (client) ID de Azure
|
||||
- **Client Secret**: Pega el client secret que copiaste
|
||||
- Clic en **Save**
|
||||
|
||||
### 2.2 Configurar Mappers (Mapeo de atributos)
|
||||
|
||||
Después de guardar, configura los mappers para traer información del usuario de Microsoft:
|
||||
|
||||
1. En la misma página del Identity Provider, ve a la pestaña **Mappers**
|
||||
2. Clic en **Add mapper**
|
||||
|
||||
**Mapper 1: Email**
|
||||
- Name: `email`
|
||||
- Sync mode override: `inherit`
|
||||
- Mapper type: `Attribute Importer`
|
||||
- Social profile JSON field path: `email`
|
||||
- User attribute name: `email`
|
||||
- Clic en **Save**
|
||||
|
||||
**Mapper 2: First Name**
|
||||
- Name: `firstName`
|
||||
- Mapper type: `Attribute Importer`
|
||||
- Social profile JSON field path: `given_name`
|
||||
- User attribute name: `firstName`
|
||||
- Clic en **Save**
|
||||
|
||||
**Mapper 3: Last Name**
|
||||
- Name: `lastName`
|
||||
- Mapper type: `Attribute Importer`
|
||||
- Social profile JSON field path: `family_name`
|
||||
- User attribute name: `lastName`
|
||||
- Clic en **Save**
|
||||
|
||||
**Mapper 4: Username**
|
||||
- Name: `username`
|
||||
- Mapper type: `Username Template Importer`
|
||||
- Template: `${CLAIM.email}`
|
||||
- Target: `BROKER_USERNAME`
|
||||
- Clic en **Save**
|
||||
|
||||
### 2.3 Configurar Redirect URI en Azure (si es necesario)
|
||||
|
||||
Si usas un realm diferente a `master`, actualiza la Redirect URI en Azure:
|
||||
|
||||
- Formato: `http://localhost:8080/realms/{REALM_NAME}/broker/microsoft/endpoint`
|
||||
- Para producción: `https://tu-dominio.com/realms/{REALM_NAME}/broker/microsoft/endpoint`
|
||||
|
||||
## Parte 3: Actualizar Frontend
|
||||
|
||||
El frontend necesita detectar y mostrar el botón de Microsoft. Keycloak proporciona esta información automáticamente.
|
||||
|
||||
### 3.1 Obtener Identity Providers disponibles
|
||||
|
||||
Tu frontend puede consultar los Identity Providers disponibles:
|
||||
|
||||
**Endpoint de Keycloak:**
|
||||
```
|
||||
GET http://localhost:8080/realms/master/broker-login/identity-providers
|
||||
```
|
||||
|
||||
Esto retorna algo como:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"alias": "microsoft",
|
||||
"displayName": "Microsoft",
|
||||
"providerId": "microsoft",
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 3.2 URL para iniciar flujo de Microsoft
|
||||
|
||||
Para iniciar el login con Microsoft, redirige al usuario a:
|
||||
```
|
||||
http://localhost:8080/realms/master/broker/microsoft/login?client_id=anexo76-frontend&redirect_uri=http://localhost:5173/auth/callback
|
||||
```
|
||||
|
||||
Parámetros:
|
||||
- `client_id`: Tu client ID de frontend en Keycloak (`anexo76-frontend`)
|
||||
- `redirect_uri`: URL a la que Keycloak redirigirá después del login exitoso
|
||||
- `response_type`: `code` (para authorization code flow)
|
||||
- `scope`: `openid profile email`
|
||||
|
||||
### 3.3 Manejar el Callback
|
||||
|
||||
Después del login con Microsoft, Keycloak redirige a tu `redirect_uri` con un `code`:
|
||||
```
|
||||
http://localhost:5173/auth/callback?code=abc123...&session_state=xyz...
|
||||
```
|
||||
|
||||
Tu frontend debe:
|
||||
1. Extraer el `code` del query string
|
||||
2. Intercambiar el `code` por tokens llamando a tu backend
|
||||
3. Tu backend llama a Keycloak para obtener los tokens
|
||||
|
||||
## Parte 4: Testing
|
||||
|
||||
### 4.1 Verificar que Microsoft aparece en la página de login
|
||||
|
||||
Ve a:
|
||||
```
|
||||
http://localhost:8080/realms/master/protocol/openid-connect/auth?client_id=anexo76-frontend&redirect_uri=http://localhost:5173&response_type=code
|
||||
```
|
||||
|
||||
Deberías ver:
|
||||
- Formulario de login tradicional (usuario/contraseña)
|
||||
- Botón o link de "Microsoft" para login social
|
||||
|
||||
### 4.2 Probar el flujo completo
|
||||
|
||||
1. Haz clic en el botón de Microsoft
|
||||
2. Serás redirigido a Microsoft login
|
||||
3. Ingresa credenciales de Microsoft
|
||||
4. Microsoft redirige a Keycloak
|
||||
5. Keycloak crea/actualiza el usuario y redirige a tu app
|
||||
6. Tu app obtiene el token y autentica al usuario
|
||||
|
||||
## Notas Importantes
|
||||
|
||||
### Multi-tenant con Microsoft
|
||||
|
||||
Si tu app es multi-tenant y quieres que cada tenant use su propio Azure AD:
|
||||
1. Crea múltiples Identity Providers en Keycloak (uno por tenant)
|
||||
2. Usa aliases diferentes: `microsoft-tenant1`, `microsoft-tenant2`
|
||||
3. En el frontend, muestra el botón correcto según el tenant
|
||||
|
||||
### Asignación automática de tenant
|
||||
|
||||
Cuando un usuario se loguea por primera vez con Microsoft, puedes:
|
||||
1. Usar un mapper para asignar atributos basados en el dominio del email
|
||||
2. Configurar "Default Tenant" en tu backend si el email es de un dominio conocido
|
||||
3. Solicitar al usuario que seleccione su tenant en el primer login
|
||||
|
||||
### Producción
|
||||
|
||||
Para producción, recuerda:
|
||||
1. Actualizar las Redirect URIs en Azure con tu dominio real
|
||||
2. Usar HTTPS
|
||||
3. Configurar correctamente los Web Origins en Keycloak
|
||||
4. Usar variables de entorno para las configuraciones
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Error: redirect_uri_mismatch
|
||||
- Verifica que la URI en Azure coincida exactamente con la de Keycloak
|
||||
- Formato: `https://tu-dominio.com/realms/{realm}/broker/{alias}/endpoint`
|
||||
|
||||
### Usuario se crea pero no tiene tenant_id
|
||||
- Configura un mapper en Keycloak para asignar tenant_id automáticamente
|
||||
- O maneja esto en tu backend en el primer login
|
||||
|
||||
### No aparece el botón de Microsoft
|
||||
- Verifica que el Identity Provider esté habilitado en Keycloak
|
||||
- Revisa que el Display Name esté configurado
|
||||
218
docs/VERIFICAR_MICROSOFT_CONFIG.md
Normal file
218
docs/VERIFICAR_MICROSOFT_CONFIG.md
Normal file
@@ -0,0 +1,218 @@
|
||||
# ✅ Verificar Configuración de Microsoft en Keycloak
|
||||
|
||||
## Paso 1: Verificar si Microsoft está configurado
|
||||
|
||||
Abre tu navegador y ve a:
|
||||
|
||||
```
|
||||
http://localhost:8080/admin/master/console/#/master/identity-providers
|
||||
```
|
||||
|
||||
Login: `admin` / `admin`
|
||||
|
||||
### ¿Qué deberías ver?
|
||||
|
||||
Si Microsoft está configurado, verás en la lista de Identity Providers:
|
||||
|
||||
- ✅ **microsoft** (o el alias que hayas usado)
|
||||
- Con estado: **Enabled** ✅
|
||||
|
||||
### Si NO ves "microsoft" en la lista:
|
||||
|
||||
**¡Necesitas configurarlo!** Sigue estos pasos:
|
||||
|
||||
---
|
||||
|
||||
## Paso 2: Configurar Microsoft en Keycloak (SI NO ESTÁ CONFIGURADO)
|
||||
|
||||
### 2.1 Crear App en Azure AD PRIMERO
|
||||
|
||||
Antes de configurar Keycloak, necesitas una aplicación en Azure:
|
||||
|
||||
1. Ve a [Azure Portal](https://portal.azure.com)
|
||||
2. Busca **Azure Active Directory** o **Microsoft Entra ID**
|
||||
3. **App registrations** → **New registration**
|
||||
4. Configura:
|
||||
|
||||
- **Name**: `Anexo76`
|
||||
- **Supported account types**: `Accounts in any organizational directory (Any Azure AD - Multitenant)`
|
||||
- **Redirect URI**:
|
||||
- Platform: `Web`
|
||||
- URI: `http://localhost:8080/realms/master/broker/microsoft/endpoint`
|
||||
- Click **Register**
|
||||
5. **Copia el Application (client) ID** - lo necesitarás
|
||||
6. Ve a **Certificates & secrets** → **New client secret**
|
||||
|
||||
- Descripción: `keycloak`
|
||||
- Expira: 24 meses
|
||||
- Click **Add**
|
||||
- **¡COPIA EL SECRET VALUE AHORA!** (solo se muestra una vez)
|
||||
|
||||
### 2.2 Agregar Microsoft a Keycloak
|
||||
|
||||
1. En Keycloak Admin Console: http://localhost:8080
|
||||
2. Login: `admin` / `admin`
|
||||
3. Menú lateral: **Identity providers**
|
||||
4. Dropdown: **Add provider** → Selecciona **Microsoft**
|
||||
5. Configura:
|
||||
|
||||
```
|
||||
Alias: microsoft
|
||||
Display name: Microsoft
|
||||
Enabled: ON ✅
|
||||
Store tokens: ON ✅
|
||||
Trust email: ON ✅
|
||||
First login flow: first broker login
|
||||
|
||||
Client ID: [PEGA TU APPLICATION ID DE AZURE]
|
||||
Client Secret: [PEGA TU SECRET DE AZURE]
|
||||
```
|
||||
|
||||
6. Click **Save**
|
||||
7. Ve a la pestaña **Mappers** y agrega estos 4 mappers:
|
||||
|
||||
**Mapper 1: email**
|
||||
|
||||
```
|
||||
Name: email
|
||||
Mapper type: Attribute Importer
|
||||
Social profile JSON field path: email
|
||||
User attribute name: email
|
||||
```
|
||||
|
||||
**Mapper 2: firstName**
|
||||
|
||||
```
|
||||
Name: firstName
|
||||
Mapper type: Attribute Importer
|
||||
Social profile JSON field path: given_name
|
||||
User attribute name: firstName
|
||||
```
|
||||
|
||||
**Mapper 3: lastName**
|
||||
|
||||
```
|
||||
Name: lastName
|
||||
Mapper type: Attribute Importer
|
||||
Social profile JSON field path: family_name
|
||||
User attribute name: lastName
|
||||
```
|
||||
|
||||
**Mapper 4: username**
|
||||
|
||||
```
|
||||
Name: username
|
||||
Mapper type: Username Template Importer
|
||||
Template: ${CLAIM.email}
|
||||
Target: BROKER_USERNAME
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Paso 4: Verificar en tu App
|
||||
|
||||
1. Asegúrate de que el frontend esté corriendo: `http://localhost:5173`
|
||||
2. Ve a la página de login: `http://localhost:5173/login`
|
||||
3. Ingresa un tenant (ej: `aduanasoft`)
|
||||
4. Click en el botón **"Iniciar sesión con Microsoft"**
|
||||
|
||||
### ¿Qué debería pasar?
|
||||
|
||||
✅ **Correcto:**
|
||||
|
||||
- Te redirige a Microsoft login
|
||||
- Ves la página de Microsoft pidiendo tu email/contraseña
|
||||
- Después de autenticarte, vuelves a tu app
|
||||
|
||||
❌ **Incorrecto (lo que te está pasando ahora):**
|
||||
|
||||
- Te lleva a la página de login de Keycloak
|
||||
- Ves el usuario "admin" ya logueado
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting Común
|
||||
|
||||
### Error: "Identity provider not found"
|
||||
|
||||
- El alias en Keycloak debe ser exactamente `microsoft` (minúsculas)
|
||||
- O cambia el código: `loginWithProvider('TU_ALIAS_EXACTO')`
|
||||
|
||||
### Error: redirect_uri_mismatch en Azure
|
||||
|
||||
- La URI en Azure debe ser EXACTAMENTE:
|
||||
```
|
||||
http://localhost:8080/realms/master/broker/microsoft/endpoint
|
||||
```
|
||||
- Nota el `/endpoint` al final
|
||||
|
||||
### Error: Unexpected error when authenticating with identity provider
|
||||
|
||||
```bash
|
||||
docker cp azure.crt anexo76-keycloak:/
|
||||
docker exec -it -u root anexo76-keycloak /bin/bash
|
||||
|
||||
keytool -importcert -trustcacerts -file /azure.crt \
|
||||
-keystore /etc/java/java-21-openjdk/java-21-openjdk-21.0.8.0.9-1.el9.x86_64/lib/security/cacerts \
|
||||
-alias azure-root -storepass changeit -noprompt
|
||||
|
||||
```
|
||||
|
||||
### Me redirige pero muestra error en Microsoft
|
||||
|
||||
- Verifica que el Client ID y Secret en Keycloak sean correctos
|
||||
- Verifica que la app en Azure esté habilitada
|
||||
|
||||
### Funciona pero el usuario no tiene tenant_id
|
||||
|
||||
- Esto es normal en el primer login
|
||||
- Puedes configurar un mapper adicional o manejarlo en tu backend
|
||||
|
||||
---
|
||||
|
||||
## Comando Rápido de Verificación
|
||||
|
||||
Ejecuta esto en una terminal:
|
||||
|
||||
```bash
|
||||
# Verificar si el endpoint del broker existe
|
||||
curl -s -o /dev/null -w "%{http_code}" "http://localhost:8080/realms/master/broker/microsoft/login?client_id=test&redirect_uri=http://localhost"
|
||||
```
|
||||
|
||||
**Resultados:**
|
||||
|
||||
- `302` = ✅ Microsoft está configurado (redirige a Microsoft)
|
||||
- `404` = ❌ Microsoft NO está configurado en Keycloak
|
||||
- `500` = ⚠️ Hay un error de configuración
|
||||
|
||||
---
|
||||
|
||||
## Resumen Rápido
|
||||
|
||||
**Para que funcione necesitas:**
|
||||
|
||||
1. ✅ App Registration en Azure AD con Client ID y Secret
|
||||
2. ✅ Identity Provider "microsoft" configurado en Keycloak
|
||||
3. ✅ Redirect URI en Azure: `http://localhost:8080/realms/master/broker/microsoft/endpoint`
|
||||
4. ✅ Variables de entorno en frontend (.env):
|
||||
```
|
||||
VITE_KEYCLOAK_URL=http://localhost:8080
|
||||
VITE_KEYCLOAK_REALM=master
|
||||
VITE_KEYCLOAK_CLIENT_ID=anexo76-frontend
|
||||
```
|
||||
|
||||
**El flujo correcto es:**
|
||||
|
||||
```
|
||||
Tu App → Keycloak Broker → Microsoft Login → Keycloak → Tu App
|
||||
```
|
||||
|
||||
**Lo que está pasando ahora:**
|
||||
|
||||
```
|
||||
Tu App → Keycloak Login (porque no encuentra el provider)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
¿Necesitas ayuda con la configuración? Primero verifica en Keycloak Admin Console si existe el Identity Provider "microsoft".
|
||||
70
docs/a76.json
Normal file
70
docs/a76.json
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"context": {
|
||||
"project_name": "Anexo76",
|
||||
"description": "Aplicación SaaS para gestión de comercio exterior conforme a Anexos 24, 30 y 22 del SAT.",
|
||||
"business_goal": "Ofrecer una plataforma multi-tenant para maquilas, IMMEX y agentes aduanales que permita manejar inventarios, pedimentos y facturas de importación/exportación con control de licencias y cumplimiento normativo."
|
||||
},
|
||||
"architecture": {
|
||||
"frontend": {
|
||||
"framework": "SvelteKit",
|
||||
"auth_integration": "keycloak-js",
|
||||
"ui_goal": "Dashboard moderno, responsivo y rápido para usuarios empresariales."
|
||||
},
|
||||
"backend": {
|
||||
"framework": "FastAPI",
|
||||
"auth": "Keycloak (OpenID Connect)",
|
||||
"db_model": "Hybrid multi-tenant",
|
||||
"shared_db": "Base de datos central para clientes pequeños y medianos",
|
||||
"dedicated_db": "Bases de datos independientes para clientes grandes o con alta operación",
|
||||
"features": [
|
||||
"Conexión dinámica a BD según tenant",
|
||||
"Middleware para validar licencias y tenants",
|
||||
"APIs RESTful versionadas (v1, v2...)",
|
||||
"Separación de capas: models (ORM), dto (Pydantic), service y routes"
|
||||
],
|
||||
"module_structure": {
|
||||
"pattern": "backend/v1/modules/{module_name}/",
|
||||
"files": {
|
||||
"models.py": "Definición ORM con SQLAlchemy",
|
||||
"dto.py": "Definición de Pydantic DTOs para entrada/salida de datos (reemplaza schemas.py)",
|
||||
"service.py": "Lógica de negocio y validaciones específicas del módulo",
|
||||
"routes.py": "Endpoints FastAPI que usan los DTOs y servicios"
|
||||
},
|
||||
"naming_convention": {
|
||||
"models": "Representan entidades persistentes (Base de datos)",
|
||||
"dto": "Data Transfer Objects para transporte entre capas y API",
|
||||
"service": "Capa de negocio (domain logic)",
|
||||
"routes": "Exposición HTTP / API layer"
|
||||
},
|
||||
"reasoning": "Se utiliza dto.py en lugar de schemas.py para reflejar un enfoque DDD y estilo arquitectónico similar a NestJS, manteniendo compatibilidad total con FastAPI y Pydantic."
|
||||
}
|
||||
},
|
||||
"auth_system": {
|
||||
"provider": "Keycloak",
|
||||
"multi_tenant_model": "Un Realm por cliente (tenant)",
|
||||
"roles": ["admin", "user", "auditor", "system"],
|
||||
"license_validation": "Middleware que verifica licencia y plan activo antes de procesar cada request"
|
||||
}
|
||||
},
|
||||
"license_management": {
|
||||
"strategy": "Control centralizado en core_db",
|
||||
"table_structure": {
|
||||
"tenant_id": "int",
|
||||
"plan": "string",
|
||||
"max_users": "int",
|
||||
"expires_at": "datetime",
|
||||
"status": "active|expired|pending"
|
||||
},
|
||||
"upgrade_flow": "El cliente puede escalar de BD compartida a BD dedicada manteniendo mismo tenant_id y realm."
|
||||
},
|
||||
"dev_ops": {
|
||||
"containerization": "Docker / Docker Compose",
|
||||
"orchestration": "Kubernetes (futuro)",
|
||||
"monitoring": ["Prometheus", "Grafana"],
|
||||
"ci_cd": "GitHub Actions o GitLab CI"
|
||||
},
|
||||
"prompt_usage": {
|
||||
"instruction": "Cuando uses este JSON, pide a la IA que genere o revise la arquitectura, código base o estrategia de despliegue respetando el modelo híbrido multi-tenant con Keycloak y FastAPI.",
|
||||
"example_request": "Diseña un flujo de autenticación multi-tenant con Keycloak y FastAPI que detecte automáticamente el tenant y seleccione la base de datos correcta. Usa dto.py en lugar de schemas.py para mantener una arquitectura estilo DDD."
|
||||
}
|
||||
}
|
||||
141
docs/analisis_flujo_migracion_csv_transporte_sin_validaciones.md
Normal file
141
docs/analisis_flujo_migracion_csv_transporte_sin_validaciones.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Analisis de flujo de migracion CSV de transporte (sin validaciones)
|
||||
|
||||
## Objetivo
|
||||
|
||||
Definir como migrar el flujo de importacion CSV de facturas hacia columnas nuevas de transporte, priorizando continuidad operativa y consistencia de IDs en `invoice_logistics`, sin depender de reglas de validacion funcional.
|
||||
|
||||
## Estado actual de la rama
|
||||
|
||||
- El pipeline actual ya separa `scan` y `commit` en `tasks.py`.
|
||||
- El mapeo de plantillas (`template_config.py`) aun esta centrado en `NUMERO TRANSPORTE`.
|
||||
- En `commit`, la persistencia de logistica usa principalmente:
|
||||
- `carrier_id` / `carrier_int_id` desde `CLAVE TRANSPORTISTA`.
|
||||
- `transport_type` desde `TIPO TRANSPORTE`.
|
||||
- `transport_num` desde `NUMERO TRANSPORTE`.
|
||||
- No existe aun una ruta establecida para columnas nuevas `CLAVE TRANSPORTE` y `NUMERO CAJA`.
|
||||
|
||||
## Intencion funcional identificada en los commits analizados
|
||||
|
||||
Sin copiar su logica de validacion, la idea de flujo que se quiso introducir es:
|
||||
|
||||
1. Separar los datos de transporte en dos entradas semanticas:
|
||||
- clave de vehiculo (`CLAVE TRANSPORTE`)
|
||||
- numero de caja/remolque (`NUMERO CAJA`)
|
||||
2. Mantener compatibilidad con archivos legacy:
|
||||
- usar `NUMERO TRANSPORTE` como fallback cuando no existan columnas nuevas.
|
||||
3. Resolver IDs internos en commit contra catalogos:
|
||||
- `vehicle_key` -> `vehicle_id`
|
||||
- `trailer_number` -> `trailer_id`
|
||||
4. Persistir tanto la clave legible (string) como su ID interno (int) en `invoice_logistics`.
|
||||
5. Mantener paridad de flujo entre scan y commit en cuanto a normalizacion y resolucion de campos, pero sin convertir scan en un bloqueo por validaciones de negocio.
|
||||
|
||||
## Flujo propuesto (sin validaciones de negocio)
|
||||
|
||||
### 1) Scan (preprocesamiento y trazabilidad)
|
||||
|
||||
Objetivo: preparar datos y metadatos, no rechazar por reglas funcionales.
|
||||
|
||||
- Leer CSV con `row_from_template`.
|
||||
- Normalizar celdas relevantes de transporte:
|
||||
- trim
|
||||
- reemplazo de NBSP por espacio
|
||||
- `None`/vacio a cadena vacia
|
||||
- Construir una estructura por renglon con campos de transporte efectivos:
|
||||
- `effective_transport_type`
|
||||
- `effective_vehicle_key`
|
||||
- `effective_trailer_number`
|
||||
- `effective_legacy_transport_num` (solo trazabilidad)
|
||||
- Guardar errores tecnicos (parseo CSV, formato bruto no interpretable), pero no bloquear por reglas de catalogo/negocio.
|
||||
|
||||
### 2) Commit (resolucion y persistencia de IDs)
|
||||
|
||||
Objetivo: persistir de forma consistente los IDs nuevos de migracion.
|
||||
|
||||
- Determinar campos efectivos por precedencia (ver tabla siguiente).
|
||||
- Resolver `carrier_int_id` con `CLAVE TRANSPORTISTA` (flujo ya existente).
|
||||
- Resolver `transport_int_id` cuando exista `effective_vehicle_key`:
|
||||
- lookup en `vehicle.vehicle_key`
|
||||
- Resolver `trailer_int_id` cuando exista `effective_trailer_number`:
|
||||
- lookup en `trailer.trailer_number`
|
||||
- Persistir en `InvoiceLogistics`:
|
||||
- string keys: `carrier_id`, `transport_id`, `trailer_num`, `transport_num`
|
||||
- int refs: `carrier_int_id`, `transport_int_id`, `trailer_int_id`
|
||||
|
||||
Importante: para migracion, si hay string pero no hay match de ID, **no romper flujo**; persistir string y dejar int en `NULL` (el FK ya permite `SET NULL`).
|
||||
|
||||
## Matriz de mapeo CSV -> `invoice_logistics`
|
||||
|
||||
| Entrada CSV | Rol | Campo destino (string) | Campo destino (int) | Catalogo/lookup |
|
||||
|---|---|---|---|---|
|
||||
| `CLAVE TRANSPORTISTA` | Transportista | `carrier_id` | `carrier_int_id` | `transporter.transporter_key -> transporter_id` |
|
||||
| `CLAVE TRANSPORTE` | Vehiculo | `transport_id` | `transport_int_id` | `vehicle.vehicle_key -> vehicle_id` |
|
||||
| `NUMERO CAJA` | Caja/Remolque | `trailer_num` | `trailer_int_id` | `trailer.trailer_number -> trailer_id` |
|
||||
| `NUMERO TRANSPORTE` (legacy) | Fallback | `transport_num` (siempre trazable) y apoyo para resolver vehicle/trailer segun precedencia | opcional | depende de reglas de precedencia |
|
||||
| `TIPO TRANSPORTE` | Tipo logistica | `transport_type` | n/a | enum interno |
|
||||
|
||||
## Reglas de precedencia de datos (nuevas vs legacy)
|
||||
|
||||
Definicion para no generar ambiguedad:
|
||||
|
||||
1. Si viene `CLAVE TRANSPORTE`, usarla para `transport_id`.
|
||||
2. Si viene `NUMERO CAJA`, usarla para `trailer_num`.
|
||||
3. Si faltan columnas nuevas y viene `NUMERO TRANSPORTE`:
|
||||
- usarlo como `transport_num` (trazabilidad legacy)
|
||||
- y usarlo como fallback de resolucion para `transport_id`/`trailer_num` solo cuando el campo nuevo correspondiente este vacio.
|
||||
4. Nunca sobreescribir un dato nuevo con legacy si el nuevo viene poblado.
|
||||
|
||||
Sugerencia para parciales (`actualizar=true`):
|
||||
- aplicar merge campo a campo: solo actualizar datos de transporte que lleguen informados en CSV; conservar los demas en la fila existente.
|
||||
|
||||
## Relacion con la migracion de IDs
|
||||
|
||||
La migracion `ca7d3c4e8b2a` ya formaliza:
|
||||
|
||||
- `carrier_int_id` -> FK a `transporter.transporter_id`
|
||||
- `transport_int_id` -> FK a `vehicle.vehicle_id`
|
||||
- `trailer_int_id` -> FK a `trailer.trailer_id`
|
||||
|
||||
Por lo tanto, el flujo CSV debe priorizar:
|
||||
|
||||
- resolver claves string de catalogo de forma determinista
|
||||
- poblar IDs internos cuando haya match
|
||||
- mantener string keys para trazabilidad y backfill futuro
|
||||
|
||||
## Impacto de implementacion por archivo (sin validaciones)
|
||||
|
||||
### `backend/api/v1/modules/a76/layouts_csv/facturas/template_config.py`
|
||||
|
||||
- Agregar columnas canonicas nuevas en encabezados:
|
||||
- `CLAVE TRANSPORTE`
|
||||
- `NUMERO CAJA`
|
||||
- Mantener `NUMERO TRANSPORTE` como compatibilidad legacy (no eliminar de inmediato).
|
||||
- Ajustar aliases para tolerar variantes de cabecera.
|
||||
|
||||
### `backend/api/v1/modules/a76/layouts_csv/facturas/tasks.py`
|
||||
|
||||
- Incorporar helper de resolucion de campos efectivos de transporte:
|
||||
- prioridad nuevas columnas
|
||||
- fallback legacy
|
||||
- En `scan`, registrar estructura normalizada (sin rechazo por reglas de catalogo).
|
||||
- En `commit`, poblar `InvoiceLogistics` con:
|
||||
- `transport_id`/`transport_int_id`
|
||||
- `trailer_num`/`trailer_int_id`
|
||||
- `transport_num` como legado/trazabilidad
|
||||
- Mantener comportamiento de no falla por ausencia de match de IDs.
|
||||
|
||||
### Nuevo helper recomendado: `backend/api/v1/modules/a76/layouts_csv/facturas/validators/transport_catalog.py`
|
||||
|
||||
- Aunque no se usen validaciones de negocio, centralizar funciones de:
|
||||
- normalizacion de celdas
|
||||
- resolucion de keys efectivas
|
||||
- lookups de IDs de vehiculo/remolque
|
||||
- Evita duplicar logica entre scan y commit.
|
||||
|
||||
## Criterios de aceptacion de esta migracion de flujo
|
||||
|
||||
- Queda definida una sola fuente de verdad para precedencia de columnas nuevas/legacy.
|
||||
- `commit` persiste consistentemente claves string e IDs int de logistica.
|
||||
- El flujo funciona aun cuando no haya match de catalogo (sin bloqueo por validacion).
|
||||
- No hay ambiguedad entre:
|
||||
- `transport_id` vs `transport_num`
|
||||
- `transport_int_id` vs `trailer_int_id`
|
||||
24
docs/keyboard_shortcuts_alt_digit_matrix.md
Normal file
24
docs/keyboard_shortcuts_alt_digit_matrix.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Alt+Numero Keyboard Navigation Matrix
|
||||
|
||||
This matrix documents the active `Alt+DigitN` shortcuts used for fast tab/view navigation in dashboard flows.
|
||||
|
||||
## Active contexts
|
||||
|
||||
| Context | Shortcut Source | Alt+Digit targets | Focus strategy |
|
||||
| --- | --- | --- | --- |
|
||||
| `Edit Broker Tabs` | `frontend/src/lib/config/shortcuts/dashboard/customs_brokers/edit.ts` | `general`, `contact`, `address`, `vu`, `doda`, `anam` | `first-input` |
|
||||
| `Edit Client Provider` | `frontend/src/lib/config/shortcuts/dashboard/clients_and_providers/edit.ts` | `general`, `address`, `programs`, `config` | `first-input` |
|
||||
| `Formulario Empresa` | `frontend/src/lib/config/shortcuts/dashboard/general_catalogs/company_information/edit.ts` | `general`, `programa`, `responsable`, `certificaciones`, `direcciones`, `config`, `certificados` | `first-input` |
|
||||
| `Formulario DODA` | `frontend/src/lib/config/shortcuts/dashboard/general_catalogs/doda/edit.ts` | `general`, `transport`, `sat`, `other` | `first-input` |
|
||||
| `Invoice Edit` | `frontend/src/lib/config/shortcuts/dashboard/invoices/edit.ts` | `general`, `observations`, `items`, `others`, `continuation` | `first-input` |
|
||||
| `Invoice Item Form (Inventory)` | `frontend/src/lib/config/shortcuts/dashboard/invoices/item/inventory.ts` | `tab1..tab4` mapped to `general`, `clasificacion`, `cantidades`, `otros` | `first-input` |
|
||||
| `Invoice Item Form (Fixed Asset)` | `frontend/src/lib/config/shortcuts/dashboard/invoices/item/fixed_asset.ts` | `tab1..tab5` mapped to `generales`, `continuacion`, `series`, `etiquetado`, `identificadores` | `first-input` |
|
||||
| `Part Form` | `frontend/src/lib/config/shortcuts/dashboard/goods/edit.ts` | `general`, `cont1`, `cont2` | `first-input` |
|
||||
| `Customs Brokers List` | `frontend/src/lib/config/shortcuts/dashboard/customs_brokers/list.ts` | `brokers`, `customs` | `trigger` |
|
||||
| `Clients Providers List` | `frontend/src/lib/config/shortcuts/dashboard/clients_and_providers/list.ts` | `all`, `clients`, `providers` | `trigger` |
|
||||
|
||||
## Notes
|
||||
|
||||
- `KeyboardManager` now has explicit focus policy entries for all contexts listed above.
|
||||
- `fixed_asset` shortcuts remain in place and are now connected in `item-sheet-fa`.
|
||||
- No shortcut set was removed in this pass; changes are additive/corrective for keyboard-only navigation.
|
||||
Reference in New Issue
Block a user