feat(auth): add user registration and response DTOs, implement registration endpoint in AuthService
feat(auth): create registration route and integrate with AuthService feat(auth): implement user registration logic in AuthService with Keycloak integration fix(config): add Keycloak admin credentials to settings fix(middleware): update tenant middleware to include new auth routes chore(docs): remove outdated testing guide and add project architecture documentation feat(frontend): implement user registration page and integrate with API feat(frontend): create login page with tenant selection and error handling refactor(frontend): update layout to use custom auth store and improve loading states
This commit is contained in:
@@ -1,329 +0,0 @@
|
||||
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ 🎉 PROYECTO ANEXO76 GENERADO 🎉 ║
|
||||
║ ║
|
||||
║ Aplicación SaaS Multi-tenant para Comercio Exterior ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
|
||||
|
||||
📊 RESUMEN EJECUTIVO
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
✅ BACKEND (FastAPI + Keycloak + SQLAlchemy)
|
||||
• 22 archivos Python generados
|
||||
• 24 dependencias configuradas
|
||||
• 3 módulos completos implementados
|
||||
• ~3,200 líneas de código
|
||||
|
||||
Módulos implementados:
|
||||
├── auth/ → Autenticación con Keycloak (OpenID Connect)
|
||||
├── tenants/ → Gestión de clientes multi-tenant
|
||||
└── licenses/ → Control de licencias y planes
|
||||
|
||||
✅ FRONTEND (SvelteKit + Keycloak-js + TailwindCSS)
|
||||
• 8 archivos TypeScript
|
||||
• 5 componentes Svelte
|
||||
• Dashboard completo con autenticación
|
||||
• ~2,000 líneas de código
|
||||
|
||||
Funcionalidades:
|
||||
├── Login/Logout completo
|
||||
├── Dashboard con información de usuario
|
||||
├── Visualización de licencias
|
||||
└── Cliente API integrado
|
||||
|
||||
✅ DOCUMENTACIÓN
|
||||
• README.md - Guía principal
|
||||
• ARCHITECTURE.md - Arquitectura técnica completa
|
||||
• KEYCLOAK_SETUP.md - Configuración paso a paso
|
||||
• TESTING_GUIDE.md - Casos de prueba
|
||||
• ~2,500 líneas de documentación
|
||||
|
||||
✅ DEVOPS
|
||||
• Docker Compose con 4 servicios
|
||||
• Scripts de inicio automático
|
||||
• Health checks configurados
|
||||
• Hot reload en desarrollo
|
||||
|
||||
|
||||
🏗️ ARQUITECTURA IMPLEMENTADA
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
PATRÓN: Modular Layered (estilo NestJS)
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ FRONTEND (SvelteKit) │
|
||||
│ • Autenticación Keycloak │
|
||||
│ • Dashboard reactivo │
|
||||
│ • TailwindCSS para estilos │
|
||||
└────────────────┬────────────────────────────────────────────┘
|
||||
│ HTTP/REST + JWT
|
||||
┌────────────────▼────────────────────────────────────────────┐
|
||||
│ API GATEWAY (FastAPI) │
|
||||
│ Middlewares: │
|
||||
│ ├── TenantMiddleware → Identifica tenant │
|
||||
│ ├── LicenseValidation → Valida licencia activa │
|
||||
│ └── RequestLogging → Logging de requests │
|
||||
└────────────────┬────────────────────────────────────────────┘
|
||||
│
|
||||
┌────────┴────────┐
|
||||
│ │
|
||||
┌───────▼──────┐ ┌──────▼────────┐
|
||||
│ MÓDULOS │ │ CORE LAYER │
|
||||
│ │ │ │
|
||||
│ Cada módulo │ │ • Config │
|
||||
│ tiene: │ │ • Database │
|
||||
│ • models │ │ • Security │
|
||||
│ • dto │ │ • Middleware │
|
||||
│ • service │ │ │
|
||||
│ • routes │ │ │
|
||||
└───────┬──────┘ └───────────────┘
|
||||
│
|
||||
┌───────▼──────────────────────────────────┐
|
||||
│ MULTI-TENANT DATABASE │
|
||||
│ ┌──────────────┐ ┌─────────────────┐ │
|
||||
│ │ Core DB │ │ Tenant 1 DB │ │
|
||||
│ │ (Shared) │ │ (Dedicated) │ │
|
||||
│ └──────────────┘ └─────────────────┘ │
|
||||
└──────────────────────────────────────────┘
|
||||
|
||||
|
||||
🎯 CARACTERÍSTICAS CLAVE
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
✓ MULTI-TENANT HÍBRIDO
|
||||
• BD compartida para clientes pequeños/medianos
|
||||
• BD dedicada para clientes enterprise
|
||||
• Migración automática entre modos
|
||||
• Row-level security en BD compartida
|
||||
|
||||
✓ AUTENTICACIÓN Y SEGURIDAD
|
||||
• Keycloak como Identity Provider
|
||||
• OpenID Connect (OIDC)
|
||||
• JWT con firma RS256
|
||||
• RBAC (Role-Based Access Control)
|
||||
• Roles: admin, user, auditor, system
|
||||
|
||||
✓ CONTROL DE LICENCIAS
|
||||
• 4 planes: Free, Basic, Professional, Enterprise
|
||||
• Validación automática en cada request
|
||||
• Tracking de uso (usuarios, storage, operaciones)
|
||||
• Límites configurables por plan
|
||||
|
||||
✓ ESTRUCTURA MODULAR
|
||||
• Patrón estilo NestJS
|
||||
• Separación clara de responsabilidades
|
||||
• DTOs con Pydantic para validación
|
||||
• Services para lógica de negocio
|
||||
• Routes para exposición HTTP
|
||||
|
||||
✓ DEVELOPER EXPERIENCE
|
||||
• Hot reload en desarrollo
|
||||
• Documentación automática (Swagger)
|
||||
• Type safety (TypeScript + Pydantic)
|
||||
• Scripts de inicio rápido
|
||||
• Logs estructurados
|
||||
|
||||
|
||||
📦 SERVICIOS DOCKER COMPOSE
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
┌────────────────┬─────────────────┬──────────────────────────────┐
|
||||
│ Servicio │ Puerto │ Descripción │
|
||||
├────────────────┼─────────────────┼──────────────────────────────┤
|
||||
│ frontend │ 5173 │ SvelteKit (desarrollo) │
|
||||
│ backend │ 8000 │ FastAPI + Uvicorn │
|
||||
│ keycloak │ 8080 │ Identity Provider │
|
||||
│ postgres │ 5432 │ Base de datos PostgreSQL 15 │
|
||||
└────────────────┴─────────────────┴──────────────────────────────┘
|
||||
|
||||
|
||||
📈 MÉTRICAS DEL PROYECTO
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Líneas de código: ~5,700 líneas
|
||||
Archivos generados: 60+ archivos
|
||||
Módulos backend: 3 (auth, tenants, licenses)
|
||||
Endpoints API: 18 endpoints
|
||||
Documentación: 4 documentos completos
|
||||
Tiempo estimado: 40+ horas de desarrollo manual
|
||||
|
||||
|
||||
🚀 CÓMO INICIAR
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
1. Ejecutar script de inicio:
|
||||
$ ./start.sh
|
||||
|
||||
2. Configurar Keycloak:
|
||||
Seguir: docs/KEYCLOAK_SETUP.md
|
||||
|
||||
3. Acceder a la aplicación:
|
||||
• Frontend: http://localhost:5173
|
||||
• Backend: http://localhost:8000/docs
|
||||
• Keycloak: http://localhost:8080
|
||||
|
||||
4. Login de prueba:
|
||||
Usuario: demo
|
||||
Password: demo123
|
||||
|
||||
|
||||
📚 DOCUMENTACIÓN DISPONIBLE
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
📄 README.md
|
||||
→ Visión general del proyecto
|
||||
→ Quick start guide
|
||||
→ Estructura del proyecto
|
||||
→ Tabla de planes de licencia
|
||||
|
||||
📄 docs/ARCHITECTURE.md
|
||||
→ Arquitectura técnica detallada
|
||||
→ Patrones implementados
|
||||
→ Flujos de autenticación
|
||||
→ Modelo de datos
|
||||
→ API reference completo
|
||||
|
||||
📄 docs/KEYCLOAK_SETUP.md
|
||||
→ Configuración paso a paso de Keycloak
|
||||
→ Creación de clientes (backend y frontend)
|
||||
→ Configuración de usuarios
|
||||
→ Mappers de atributos
|
||||
→ Troubleshooting
|
||||
|
||||
📄 docs/TESTING_GUIDE.md
|
||||
→ Verificación de servicios
|
||||
→ Pruebas de endpoints
|
||||
→ Casos de prueba con curl
|
||||
→ Pruebas de frontend
|
||||
→ Debugging tips
|
||||
|
||||
|
||||
🎓 PRÓXIMOS MÓDULOS A IMPLEMENTAR
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Siguiendo el mismo patrón modular, puedes agregar:
|
||||
|
||||
📦 inventories/
|
||||
→ Gestión de inventarios para IMMEX
|
||||
→ Control de entradas/salidas
|
||||
→ Trazabilidad de productos
|
||||
|
||||
📦 pedimentos/
|
||||
→ Pedimentos aduanales
|
||||
→ Anexo 24 compliance
|
||||
→ Validaciones SAT
|
||||
|
||||
📦 invoices/
|
||||
→ Facturas de importación/exportación
|
||||
→ Complementos de comercio exterior
|
||||
→ Integración con CFDI
|
||||
|
||||
📦 reports/
|
||||
→ Reportes avanzados
|
||||
→ Dashboards personalizados
|
||||
→ Exports (Excel, PDF)
|
||||
|
||||
📦 webhooks/
|
||||
→ Integraciones con sistemas externos
|
||||
→ Notificaciones automáticas
|
||||
→ Event-driven architecture
|
||||
|
||||
|
||||
🔧 TECNOLOGÍAS Y VERSIONES
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Backend:
|
||||
├── Python 3.11+
|
||||
├── FastAPI 0.110.0
|
||||
├── SQLAlchemy 2.0.27
|
||||
├── Pydantic 2.6.1
|
||||
├── Keycloak-Python 3.9.1
|
||||
└── PostgreSQL 15
|
||||
|
||||
Frontend:
|
||||
├── Node.js 20+
|
||||
├── SvelteKit 2.43.2
|
||||
├── Svelte 5.39.5
|
||||
├── TypeScript 5.9.2
|
||||
├── Keycloak-js 26.2.1
|
||||
├── TailwindCSS 4.1.13
|
||||
└── Vite 7.1.7
|
||||
|
||||
DevOps:
|
||||
├── Docker 24+
|
||||
├── Docker Compose 2.x
|
||||
└── (Futuro) Kubernetes
|
||||
|
||||
|
||||
✅ CHECKLIST DE ENTREGA
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
[✓] Backend FastAPI con 3 módulos completos
|
||||
[✓] Frontend SvelteKit con autenticación
|
||||
[✓] Base de datos multi-tenant (modelo híbrido)
|
||||
[✓] Sistema de autenticación con Keycloak
|
||||
[✓] Control de licencias con middleware
|
||||
[✓] DTOs con Pydantic (estilo NestJS)
|
||||
[✓] Docker Compose con 4 servicios
|
||||
[✓] Scripts de inicio automático (start.sh)
|
||||
[✓] Script de verificación (verify.sh)
|
||||
[✓] Documentación completa (4 docs)
|
||||
[✓] README con instrucciones claras
|
||||
[✓] .gitignore configurado
|
||||
[✓] Datos de prueba iniciales
|
||||
[✓] Health checks en todos los servicios
|
||||
[✓] Hot reload en desarrollo
|
||||
[✓] CORS configurado correctamente
|
||||
|
||||
|
||||
🎊 ESTADO DEL PROYECTO
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
✅ GENERACIÓN COMPLETADA AL 100%
|
||||
|
||||
El proyecto Anexo76 ha sido generado completamente siguiendo el blueprint
|
||||
técnico proporcionado. Todos los archivos, módulos, configuraciones y
|
||||
documentación están listos para su uso.
|
||||
|
||||
|
||||
🚦 PRÓXIMOS PASOS
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
1. ✓ Verificar estructura: ./verify.sh
|
||||
2. → Iniciar servicios: ./start.sh
|
||||
3. → Configurar Keycloak: Ver docs/KEYCLOAK_SETUP.md
|
||||
4. → Probar aplicación: Ver docs/TESTING_GUIDE.md
|
||||
5. → Desarrollar nuevos módulos: Ver docs/ARCHITECTURE.md
|
||||
|
||||
|
||||
💡 TIPS ADICIONALES
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
• Para ver logs: docker-compose logs -f [servicio]
|
||||
• Para detener: docker-compose down
|
||||
• Para reiniciar: docker-compose restart [servicio]
|
||||
• Para limpiar todo: docker-compose down -v
|
||||
• Docs interactivas: http://localhost:8000/docs
|
||||
• Monitorear BD: pgAdmin o herramienta similar
|
||||
|
||||
|
||||
📞 SOPORTE
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
• Arquitectura: Ver docs/ARCHITECTURE.md
|
||||
• Setup Keycloak: Ver docs/KEYCLOAK_SETUP.md
|
||||
• Testing: Ver docs/TESTING_GUIDE.md
|
||||
• Issues: Revisar logs con docker-compose
|
||||
|
||||
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Proyecto generado el 17 de Octubre, 2025
|
||||
Stack: FastAPI + SvelteKit + Keycloak + PostgreSQL
|
||||
Arquitectura: Multi-tenant Modular
|
||||
|
||||
¡Listo para producción! 🚀
|
||||
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
301
SUMMARY.md
301
SUMMARY.md
@@ -1,301 +0,0 @@
|
||||
# 🎉 Proyecto Anexo76 - Generado Exitosamente
|
||||
|
||||
## ✅ Resumen de lo Generado
|
||||
|
||||
### 📁 Estructura Completa Creada
|
||||
|
||||
#### Backend (FastAPI + Keycloak + SQLAlchemy)
|
||||
```
|
||||
backend/
|
||||
├── main.py ✅ Aplicación FastAPI con middlewares
|
||||
├── init_db.py ✅ Script de inicialización de BD
|
||||
├── requirements.txt ✅ Dependencias actualizadas con comentarios
|
||||
├── Dockerfile ✅ Containerización
|
||||
├── .env.example ✅ Variables de entorno
|
||||
│
|
||||
├── core/ ✅ Capa core compartida
|
||||
│ ├── config.py ✅ Configuración con Pydantic Settings
|
||||
│ ├── database.py ✅ Multi-tenant DB (hybrid model)
|
||||
│ ├── security.py ✅ Auth Keycloak + JWT
|
||||
│ ├── middleware.py ✅ Tenant, License, Logging middlewares
|
||||
│ └── __init__.py ✅
|
||||
│
|
||||
└── api/v1/
|
||||
├── router.py ✅ Router principal v1
|
||||
└── modules/ ✅ Módulos estilo NestJS
|
||||
├── auth/ ✅ Autenticación completa
|
||||
│ ├── dto.py
|
||||
│ ├── service.py
|
||||
│ ├── routes.py
|
||||
│ └── __init__.py
|
||||
├── tenants/ ✅ Gestión de tenants
|
||||
│ ├── models.py
|
||||
│ ├── dto.py
|
||||
│ ├── service.py
|
||||
│ ├── routes.py
|
||||
│ └── __init__.py
|
||||
└── licenses/ ✅ Control de licencias
|
||||
├── models.py
|
||||
├── dto.py
|
||||
├── service.py
|
||||
├── routes.py
|
||||
└── __init__.py
|
||||
```
|
||||
|
||||
#### Frontend (SvelteKit + Keycloak-js)
|
||||
```
|
||||
frontend/
|
||||
├── src/
|
||||
│ ├── routes/
|
||||
│ │ ├── +layout.svelte ✅ Layout con init Keycloak
|
||||
│ │ ├── +page.svelte ✅ Dashboard completo
|
||||
│ │ └── callback/ ✅ OAuth callback
|
||||
│ │ └── +page.svelte
|
||||
│ │
|
||||
│ ├── lib/
|
||||
│ │ ├── auth.ts ✅ Servicio autenticación
|
||||
│ │ └── api.ts ✅ Cliente API
|
||||
│ │
|
||||
│ └── app.html ✅ HTML base
|
||||
│
|
||||
├── static/
|
||||
│ └── silent-check-sso.html ✅ SSO silencioso
|
||||
│
|
||||
├── Dockerfile ✅ Containerización
|
||||
├── .env ✅ Variables configuradas
|
||||
└── package.json ✅ Con keycloak-js instalado
|
||||
```
|
||||
|
||||
#### Documentación
|
||||
```
|
||||
docs/
|
||||
├── ARCHITECTURE.md ✅ Arquitectura técnica completa
|
||||
├── KEYCLOAK_SETUP.md ✅ Guía configuración Keycloak
|
||||
└── TESTING_GUIDE.md ✅ Guía de pruebas exhaustiva
|
||||
```
|
||||
|
||||
#### DevOps
|
||||
```
|
||||
├── docker-compose.yml ✅ 4 servicios configurados
|
||||
├── start.sh ✅ Script de inicio rápido
|
||||
├── README.md ✅ Documentación principal
|
||||
└── .gitignore ✅ Archivos a ignorar
|
||||
```
|
||||
|
||||
## 🎯 Características Implementadas
|
||||
|
||||
### Backend
|
||||
- ✅ **FastAPI** con documentación automática (Swagger)
|
||||
- ✅ **Autenticación Keycloak** (OpenID Connect)
|
||||
- ✅ **Multi-tenant híbrido** (BD compartida + dedicada)
|
||||
- ✅ **Middleware de licencias** con validación automática
|
||||
- ✅ **Estructura modular** estilo NestJS con DTOs
|
||||
- ✅ **Separación de capas**: models, dto, service, routes
|
||||
- ✅ **3 módulos completos**: auth, tenants, licenses
|
||||
- ✅ **CORS configurado**
|
||||
- ✅ **Logging estructurado**
|
||||
- ✅ **SQLAlchemy 2.0** con soporte async
|
||||
- ✅ **Pydantic v2** para validaciones
|
||||
|
||||
### Frontend
|
||||
- ✅ **SvelteKit** con TypeScript
|
||||
- ✅ **Integración Keycloak-js** completa
|
||||
- ✅ **Login/Logout** funcional
|
||||
- ✅ **Dashboard** con información de usuario y licencia
|
||||
- ✅ **TailwindCSS 4** para estilos
|
||||
- ✅ **Stores reactivos** para estado de auth
|
||||
- ✅ **Cliente API** con manejo de tokens
|
||||
- ✅ **SSO silencioso** configurado
|
||||
- ✅ **Rutas protegidas**
|
||||
|
||||
### Infraestructura
|
||||
- ✅ **Docker Compose** con 4 servicios
|
||||
- ✅ **PostgreSQL 15** para BD core
|
||||
- ✅ **Keycloak 23** para autenticación
|
||||
- ✅ **Hot reload** en desarrollo
|
||||
- ✅ **Volúmenes persistentes**
|
||||
- ✅ **Health checks**
|
||||
|
||||
### Seguridad
|
||||
- ✅ **JWT con RS256**
|
||||
- ✅ **RBAC** (Role-Based Access Control)
|
||||
- ✅ **Validación de tenant** en cada request
|
||||
- ✅ **Control de licencias** automático
|
||||
- ✅ **Isolation por tenant_id**
|
||||
|
||||
## 📊 Endpoints API Disponibles
|
||||
|
||||
### Autenticación (`/v1/auth`)
|
||||
- `POST /auth/login` - Login con Keycloak
|
||||
- `POST /auth/refresh` - Renovar token
|
||||
- `GET /auth/me` - Info usuario actual
|
||||
- `POST /auth/logout` - Cerrar sesión
|
||||
- `GET /auth/health` - Health check
|
||||
|
||||
### Tenants (`/v1/tenants`)
|
||||
- `POST /tenants` - Crear tenant
|
||||
- `GET /tenants` - Listar tenants
|
||||
- `GET /tenants/{id}` - Obtener tenant
|
||||
- `PUT /tenants/{id}` - Actualizar tenant
|
||||
- `DELETE /tenants/{id}` - Eliminar tenant
|
||||
- `GET /tenants/slug/{slug}` - Buscar por slug
|
||||
|
||||
### Licencias (`/v1/licenses`)
|
||||
- `POST /licenses` - Crear licencia
|
||||
- `GET /licenses/tenant/{id}` - Obtener licencia
|
||||
- `PUT /licenses/tenant/{id}` - Actualizar licencia
|
||||
- `GET /licenses/validate/{id}` - Validar licencia
|
||||
- `GET /licenses/usage/{id}` - Uso de licencia
|
||||
- `GET /licenses/my-license` - Mi licencia
|
||||
|
||||
## 🚀 Cómo Iniciar
|
||||
|
||||
### Opción 1: Script Automático (Recomendado)
|
||||
```bash
|
||||
cd /home/alexeer/dev/anexo76
|
||||
./start.sh
|
||||
```
|
||||
|
||||
### Opción 2: Manual
|
||||
```bash
|
||||
# 1. Copiar .env
|
||||
cp backend/.env.example backend/.env
|
||||
cp frontend/.env.example frontend/.env
|
||||
|
||||
# 2. Iniciar servicios
|
||||
docker-compose up -d
|
||||
|
||||
# 3. Esperar PostgreSQL
|
||||
sleep 10
|
||||
|
||||
# 4. Inicializar BD
|
||||
cd backend
|
||||
python3 init_db.py
|
||||
cd ..
|
||||
```
|
||||
|
||||
### Siguiente Paso: Configurar Keycloak
|
||||
Seguir la guía: `docs/KEYCLOAK_SETUP.md`
|
||||
|
||||
## 🔗 URLs de Acceso
|
||||
|
||||
| Servicio | URL | Credenciales |
|
||||
|----------|-----|--------------|
|
||||
| Frontend | http://localhost:5173 | Usuario Keycloak |
|
||||
| Backend API | http://localhost:8000 | Token JWT |
|
||||
| API Docs | http://localhost:8000/docs | - |
|
||||
| Keycloak | http://localhost:8080 | admin / admin |
|
||||
| PostgreSQL | localhost:5432 | postgres / postgres |
|
||||
|
||||
## 👤 Usuario de Prueba
|
||||
|
||||
Después de configurar Keycloak:
|
||||
- **Usuario**: `demo`
|
||||
- **Password**: `demo123`
|
||||
- **Tenant**: Empresa Demo (ID: 1)
|
||||
- **Licencia**: Professional (50 usuarios, 100GB)
|
||||
|
||||
## 📚 Documentación Generada
|
||||
|
||||
1. **README.md** - Visión general y quick start
|
||||
2. **docs/ARCHITECTURE.md** - Arquitectura técnica detallada
|
||||
3. **docs/KEYCLOAK_SETUP.md** - Guía paso a paso Keycloak
|
||||
4. **docs/TESTING_GUIDE.md** - Casos de prueba exhaustivos
|
||||
|
||||
## 🎨 Características de Diseño
|
||||
|
||||
### Arquitectura
|
||||
- **Modular**: Estilo NestJS con separación clara
|
||||
- **Escalable**: Multi-tenant híbrido
|
||||
- **Mantenible**: DTOs + Services + Routes
|
||||
- **Documentado**: Código auto-documentado + docs
|
||||
|
||||
### Patrones Implementados
|
||||
- **Repository Pattern** (implícito en services)
|
||||
- **DTO Pattern** (Pydantic models)
|
||||
- **Middleware Pattern** (tenant, license, logging)
|
||||
- **Dependency Injection** (FastAPI Depends)
|
||||
- **Store Pattern** (Svelte stores para auth)
|
||||
|
||||
## 🔮 Próximos Módulos a Implementar
|
||||
|
||||
Siguiendo la misma estructura, puedes agregar:
|
||||
|
||||
```
|
||||
backend/v1/modules/
|
||||
├── inventories/ # Gestión de inventarios
|
||||
│ ├── models.py
|
||||
│ ├── dto.py
|
||||
│ ├── service.py
|
||||
│ └── routes.py
|
||||
│
|
||||
├── pedimentos/ # Pedimentos aduanales
|
||||
├── invoices/ # Facturas
|
||||
├── reports/ # Reportes
|
||||
└── webhooks/ # Integraciones
|
||||
```
|
||||
|
||||
Cada módulo sigue el mismo patrón de 4 archivos.
|
||||
|
||||
## 💡 Consejos para Desarrollo
|
||||
|
||||
### Agregar Nuevo Módulo
|
||||
1. Crear carpeta en `backend/v1/modules/{nombre}`
|
||||
2. Crear 4 archivos: models.py, dto.py, service.py, routes.py
|
||||
3. Registrar router en `backend/v1/router.py`
|
||||
4. Crear migración de BD si hay modelos nuevos
|
||||
|
||||
### Agregar Nueva Ruta Frontend
|
||||
1. Crear carpeta en `frontend/src/routes/{ruta}`
|
||||
2. Crear `+page.svelte` para la página
|
||||
3. Usar `$isAuthenticated` para proteger ruta
|
||||
4. Usar `api.{modulo}.metodo()` para llamar backend
|
||||
|
||||
### Debugging
|
||||
- Backend: `docker-compose logs -f backend`
|
||||
- Frontend: Abrir DevTools (F12) en navegador
|
||||
- BD: `docker-compose exec postgres psql -U postgres -d anexo76_core`
|
||||
|
||||
## ✅ Checklist Post-Generación
|
||||
|
||||
- [x] Backend generado con 3 módulos completos
|
||||
- [x] Frontend con autenticación Keycloak
|
||||
- [x] Docker Compose configurado
|
||||
- [x] Base de datos con modelos multi-tenant
|
||||
- [x] Middlewares de seguridad y licencias
|
||||
- [x] DTOs con Pydantic para todos los módulos
|
||||
- [x] Documentación completa (4 archivos)
|
||||
- [x] Script de inicio automático
|
||||
- [x] .gitignore configurado
|
||||
- [x] README.md con instrucciones
|
||||
|
||||
## 🎓 Recursos de Aprendizaje
|
||||
|
||||
- **FastAPI**: https://fastapi.tiangolo.com
|
||||
- **Keycloak**: https://www.keycloak.org/docs
|
||||
- **SvelteKit**: https://svelte.dev/docs/kit
|
||||
- **SQLAlchemy**: https://docs.sqlalchemy.org
|
||||
- **Pydantic**: https://docs.pydantic.dev
|
||||
|
||||
## 🤝 Contribuir
|
||||
|
||||
El proyecto está listo para:
|
||||
- ✅ Agregar nuevos módulos
|
||||
- ✅ Implementar tests
|
||||
- ✅ Configurar CI/CD
|
||||
- ✅ Deploy a producción
|
||||
- ✅ Agregar monitoreo
|
||||
|
||||
---
|
||||
|
||||
## 🎊 ¡Proyecto Anexo76 Generado Exitosamente!
|
||||
|
||||
**Todo está listo para empezar a desarrollar.**
|
||||
|
||||
**Siguiente paso**: Ejecutar `./start.sh` y seguir `docs/KEYCLOAK_SETUP.md`
|
||||
|
||||
---
|
||||
|
||||
**Generado**: Octubre 2025
|
||||
**Stack**: FastAPI + SvelteKit + Keycloak + PostgreSQL
|
||||
**Arquitectura**: Multi-tenant Modular (estilo NestJS)
|
||||
@@ -69,3 +69,43 @@ class UserInfoResponseDTO(BaseModel):
|
||||
class LogoutRequestDTO(BaseModel):
|
||||
"""DTO para solicitud de logout"""
|
||||
refresh_token: str = Field(..., description="Refresh token para invalidar")
|
||||
|
||||
|
||||
class RegisterRequestDTO(BaseModel):
|
||||
"""DTO para solicitud de registro"""
|
||||
username: str = Field(..., min_length=3, max_length=50, description="Nombre de usuario")
|
||||
email: EmailStr = Field(..., description="Email del usuario")
|
||||
password: str = Field(..., min_length=8, description="Contraseña")
|
||||
first_name: str = Field(..., min_length=2, max_length=50, description="Nombre")
|
||||
last_name: str = Field(..., min_length=2, max_length=50, description="Apellido")
|
||||
tenant_slug: str = Field(..., description="Slug del tenant")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"username": "jperez",
|
||||
"email": "jperez@ejemplo.com",
|
||||
"password": "MiPassword123!",
|
||||
"first_name": "Juan",
|
||||
"last_name": "Pérez",
|
||||
"tenant_slug": "empresa-abc"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class RegisterResponseDTO(BaseModel):
|
||||
"""DTO para respuesta de registro"""
|
||||
user_id: str
|
||||
username: str
|
||||
email: str
|
||||
message: str
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"user_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||
"username": "jperez",
|
||||
"email": "jperez@ejemplo.com",
|
||||
"message": "User registered successfully"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ from .dto import (
|
||||
TokenResponseDTO,
|
||||
RefreshTokenRequestDTO,
|
||||
UserInfoResponseDTO,
|
||||
LogoutRequestDTO
|
||||
LogoutRequestDTO,
|
||||
RegisterRequestDTO,
|
||||
RegisterResponseDTO
|
||||
)
|
||||
from .service import AuthService
|
||||
|
||||
@@ -20,6 +22,31 @@ router = APIRouter(prefix="/auth", tags=["Authentication"])
|
||||
security = HTTPBearer()
|
||||
|
||||
|
||||
@router.post("/register", response_model=RegisterResponseDTO, status_code=201)
|
||||
async def register(
|
||||
register_data: RegisterRequestDTO,
|
||||
db: Session = Depends(get_core_db)
|
||||
):
|
||||
"""
|
||||
Registra un nuevo usuario en Keycloak
|
||||
|
||||
El usuario debe proporcionar:
|
||||
- username: Nombre de usuario único
|
||||
- email: Email único
|
||||
- password: Contraseña (mínimo 8 caracteres)
|
||||
- first_name: Nombre
|
||||
- last_name: Apellido
|
||||
- tenant_slug: Slug del tenant al que pertenece
|
||||
|
||||
El usuario se crea automáticamente en Keycloak con:
|
||||
- Cuenta habilitada
|
||||
- Rol 'user' asignado por defecto
|
||||
- Atributos de tenant
|
||||
"""
|
||||
service = AuthService(db)
|
||||
return service.register(register_data)
|
||||
|
||||
|
||||
@router.post("/login", response_model=TokenResponseDTO)
|
||||
async def login(
|
||||
login_data: LoginRequestDTO,
|
||||
|
||||
@@ -13,7 +13,9 @@ from .dto import (
|
||||
TokenResponseDTO,
|
||||
RefreshTokenRequestDTO,
|
||||
UserInfoResponseDTO,
|
||||
LogoutRequestDTO
|
||||
LogoutRequestDTO,
|
||||
RegisterRequestDTO,
|
||||
RegisterResponseDTO
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -56,13 +58,19 @@ class AuthService:
|
||||
if not tenant.is_active:
|
||||
raise HTTPException(status_code=403, detail="Tenant is not active")
|
||||
|
||||
# Cambiar realm al del tenant
|
||||
self.keycloak_openid.realm_name = tenant.keycloak_realm
|
||||
# Crear nueva instancia de KeycloakOpenID con el realm del tenant
|
||||
keycloak_client = KeycloakOpenID(
|
||||
server_url=settings.KEYCLOAK_SERVER_URL,
|
||||
client_id=settings.KEYCLOAK_CLIENT_ID,
|
||||
realm_name=tenant.keycloak_realm,
|
||||
client_secret_key=settings.KEYCLOAK_CLIENT_SECRET
|
||||
)
|
||||
|
||||
# Obtener token de Keycloak
|
||||
token_response = self.keycloak_openid.token(
|
||||
token_response = keycloak_client.token(
|
||||
username=login_data.username,
|
||||
password=login_data.password
|
||||
password=login_data.password,
|
||||
grant_type=["password"]
|
||||
)
|
||||
|
||||
logger.info(f"User logged in: {login_data.username} (tenant: {tenant.slug})")
|
||||
@@ -173,3 +181,97 @@ class AuthService:
|
||||
except Exception as e:
|
||||
logger.error(f"Logout error: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Logout error")
|
||||
|
||||
def register(self, register_data: RegisterRequestDTO) -> RegisterResponseDTO:
|
||||
"""
|
||||
Registra un nuevo usuario en Keycloak
|
||||
|
||||
Args:
|
||||
register_data: Datos del usuario a registrar
|
||||
|
||||
Returns:
|
||||
RegisterResponseDTO con información del usuario creado
|
||||
|
||||
Raises:
|
||||
HTTPException: Si el registro falla
|
||||
"""
|
||||
try:
|
||||
# Verificar que el tenant existe
|
||||
from api.v1.modules.a76.tenants.service import TenantService
|
||||
tenant_service = TenantService(self.db)
|
||||
tenant = tenant_service.get_tenant_by_slug(register_data.tenant_slug)
|
||||
|
||||
if not tenant:
|
||||
raise HTTPException(status_code=404, detail="Tenant not found")
|
||||
|
||||
if not tenant.is_active:
|
||||
raise HTTPException(status_code=403, detail="Tenant is not active")
|
||||
|
||||
# Crear instancia de KeycloakAdmin para gestión de usuarios
|
||||
keycloak_admin = KeycloakAdmin(
|
||||
server_url=settings.KEYCLOAK_SERVER_URL,
|
||||
username=settings.KEYCLOAK_ADMIN_USERNAME,
|
||||
password=settings.KEYCLOAK_ADMIN_PASSWORD,
|
||||
realm_name=tenant.keycloak_realm,
|
||||
user_realm_name="master", # El admin suele estar en master realm
|
||||
verify=True
|
||||
)
|
||||
|
||||
# Preparar datos del usuario para Keycloak
|
||||
user_data = {
|
||||
"username": register_data.username,
|
||||
"email": register_data.email,
|
||||
"firstName": register_data.first_name,
|
||||
"lastName": register_data.last_name,
|
||||
"enabled": True,
|
||||
"emailVerified": False,
|
||||
"credentials": [{
|
||||
"type": "password",
|
||||
"value": register_data.password,
|
||||
"temporary": False
|
||||
}],
|
||||
"attributes": {
|
||||
"tenant_id": str(tenant.id),
|
||||
"tenant_slug": tenant.slug
|
||||
}
|
||||
}
|
||||
|
||||
# Crear usuario en Keycloak
|
||||
user_id = keycloak_admin.create_user(user_data)
|
||||
|
||||
# Asignar rol por defecto (user) - opcional, solo si existe
|
||||
try:
|
||||
user_role = keycloak_admin.get_realm_role("user")
|
||||
if user_role:
|
||||
keycloak_admin.assign_realm_roles(user_id, [user_role])
|
||||
logger.info(f"Assigned 'user' role to {register_data.username}")
|
||||
except KeycloakError as e:
|
||||
# El rol 'user' no existe, no es un error crítico
|
||||
logger.warning(f"Could not assign 'user' role: {str(e)}")
|
||||
|
||||
logger.info(f"User registered: {register_data.username} (tenant: {tenant.slug}, user_id: {user_id})")
|
||||
|
||||
return RegisterResponseDTO(
|
||||
user_id=user_id,
|
||||
username=register_data.username,
|
||||
email=register_data.email,
|
||||
message="User registered successfully"
|
||||
)
|
||||
|
||||
except KeycloakError as e:
|
||||
error_message = str(e)
|
||||
logger.warning(f"Keycloak registration failed: {error_message}")
|
||||
|
||||
# Mensajes de error más específicos
|
||||
if "User exists" in error_message or "409" in error_message:
|
||||
raise HTTPException(status_code=409, detail="Username or email already exists")
|
||||
elif "Invalid" in error_message:
|
||||
raise HTTPException(status_code=400, detail="Invalid user data")
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail="Registration error")
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Registration error: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Registration error")
|
||||
|
||||
@@ -26,6 +26,8 @@ class Settings(BaseSettings):
|
||||
KEYCLOAK_REALM: str = "master"
|
||||
KEYCLOAK_CLIENT_ID: str = "anexo76-backend"
|
||||
KEYCLOAK_CLIENT_SECRET: str = ""
|
||||
KEYCLOAK_ADMIN_USERNAME: str = "admin"
|
||||
KEYCLOAK_ADMIN_PASSWORD: str = "admin"
|
||||
|
||||
# Security
|
||||
SECRET_KEY: str = "change-this-secret-key-in-production"
|
||||
|
||||
@@ -29,7 +29,9 @@ class TenantMiddleware(BaseHTTPMiddleware):
|
||||
"/docs",
|
||||
"/redoc",
|
||||
"/openapi.json",
|
||||
"/api/v1/auth",
|
||||
"/v1/auth",
|
||||
"/api/v1/status",
|
||||
"/v1/status",
|
||||
"/health",
|
||||
"/"
|
||||
@@ -86,7 +88,9 @@ class LicenseValidationMiddleware(BaseHTTPMiddleware):
|
||||
"/docs",
|
||||
"/redoc",
|
||||
"/openapi.json",
|
||||
"/api/v1/auth",
|
||||
"/v1/auth",
|
||||
"/api/v1/status",
|
||||
"/v1/status",
|
||||
"/health",
|
||||
"/"
|
||||
@@ -112,7 +116,7 @@ class LicenseValidationMiddleware(BaseHTTPMiddleware):
|
||||
db = CoreSessionLocal()
|
||||
try:
|
||||
# Importar aquí para evitar imports circulares
|
||||
from api.v1.modules.licenses.service import LicenseService
|
||||
from api.v1.modules.a76.licenses.service import LicenseService
|
||||
|
||||
license_service = LicenseService(db)
|
||||
license_info = license_service.validate_license(tenant_id)
|
||||
|
||||
@@ -1,417 +0,0 @@
|
||||
# Guía de Prueba Rápida - Anexo76
|
||||
|
||||
Esta guía te ayudará a probar todas las funcionalidades básicas de Anexo76 después de la instalación.
|
||||
|
||||
## Prerrequisitos
|
||||
|
||||
✅ Haber ejecutado `./start.sh` exitosamente
|
||||
✅ Haber configurado Keycloak siguiendo `docs/KEYCLOAK_SETUP.md`
|
||||
✅ Tener los servicios corriendo
|
||||
|
||||
## Verificar Estado de Servicios
|
||||
|
||||
```bash
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
Deberías ver 4 servicios en estado "Up":
|
||||
- postgres
|
||||
- keycloak
|
||||
- backend
|
||||
- frontend
|
||||
|
||||
## 1. Probar Backend API
|
||||
|
||||
### Health Check
|
||||
```bash
|
||||
curl http://localhost:8000/health
|
||||
```
|
||||
|
||||
Respuesta esperada:
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"environment": "development"
|
||||
}
|
||||
```
|
||||
|
||||
### Status de API
|
||||
```bash
|
||||
curl http://localhost:8000/v1/status
|
||||
```
|
||||
|
||||
Respuesta esperada:
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"version": "1.0.0",
|
||||
"api": "v1"
|
||||
}
|
||||
```
|
||||
|
||||
### Documentación Interactiva
|
||||
Abrir en navegador: http://localhost:8000/docs
|
||||
|
||||
Deberías ver la interfaz Swagger UI con todos los endpoints documentados.
|
||||
|
||||
## 2. Probar Autenticación con Keycloak
|
||||
|
||||
### Obtener Token (vía API directa)
|
||||
|
||||
```bash
|
||||
# Reemplaza YOUR_CLIENT_SECRET con el secret de Keycloak
|
||||
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=YOUR_CLIENT_SECRET" \
|
||||
-d "username=demo" \
|
||||
-d "password=demo123" \
|
||||
-d "grant_type=password"
|
||||
```
|
||||
|
||||
Respuesta esperada (fragmento):
|
||||
```json
|
||||
{
|
||||
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"expires_in": 300,
|
||||
"refresh_expires_in": 1800,
|
||||
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"token_type": "Bearer"
|
||||
}
|
||||
```
|
||||
|
||||
### Usar Token para Llamar API
|
||||
|
||||
```bash
|
||||
# Guarda el access_token en una variable
|
||||
TOKEN="tu-access-token-aqui"
|
||||
|
||||
# Llamar endpoint protegido
|
||||
curl -X GET http://localhost:8000/v1/auth/me \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
Respuesta esperada:
|
||||
```json
|
||||
{
|
||||
"sub": "a1b2c3d4-...",
|
||||
"email": "demo@empresa-demo.com",
|
||||
"name": "Usuario Demo",
|
||||
"preferred_username": "demo",
|
||||
"tenant_id": 1,
|
||||
"roles": ["user", "admin"]
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Probar Módulo de Tenants
|
||||
|
||||
### Listar Tenants (requiere rol admin)
|
||||
```bash
|
||||
curl -X GET http://localhost:8000/v1/tenants \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
Respuesta esperada:
|
||||
```json
|
||||
{
|
||||
"tenants": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Empresa Demo S.A. de C.V.",
|
||||
"slug": "empresa-demo",
|
||||
"type": "shared",
|
||||
"keycloak_realm": "master",
|
||||
"is_active": true,
|
||||
...
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
"page_size": 50
|
||||
}
|
||||
```
|
||||
|
||||
### Obtener Tenant por ID
|
||||
```bash
|
||||
curl -X GET http://localhost:8000/v1/tenants/1 \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
### Obtener Tenant por Slug
|
||||
```bash
|
||||
curl -X GET http://localhost:8000/v1/tenants/slug/empresa-demo \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
## 4. Probar Módulo de Licencias
|
||||
|
||||
### Obtener Mi Licencia
|
||||
```bash
|
||||
curl -X GET http://localhost:8000/v1/licenses/my-license \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
Respuesta esperada:
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"tenant_id": 1,
|
||||
"plan": "professional",
|
||||
"status": "active",
|
||||
"max_users": 50,
|
||||
"max_storage_gb": 100,
|
||||
"max_monthly_operations": 25000,
|
||||
"feature_api_access": true,
|
||||
"feature_advanced_reports": true,
|
||||
"feature_integrations": true,
|
||||
"feature_dedicated_support": false,
|
||||
"expires_at": "2026-10-17T...",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Validar Licencia
|
||||
```bash
|
||||
curl -X GET http://localhost:8000/v1/licenses/validate/1 \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
Respuesta esperada:
|
||||
```json
|
||||
{
|
||||
"is_valid": true,
|
||||
"status": "active",
|
||||
"plan": "professional",
|
||||
"expires_at": "2026-10-17T...",
|
||||
"reason": null
|
||||
}
|
||||
```
|
||||
|
||||
### Obtener Uso de Licencia
|
||||
```bash
|
||||
curl -X GET http://localhost:8000/v1/licenses/usage/1 \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
## 5. Probar Frontend
|
||||
|
||||
### Abrir Aplicación
|
||||
Abrir en navegador: http://localhost:5173
|
||||
|
||||
### Probar Login
|
||||
1. Click en botón "Iniciar Sesión"
|
||||
2. Serás redirigido a Keycloak
|
||||
3. Ingresar credenciales:
|
||||
- Usuario: `demo`
|
||||
- Password: `demo123`
|
||||
4. Deberías ser redirigido de vuelta al dashboard
|
||||
|
||||
### Verificar Dashboard
|
||||
Después del login, deberías ver:
|
||||
- ✅ Nombre y email del usuario
|
||||
- ✅ Información de licencia (plan, estado, límites)
|
||||
- ✅ Información de usuario (ID, roles, tenant ID)
|
||||
- ✅ Botón "Cerrar Sesión"
|
||||
|
||||
### Probar Logout
|
||||
1. Click en "Cerrar Sesión"
|
||||
2. Deberías volver a la pantalla de bienvenida
|
||||
|
||||
## 6. Pruebas de Middleware
|
||||
|
||||
### Probar sin Token (debe fallar)
|
||||
```bash
|
||||
curl -X GET http://localhost:8000/v1/tenants/1
|
||||
```
|
||||
|
||||
Respuesta esperada (error 401):
|
||||
```json
|
||||
{
|
||||
"detail": "Missing or invalid authorization header"
|
||||
}
|
||||
```
|
||||
|
||||
### Probar con Token Inválido (debe fallar)
|
||||
```bash
|
||||
curl -X GET http://localhost:8000/v1/tenants/1 \
|
||||
-H "Authorization: Bearer token-invalido"
|
||||
```
|
||||
|
||||
Respuesta esperada (error 401):
|
||||
```json
|
||||
{
|
||||
"detail": "Could not validate credentials"
|
||||
}
|
||||
```
|
||||
|
||||
### Probar sin Tenant ID en Token (debe fallar)
|
||||
Si el token no tiene `tenant_id`, debería recibir error 400:
|
||||
```json
|
||||
{
|
||||
"detail": "Tenant ID not found in token"
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Verificar Logs
|
||||
|
||||
### Ver logs de todos los servicios
|
||||
```bash
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
### Ver logs solo del backend
|
||||
```bash
|
||||
docker-compose logs -f backend
|
||||
```
|
||||
|
||||
### Ver logs solo del frontend
|
||||
```bash
|
||||
docker-compose logs -f frontend
|
||||
```
|
||||
|
||||
### Ver logs de PostgreSQL
|
||||
```bash
|
||||
docker-compose logs -f postgres
|
||||
```
|
||||
|
||||
## 8. Probar Base de Datos
|
||||
|
||||
### Conectarse a PostgreSQL
|
||||
```bash
|
||||
docker-compose exec postgres psql -U postgres -d anexo76_core
|
||||
```
|
||||
|
||||
### Consultas útiles
|
||||
```sql
|
||||
-- Ver tenants
|
||||
SELECT * FROM tenants;
|
||||
|
||||
-- Ver licencias
|
||||
SELECT * FROM licenses;
|
||||
|
||||
-- Ver información de licencia con tenant
|
||||
SELECT t.name, t.slug, l.plan, l.status, l.expires_at
|
||||
FROM tenants t
|
||||
JOIN licenses l ON l.tenant_id = t.id;
|
||||
|
||||
-- Salir
|
||||
\q
|
||||
```
|
||||
|
||||
## 9. Casos de Prueba Adicionales
|
||||
|
||||
### Crear Nuevo Tenant (requiere rol admin)
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/v1/tenants \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Nueva Empresa S.A.",
|
||||
"slug": "nueva-empresa",
|
||||
"keycloak_realm": "master",
|
||||
"type": "shared",
|
||||
"contact_name": "Juan Pérez",
|
||||
"contact_email": "juan@nueva-empresa.com"
|
||||
}'
|
||||
```
|
||||
|
||||
### Actualizar Tenant
|
||||
```bash
|
||||
curl -X PUT http://localhost:8000/v1/tenants/1 \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"contact_phone": "+52 55 9999 8888"
|
||||
}'
|
||||
```
|
||||
|
||||
### Crear Licencia para Tenant
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/v1/licenses \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"tenant_id": 2,
|
||||
"plan": "basic",
|
||||
"max_users": 20,
|
||||
"max_storage_gb": 50,
|
||||
"max_monthly_operations": 10000,
|
||||
"starts_at": "2025-10-17T00:00:00Z",
|
||||
"expires_at": "2026-10-17T23:59:59Z"
|
||||
}'
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Error: "Connection refused" al llamar API
|
||||
- Verificar que el backend esté corriendo: `docker-compose ps`
|
||||
- Ver logs: `docker-compose logs backend`
|
||||
- Reiniciar: `docker-compose restart backend`
|
||||
|
||||
### Error: "Tenant ID not found in token"
|
||||
- Verificar que el usuario en Keycloak tenga el atributo `tenant_id` configurado
|
||||
- Verificar que el mapper de Keycloak esté configurado correctamente
|
||||
|
||||
### Frontend muestra "Cargando" indefinidamente
|
||||
- Abrir consola del navegador (F12) y revisar errores
|
||||
- Verificar que Keycloak esté accesible: http://localhost:8080
|
||||
- Verificar configuración en `frontend/.env`
|
||||
|
||||
### Base de datos vacía
|
||||
- Ejecutar script de inicialización:
|
||||
```bash
|
||||
docker-compose exec backend python init_db.py
|
||||
```
|
||||
|
||||
### Keycloak no responde
|
||||
- Esperar unos minutos (puede tardar en iniciar)
|
||||
- Ver logs: `docker-compose logs keycloak`
|
||||
- Reiniciar: `docker-compose restart keycloak`
|
||||
|
||||
## Limpiar y Reiniciar
|
||||
|
||||
### Detener todo
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### Detener y eliminar volúmenes (borra BD)
|
||||
```bash
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
### Reiniciar desde cero
|
||||
```bash
|
||||
docker-compose down -v
|
||||
./start.sh
|
||||
```
|
||||
|
||||
## Checklist de Verificación
|
||||
|
||||
- [ ] Backend responde en http://localhost:8000
|
||||
- [ ] Frontend carga en http://localhost:5173
|
||||
- [ ] Keycloak accesible en http://localhost:8080
|
||||
- [ ] Documentación API visible en http://localhost:8000/docs
|
||||
- [ ] Login funciona correctamente
|
||||
- [ ] Dashboard muestra información de usuario
|
||||
- [ ] Dashboard muestra información de licencia
|
||||
- [ ] Logout funciona correctamente
|
||||
- [ ] API responde a peticiones con token válido
|
||||
- [ ] API rechaza peticiones sin token
|
||||
- [ ] Base de datos tiene tenant y licencia de prueba
|
||||
|
||||
## Próximos Pasos
|
||||
|
||||
Una vez que todas las pruebas pasen:
|
||||
|
||||
1. ✅ Revisar la documentación en `docs/ARCHITECTURE.md`
|
||||
2. ✅ Explorar el código fuente de los módulos
|
||||
3. ✅ Personalizar configuración según necesidades
|
||||
4. ✅ Comenzar a desarrollar módulos adicionales
|
||||
5. ✅ Configurar ambiente de producción
|
||||
|
||||
---
|
||||
|
||||
**¿Problemas?** Revisa los logs con `docker-compose logs -f` o abre un issue.
|
||||
|
||||
**¡Todo funciona!** 🎉 Estás listo para desarrollar sobre Anexo76.
|
||||
1
frontend/project.inlang/.gitignore
vendored
Normal file
1
frontend/project.inlang/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
cache
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { initKeycloak, authStore } from '$lib/auth';
|
||||
import { initAuth, authStore as customAuthStore } from '$lib/auth';
|
||||
import '../app.css';
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
let initialized = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
// Inicializar Keycloak al cargar la aplicación
|
||||
await initKeycloak();
|
||||
// Usar auth-custom que es instantáneo (sin peticiones de red)
|
||||
await initAuth();
|
||||
initialized = true;
|
||||
});
|
||||
</script>
|
||||
@@ -18,7 +18,7 @@
|
||||
<link rel="icon" href={favicon} />
|
||||
</svelte:head>
|
||||
|
||||
{#if initialized && !$authStore.isLoading}
|
||||
{#if initialized}
|
||||
{@render children?.()}
|
||||
{:else}
|
||||
<div class="flex min-h-screen items-center justify-center bg-gray-50">
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { isAuthenticated, currentUser, login, logout } from '$lib/auth';
|
||||
import { isAuthenticated, currentUser, logout } from '$lib/auth';
|
||||
import { api } from '$lib/api';
|
||||
|
||||
let licenseInfo: any = $state(null);
|
||||
let loadingLicense = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
// Si está autenticado, cargar información de licencia
|
||||
// La autenticación ya se inicializó en +layout.svelte
|
||||
// Solo cargar información de licencia si está autenticado
|
||||
if ($isAuthenticated && $currentUser?.tenantId) {
|
||||
await loadLicenseInfo();
|
||||
loadLicenseInfo(); // Sin await - carga en background
|
||||
}
|
||||
});
|
||||
|
||||
@@ -24,7 +25,7 @@
|
||||
}
|
||||
|
||||
async function handleLogin() {
|
||||
await login();
|
||||
goto('/login');
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
@@ -50,14 +51,14 @@
|
||||
</div>
|
||||
<button
|
||||
onclick={handleLogout}
|
||||
class="rounded-md bg-red-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
|
||||
class="rounded-md bg-red-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-offset-2 focus-visible:outline-red-600"
|
||||
>
|
||||
Cerrar Sesión
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
onclick={handleLogin}
|
||||
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
>
|
||||
Iniciar Sesión
|
||||
</button>
|
||||
@@ -81,9 +82,15 @@
|
||||
Ideal para maquilas, empresas IMMEX y agentes aduanales.
|
||||
</p>
|
||||
<div class="mt-10 flex items-center justify-center gap-x-6">
|
||||
<a
|
||||
href="/register"
|
||||
class="rounded-md border-2 border-blue-600 px-6 py-3 text-base font-semibold text-blue-600 hover:bg-blue-50 focus-visible:outline focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
>
|
||||
Registrarse
|
||||
</a>
|
||||
<button
|
||||
onclick={handleLogin}
|
||||
class="rounded-md bg-blue-600 px-6 py-3 text-base font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
class="rounded-md bg-blue-600 px-6 py-3 text-base font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
>
|
||||
Iniciar Sesión
|
||||
</button>
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { authStore } from '$lib/auth';
|
||||
|
||||
onMount(async () => {
|
||||
// Esperar a que Keycloak procese el callback
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Redirigir al home
|
||||
if ($authStore.isAuthenticated) {
|
||||
goto('/');
|
||||
} else {
|
||||
goto('/?error=auth_failed');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex min-h-screen items-center justify-center bg-gray-50">
|
||||
<div class="text-center">
|
||||
<div class="mb-4 inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-blue-600 border-r-transparent"></div>
|
||||
<p class="text-gray-600">Procesando autenticación...</p>
|
||||
</div>
|
||||
</div>
|
||||
120
frontend/src/routes/login/+page.svelte
Normal file
120
frontend/src/routes/login/+page.svelte
Normal file
@@ -0,0 +1,120 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { login } from '$lib/auth';
|
||||
|
||||
let username = $state('demo');
|
||||
let password = $state('demo123');
|
||||
let tenantSlug = $state('aduanasoft');
|
||||
let error = $state('');
|
||||
let loading = $state(false);
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
error = '';
|
||||
loading = true;
|
||||
|
||||
const result = await login({
|
||||
username,
|
||||
password,
|
||||
tenant_slug: tenantSlug
|
||||
});
|
||||
|
||||
loading = false;
|
||||
|
||||
if (result.success) {
|
||||
goto('/');
|
||||
} else {
|
||||
error = result.error || 'Error de autenticación';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||
Anexo76
|
||||
</h2>
|
||||
<p class="mt-2 text-center text-sm text-gray-600">
|
||||
Inicia sesión en tu cuenta
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form class="mt-8 space-y-6" onsubmit={handleSubmit}>
|
||||
{#if error}
|
||||
<div class="rounded-md bg-red-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">
|
||||
{error}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="rounded-md shadow-sm -space-y-px">
|
||||
<div>
|
||||
<label for="tenant-slug" class="sr-only">Tenant</label>
|
||||
<input
|
||||
id="tenant-slug"
|
||||
name="tenant"
|
||||
type="text"
|
||||
required
|
||||
bind:value={tenantSlug}
|
||||
disabled={loading}
|
||||
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Tenant (empresa)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="username" class="sr-only">Usuario</label>
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
required
|
||||
bind:value={username}
|
||||
disabled={loading}
|
||||
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Usuario"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="password" class="sr-only">Contraseña</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
bind:value={password}
|
||||
disabled={loading}
|
||||
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Contraseña"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{#if loading}
|
||||
Iniciando sesión...
|
||||
{:else}
|
||||
Iniciar sesión
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<p class="text-xs text-gray-500">
|
||||
Credenciales de prueba:<br />
|
||||
Usuario: demo | Contraseña: demo123 | Tenant: aduanasoft
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
237
frontend/src/routes/register/+page.svelte
Normal file
237
frontend/src/routes/register/+page.svelte
Normal file
@@ -0,0 +1,237 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { api } from '$lib/api';
|
||||
|
||||
let formData = $state({
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
tenant_slug: 'aduanasoft' // Por defecto
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state('');
|
||||
let passwordError = $state('');
|
||||
|
||||
async function handleRegister(e: Event) {
|
||||
e.preventDefault();
|
||||
error = '';
|
||||
passwordError = '';
|
||||
|
||||
// Validar que las contraseñas coincidan
|
||||
if (formData.password !== formData.confirmPassword) {
|
||||
passwordError = 'Las contraseñas no coinciden';
|
||||
return;
|
||||
}
|
||||
|
||||
// Validar longitud de contraseña
|
||||
if (formData.password.length < 8) {
|
||||
passwordError = 'La contraseña debe tener al menos 8 caracteres';
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
const response = await api.auth.register({
|
||||
username: formData.username,
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
first_name: formData.first_name,
|
||||
last_name: formData.last_name,
|
||||
tenant_slug: formData.tenant_slug
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
// Registro exitoso, redirigir al login
|
||||
alert(`¡Registro exitoso! Bienvenido ${response.data.username}`);
|
||||
goto('/login');
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al registrar usuario';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
goto('/');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="mx-auto max-w-md">
|
||||
<!-- Header -->
|
||||
<div class="text-center">
|
||||
<h2 class="text-3xl font-bold tracking-tight text-gray-900">Crear cuenta</h2>
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
¿Ya tienes una cuenta?
|
||||
<a href="/login" class="font-medium text-blue-600 hover:text-blue-500">
|
||||
Inicia sesión
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Formulario de registro -->
|
||||
<div class="mt-8">
|
||||
<div class="rounded-lg bg-white px-6 py-8 shadow">
|
||||
<form class="space-y-6" onsubmit={handleRegister}>
|
||||
<!-- Username -->
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700">
|
||||
Usuario
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
bind:value={formData.username}
|
||||
required
|
||||
minlength="3"
|
||||
maxlength="50"
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500"
|
||||
placeholder="usuario123"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
bind:value={formData.email}
|
||||
required
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500"
|
||||
placeholder="usuario@ejemplo.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Nombre -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="first_name" class="block text-sm font-medium text-gray-700">
|
||||
Nombre
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="first_name"
|
||||
bind:value={formData.first_name}
|
||||
required
|
||||
minlength="2"
|
||||
maxlength="50"
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500"
|
||||
placeholder="Juan"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="last_name" class="block text-sm font-medium text-gray-700">
|
||||
Apellido
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="last_name"
|
||||
bind:value={formData.last_name}
|
||||
required
|
||||
minlength="2"
|
||||
maxlength="50"
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500"
|
||||
placeholder="Pérez"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contraseña -->
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700">
|
||||
Contraseña
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
bind:value={formData.password}
|
||||
required
|
||||
minlength="8"
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500"
|
||||
placeholder="Mínimo 8 caracteres"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Confirmar contraseña -->
|
||||
<div>
|
||||
<label for="confirmPassword" class="block text-sm font-medium text-gray-700">
|
||||
Confirmar contraseña
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="confirmPassword"
|
||||
bind:value={formData.confirmPassword}
|
||||
required
|
||||
minlength="8"
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500"
|
||||
placeholder="Repite tu contraseña"
|
||||
/>
|
||||
{#if passwordError}
|
||||
<p class="mt-1 text-sm text-red-600">{passwordError}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Tenant -->
|
||||
<div>
|
||||
<label for="tenant_slug" class="block text-sm font-medium text-gray-700">
|
||||
Empresa
|
||||
</label>
|
||||
<select
|
||||
id="tenant_slug"
|
||||
bind:value={formData.tenant_slug}
|
||||
required
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500"
|
||||
>
|
||||
<option value="aduanasoft">AduanaSoft</option>
|
||||
<!-- Agregar más tenants aquí -->
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Errores -->
|
||||
{#if error}
|
||||
<div class="rounded-md bg-red-50 p-4">
|
||||
<p class="text-sm text-red-800">{error}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Botones -->
|
||||
<div class="flex gap-4">
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleCancel}
|
||||
disabled={loading}
|
||||
class="flex-1 rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-semibold text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
class="flex-1 rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Registrando...' : 'Registrarse'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Info adicional -->
|
||||
<div class="mt-6 border-t border-gray-200 pt-6">
|
||||
<p class="text-xs text-gray-500">
|
||||
Al registrarte, aceptas nuestros términos de servicio y política de privacidad.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
2
start.sh
2
start.sh
@@ -92,7 +92,7 @@ echo ""
|
||||
|
||||
# 3. Limpiar contenedores previos si existen
|
||||
echo -e "${BLUE}[3/7] Limpiando contenedores previos...${NC}"
|
||||
docker-compose down -v 2>/dev/null || true
|
||||
docker-compose down 2>/dev/null || true
|
||||
echo -e "${GREEN}✓ Contenedores previos limpiados${NC}"
|
||||
echo ""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user