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:
2025-10-19 21:05:07 -05:00
parent 58a2f0ade6
commit f460c2b8aa
16 changed files with 559 additions and 1090 deletions

View File

@@ -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! 🚀
═══════════════════════════════════════════════════════════════════════════════

View File

@@ -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)

View File

@@ -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"
}
}

View File

@@ -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,

View File

@@ -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")

View File

@@ -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"

View File

@@ -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)

View File

@@ -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
View File

@@ -0,0 +1 @@
cache

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View 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>

View 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>

View File

@@ -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 ""