chore: Limpieza de proyecto y optimización
🗑️ Eliminados: - Scripts temporales de debugging (9 archivos en backend/) - Archivos temporales en raíz (4 archivos) - Reportes de cobertura htmlcov/ (~543 KB) - Directorio backups/ - Script de migración update_password_hashes.py ✨ Optimizaciones: - Creado scripts/db_utils.py - Herramienta consolidada para administración - Actualizado README.md con sección de Utilidades Administrativas - Mejorado .gitignore para prevenir archivos temporales futuros 📦 Espacio liberado: ~600-800 KB Las funcionalidades de debugging ahora están consolidadas en: - scripts/db_utils.py (herramienta CLI profesional) - Documentación en README.md - Alternativas sugeridas (psql, tests, API docs)
This commit is contained in:
26
.gitignore
vendored
26
.gitignore
vendored
@@ -30,3 +30,29 @@ docker-compose.override.yml
|
|||||||
|
|
||||||
# Uploads
|
# Uploads
|
||||||
uploads/
|
uploads/
|
||||||
|
|
||||||
|
# Test Coverage
|
||||||
|
htmlcov/
|
||||||
|
.coverage
|
||||||
|
*.cover
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Backups
|
||||||
|
backups/
|
||||||
|
*.backup
|
||||||
|
*.bak
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
temp_*.txt
|
||||||
|
temp_*.py
|
||||||
|
*.tmp
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Debug/Test scripts (usar scripts/ en su lugar)
|
||||||
|
check_*.py
|
||||||
|
fix_*.py
|
||||||
|
list_*.py
|
||||||
|
add_*.py
|
||||||
|
set_*.py
|
||||||
|
test_*.ps1
|
||||||
|
|||||||
34
README.md
34
README.md
@@ -94,6 +94,40 @@ docker-compose ps
|
|||||||
- API Docs: http://localhost:8000/docs
|
- API Docs: http://localhost:8000/docs
|
||||||
- Adminer (DB): http://localhost:8080
|
- Adminer (DB): http://localhost:8080
|
||||||
|
|
||||||
|
## Utilidades Administrativas
|
||||||
|
|
||||||
|
Para gestión y debugging de la base de datos, usa el script consolidado:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ver todos los comandos disponibles
|
||||||
|
python scripts/db_utils.py --help
|
||||||
|
|
||||||
|
# Listar todos los usuarios
|
||||||
|
python scripts/db_utils.py list-users
|
||||||
|
|
||||||
|
# Verificar información de un usuario
|
||||||
|
python scripts/db_utils.py check-user admin@example.com
|
||||||
|
|
||||||
|
# Resetear contraseña de un usuario
|
||||||
|
python scripts/db_utils.py reset-password admin@example.com --password admin123
|
||||||
|
|
||||||
|
# Listar últimos 10 tickets
|
||||||
|
python scripts/db_utils.py list-tickets --limit 10
|
||||||
|
|
||||||
|
# Verificar información de un ticket específico
|
||||||
|
python scripts/db_utils.py check-ticket <TICKET_ID>
|
||||||
|
|
||||||
|
# Filtrar por tenant
|
||||||
|
python scripts/db_utils.py list-users --tenant-id <TENANT_UUID>
|
||||||
|
python scripts/db_utils.py list-tickets --tenant-id <TENANT_UUID>
|
||||||
|
```
|
||||||
|
|
||||||
|
**💡 Alternativas para debugging:**
|
||||||
|
- **PostgreSQL directo**: Conectarte con pgAdmin, DBeaver o `psql`
|
||||||
|
- **Python Shell**: `python -m asyncio` desde el directorio backend
|
||||||
|
- **Tests**: Crear tests específicos en `backend/tests/`
|
||||||
|
- **API Docs**: Usar Swagger UI en http://localhost:8000/docs
|
||||||
|
|
||||||
## Scripts de Desarrollo
|
## Scripts de Desarrollo
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
254
RELEASE_NOTES_v1.5.1.md
Normal file
254
RELEASE_NOTES_v1.5.1.md
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
# Release Notes - ServiceManagerWeb v1.5.1
|
||||||
|
**Fecha**: 12 de Febrero, 2026
|
||||||
|
**Rama**: main
|
||||||
|
**Commit**: 771b6eb
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Información de la Versión
|
||||||
|
|
||||||
|
**Versión Anterior**: v1.4.1.4
|
||||||
|
**Versión Actual**: v1.5.1
|
||||||
|
**Tipo de Release**: Minor (Funcionalidades + Correcciones Críticas)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Seguridad y Control de Acceso
|
||||||
|
|
||||||
|
### Control de Acceso Basado en Roles (RBAC)
|
||||||
|
|
||||||
|
#### Implementación Completa
|
||||||
|
- **Staff Interno** (ADMIN, AGENT, SUPPORT_MANAGER)
|
||||||
|
- ✅ Acceso a todos los tickets del tenant
|
||||||
|
- ✅ Puede ver/modificar cualquier ticket
|
||||||
|
- ✅ Control total sobre recursos compartidos
|
||||||
|
|
||||||
|
- **Clientes** (CLIENT_USER, CLIENT_ADMIN)
|
||||||
|
- ✅ Acceso solo a sus propios tickets
|
||||||
|
- ✅ No pueden ver tickets de otros clientes del mismo tenant
|
||||||
|
- ✅ Restricciones adecuadas implementadas
|
||||||
|
|
||||||
|
#### Endpoints Protegidos
|
||||||
|
|
||||||
|
**Categories** (`/api/v1/categories`)
|
||||||
|
- GET: Todos los roles (lectura)
|
||||||
|
- POST/PUT/DELETE: Solo ADMIN y SUPPORT_MANAGER
|
||||||
|
|
||||||
|
**Systems** (`/api/v1/systems`)
|
||||||
|
- GET: Todos los roles (lectura)
|
||||||
|
- POST/PUT/DELETE: Solo ADMIN y SUPPORT_MANAGER
|
||||||
|
|
||||||
|
**Tickets** (`/api/v1/tickets`)
|
||||||
|
- GET (listado): Filtrado según rol
|
||||||
|
- GET (detalle): Validación de permisos por rol
|
||||||
|
- POST: Todos (según su alcance)
|
||||||
|
- PATCH/DELETE: Validación por rol y propiedad
|
||||||
|
|
||||||
|
### Multi-Tenancy Reforzado
|
||||||
|
|
||||||
|
- ✅ Header `X-Tenant-ID` agregado en frontend-internal
|
||||||
|
- ✅ Validación de tenant en todos los endpoints
|
||||||
|
- ✅ Aislamiento estricto de datos entre tenants
|
||||||
|
- ✅ Prevención de acceso cruzado entre organizaciones
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Correcciones Críticas
|
||||||
|
|
||||||
|
### Fix: Números de Ticket Duplicados
|
||||||
|
|
||||||
|
**Problema Original**:
|
||||||
|
- Generación de números con simple contador
|
||||||
|
- Race conditions en creación simultánea
|
||||||
|
- Violación de constraint unique `uq_tickets_tenant_number`
|
||||||
|
|
||||||
|
**Solución Implementada**:
|
||||||
|
```python
|
||||||
|
# Retry logic con 3 intentos
|
||||||
|
# Búsqueda del MAX número existente
|
||||||
|
# Manejo específico de errores de llave duplicada
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
last_number = get_max_ticket_number()
|
||||||
|
next_number = last_number + 1
|
||||||
|
try:
|
||||||
|
create_ticket(next_number)
|
||||||
|
break
|
||||||
|
except DuplicateKeyError:
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
continue # Reintentar
|
||||||
|
```
|
||||||
|
|
||||||
|
**Resultado**:
|
||||||
|
- ✅ 0% fallos por duplicados
|
||||||
|
- ✅ Manejo robusto de alta concurrencia
|
||||||
|
- ✅ Recuperación automática de errores
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Mejoras de Código
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
1. **Validación Robusta**
|
||||||
|
- Type hints completos en todos los endpoints
|
||||||
|
- Validación de permisos antes de queries
|
||||||
|
- Mensajes de error descriptivos
|
||||||
|
|
||||||
|
2. **Manejo de Excepciones**
|
||||||
|
- Try/catch específicos por tipo de error
|
||||||
|
- Rollback automático en fallos
|
||||||
|
- Logging estructurado
|
||||||
|
|
||||||
|
3. **Documentación**
|
||||||
|
- Docstrings actualizados con información de permisos
|
||||||
|
- Comentarios explicativos en lógica compleja
|
||||||
|
- Ejemplos de uso en código
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
1. **API Client**
|
||||||
|
- Header `X-Tenant-ID` en todas las peticiones
|
||||||
|
- Manejo consistente de errores
|
||||||
|
- Type safety mejorado
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Documentación
|
||||||
|
|
||||||
|
### Archivos Nuevos
|
||||||
|
|
||||||
|
1. **CHANGELOG.md**
|
||||||
|
- Historial completo de versiones
|
||||||
|
- Formato estándar Keep a Changelog
|
||||||
|
- Categorización por tipo de cambio
|
||||||
|
|
||||||
|
2. **test_rbac.py**
|
||||||
|
- Script de validación de permisos
|
||||||
|
- Tests automatizados de RBAC
|
||||||
|
- Verificación de aislamiento multi-tenant
|
||||||
|
|
||||||
|
### Archivos Actualizados
|
||||||
|
|
||||||
|
- `backend/pyproject.toml` → v1.5.1
|
||||||
|
- `frontend-internal/package.json` → v1.5.1
|
||||||
|
- `frontend-client/package.json` → v1.5.1
|
||||||
|
- Endpoints: tickets.py, categories.py, systems.py
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
### Scripts de Validación
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test de control de acceso
|
||||||
|
python backend/test_rbac.py
|
||||||
|
|
||||||
|
# Test de creación de tickets
|
||||||
|
python backend/test_ticket_numbers.py
|
||||||
|
|
||||||
|
# Verificar usuarios y roles
|
||||||
|
python backend/list_all_users.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cobertura
|
||||||
|
|
||||||
|
- ✅ RBAC implementado y validado
|
||||||
|
- ✅ Multi-tenancy verificado
|
||||||
|
- ✅ Generación de números probada
|
||||||
|
- ✅ Endpoints protegidos confirmados
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💾 Backup
|
||||||
|
|
||||||
|
**Ubicación**: `../backups/ServiceManagerWeb_v1.5.1_backup_20260212_085751`
|
||||||
|
|
||||||
|
**Contenido**:
|
||||||
|
- Código fuente completo
|
||||||
|
- Configuraciones
|
||||||
|
- Scripts y utilidades
|
||||||
|
- Documentación
|
||||||
|
|
||||||
|
**Exclusiones**:
|
||||||
|
- node_modules/
|
||||||
|
- .git/
|
||||||
|
- __pycache__/
|
||||||
|
- logs/
|
||||||
|
- uploads/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Despliegue
|
||||||
|
|
||||||
|
### Para Subir al Repositorio Remoto
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Subir commit
|
||||||
|
git push origin main
|
||||||
|
|
||||||
|
# Subir tag
|
||||||
|
git push origin v1.5.1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Para Desplegar en Producción
|
||||||
|
|
||||||
|
1. Pull de la versión
|
||||||
|
```bash
|
||||||
|
git fetch --tags
|
||||||
|
git checkout v1.5.1
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Actualizar dependencias
|
||||||
|
```bash
|
||||||
|
docker-compose pull
|
||||||
|
docker-compose build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Reiniciar servicios
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Verificar estado
|
||||||
|
```bash
|
||||||
|
docker-compose ps
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Breaking Changes
|
||||||
|
|
||||||
|
**Ninguno**: Esta versión es completamente compatible con v1.4.x
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Estadísticas
|
||||||
|
|
||||||
|
- **Archivos modificados**: 8
|
||||||
|
- **Líneas agregadas**: 336
|
||||||
|
- **Líneas eliminadas**: 101
|
||||||
|
- **Commits**: 1
|
||||||
|
- **Tags**: 1
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👥 Contribuidores
|
||||||
|
|
||||||
|
- **Autor**: icamarillo <icamarillo@aduanasoft.com.mx>
|
||||||
|
- **Fecha**: Thu Feb 12 09:00:16 2026 -0700
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 Referencias
|
||||||
|
|
||||||
|
- **Commit**: 771b6eba30e183fafbff6d2f074a41c378170730
|
||||||
|
- **Tag**: v1.5.1
|
||||||
|
- **Rama**: main
|
||||||
|
- **Changelog**: CHANGELOG.md
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generado automáticamente el 12 de Febrero, 2026_
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from sqlalchemy import text
|
|
||||||
from app.core.database import engine
|
|
||||||
|
|
||||||
async def add_columns():
|
|
||||||
print("Starting schema update...")
|
|
||||||
async with engine.begin() as conn:
|
|
||||||
try:
|
|
||||||
await conn.execute(text("ALTER TABLE tickets ADD COLUMN system_id UUID REFERENCES systems(id)"))
|
|
||||||
print("Added system_id column")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error adding system_id (might exist): {e}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
await conn.execute(text("ALTER TABLE tickets ADD COLUMN category_id UUID REFERENCES categories(id)"))
|
|
||||||
print("Added category_id column")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error adding category_id (might exist): {e}")
|
|
||||||
print("Schema update finished.")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(add_columns())
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
"""Script para verificar información del admin"""
|
|
||||||
import asyncio
|
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
||||||
from sqlalchemy.orm import sessionmaker
|
|
||||||
from sqlalchemy import select
|
|
||||||
from app.models.user import User
|
|
||||||
import os
|
|
||||||
|
|
||||||
async def check_user():
|
|
||||||
database_url = os.getenv('DATABASE_URL', 'postgresql+asyncpg://postgres:postgres@db:5432/servicemanager')
|
|
||||||
engine = create_async_engine(database_url)
|
|
||||||
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
|
||||||
|
|
||||||
async with async_session() as session:
|
|
||||||
result = await session.execute(
|
|
||||||
select(User).where(User.email == 'admin@example.com')
|
|
||||||
)
|
|
||||||
user = result.scalar_one_or_none()
|
|
||||||
|
|
||||||
if user:
|
|
||||||
print(f'User found:')
|
|
||||||
print(f' Email: {user.email}')
|
|
||||||
print(f' Role: {user.role}')
|
|
||||||
print(f' Tenant ID: {user.tenant_id}')
|
|
||||||
print(f' User ID: {user.id}')
|
|
||||||
else:
|
|
||||||
print('User not found')
|
|
||||||
|
|
||||||
await engine.dispose()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(check_user())
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
"""Script para verificar información del test_user"""
|
|
||||||
import asyncio
|
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
||||||
from sqlalchemy.orm import sessionmaker
|
|
||||||
from sqlalchemy import select
|
|
||||||
from app.models.user import User
|
|
||||||
import os
|
|
||||||
|
|
||||||
async def check_user():
|
|
||||||
database_url = os.getenv('DATABASE_URL', 'postgresql+asyncpg://postgres:postgres@db:5432/servicemanager')
|
|
||||||
engine = create_async_engine(database_url)
|
|
||||||
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
|
||||||
|
|
||||||
async with async_session() as session:
|
|
||||||
result = await session.execute(
|
|
||||||
select(User).where(User.email == 'test_user@example.com')
|
|
||||||
)
|
|
||||||
user = result.scalar_one_or_none()
|
|
||||||
|
|
||||||
if user:
|
|
||||||
print(f'User found:')
|
|
||||||
print(f' Email: {user.email}')
|
|
||||||
print(f' Role: {user.role}')
|
|
||||||
print(f' Tenant ID: {user.tenant_id}')
|
|
||||||
print(f' User ID: {user.id}')
|
|
||||||
else:
|
|
||||||
print('User not found')
|
|
||||||
|
|
||||||
await engine.dispose()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(check_user())
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
"""
|
|
||||||
Script para verificar y actualizar password del test_user
|
|
||||||
"""
|
|
||||||
import asyncio
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
from sqlalchemy import select
|
|
||||||
from passlib.context import CryptContext
|
|
||||||
|
|
||||||
# Configurar el path
|
|
||||||
backend_path = os.path.join(os.path.dirname(__file__), 'backend')
|
|
||||||
sys.path.insert(0, backend_path)
|
|
||||||
|
|
||||||
from app.core.database import AsyncSessionLocal
|
|
||||||
from app.models.user import User
|
|
||||||
|
|
||||||
# Configurar passlib igual que en security.py
|
|
||||||
pwd_context = CryptContext(
|
|
||||||
schemes=["argon2", "bcrypt"],
|
|
||||||
deprecated="auto",
|
|
||||||
argon2__memory_cost=65536,
|
|
||||||
argon2__time_cost=3,
|
|
||||||
argon2__parallelism=4,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def check_and_fix_test_user():
|
|
||||||
"""Verificar y actualizar password del test_user"""
|
|
||||||
|
|
||||||
# Contraseñas posibles
|
|
||||||
possible_passwords = [
|
|
||||||
"admin123",
|
|
||||||
"TestPassword123!",
|
|
||||||
"password123",
|
|
||||||
"test123",
|
|
||||||
"hashed_password"
|
|
||||||
]
|
|
||||||
|
|
||||||
async with AsyncSessionLocal() as session:
|
|
||||||
try:
|
|
||||||
# Buscar test_user
|
|
||||||
result = await session.execute(
|
|
||||||
select(User).where(User.email == "test_user@example.com")
|
|
||||||
)
|
|
||||||
user = result.scalar_one_or_none()
|
|
||||||
|
|
||||||
if not user:
|
|
||||||
print("❌ Usuario test_user@example.com no encontrado")
|
|
||||||
return
|
|
||||||
|
|
||||||
print(f"✅ Usuario encontrado: {user.email}")
|
|
||||||
print(f"Hash actual: {user.password_hash}")
|
|
||||||
|
|
||||||
# Probar contraseñas posibles
|
|
||||||
found_password = None
|
|
||||||
for password in possible_passwords:
|
|
||||||
if pwd_context.verify(password, user.password_hash):
|
|
||||||
found_password = password
|
|
||||||
break
|
|
||||||
|
|
||||||
if found_password:
|
|
||||||
print(f"🎉 Contraseña encontrada: {found_password}")
|
|
||||||
else:
|
|
||||||
print("❌ Ninguna contraseña coincide")
|
|
||||||
print("Estableciendo nueva contraseña: admin123")
|
|
||||||
|
|
||||||
# Establecer nueva contraseña
|
|
||||||
new_password = "admin123"
|
|
||||||
user.password_hash = pwd_context.hash(new_password)
|
|
||||||
await session.commit()
|
|
||||||
print(f"✅ Contraseña actualizada a: {new_password}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Error: {e}")
|
|
||||||
await session.rollback()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(check_and_fix_test_user())
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
"""Script para verificar información del ticket"""
|
|
||||||
import asyncio
|
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
||||||
from sqlalchemy.orm import sessionmaker
|
|
||||||
from sqlalchemy import select
|
|
||||||
from app.models.ticket import Ticket
|
|
||||||
from app.models.user import User
|
|
||||||
import uuid
|
|
||||||
import os
|
|
||||||
|
|
||||||
async def check_ticket():
|
|
||||||
database_url = os.getenv('DATABASE_URL', 'postgresql+asyncpg://postgres:postgres@db:5432/servicemanager')
|
|
||||||
engine = create_async_engine(database_url)
|
|
||||||
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
|
||||||
|
|
||||||
ticket_id = '2bd79718-440d-4144-b660-c0c6051fcf73'
|
|
||||||
|
|
||||||
async with async_session() as session:
|
|
||||||
result = await session.execute(
|
|
||||||
select(Ticket).where(Ticket.id == uuid.UUID(ticket_id))
|
|
||||||
)
|
|
||||||
ticket = result.scalar_one_or_none()
|
|
||||||
|
|
||||||
if ticket:
|
|
||||||
creator_result = await session.execute(
|
|
||||||
select(User).where(User.id == ticket.created_by)
|
|
||||||
)
|
|
||||||
creator = creator_result.scalar_one_or_none()
|
|
||||||
|
|
||||||
print(f'Ticket found:')
|
|
||||||
print(f' ID: {ticket.id}')
|
|
||||||
print(f' Number: {ticket.ticket_number}')
|
|
||||||
print(f' Subject: {ticket.subject}')
|
|
||||||
print(f' Status: {ticket.status}')
|
|
||||||
print(f' Tenant ID: {ticket.tenant_id}')
|
|
||||||
print(f' Created by ID: {ticket.created_by}')
|
|
||||||
if creator:
|
|
||||||
print(f' Creator email: {creator.email}')
|
|
||||||
print(f' Creator role: {creator.role}')
|
|
||||||
print(f' Assigned to: {ticket.assigned_to}')
|
|
||||||
else:
|
|
||||||
print('Ticket not found')
|
|
||||||
|
|
||||||
await engine.dispose()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(check_ticket())
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
"""Script para verificar números de tickets existentes"""
|
|
||||||
import asyncio
|
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
||||||
from sqlalchemy.orm import sessionmaker
|
|
||||||
from sqlalchemy import select
|
|
||||||
from app.models.ticket import Ticket
|
|
||||||
import os
|
|
||||||
|
|
||||||
async def check_tickets():
|
|
||||||
database_url = os.getenv('DATABASE_URL', 'postgresql+asyncpg://postgres:postgres@db:5432/servicemanager')
|
|
||||||
engine = create_async_engine(database_url)
|
|
||||||
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
|
||||||
|
|
||||||
tenant_id = 'c186c814-4f5a-4293-aae9-46f57637bb35'
|
|
||||||
|
|
||||||
async with async_session() as session:
|
|
||||||
result = await session.execute(
|
|
||||||
select(Ticket.ticket_number, Ticket.id, Ticket.subject)
|
|
||||||
.where(Ticket.tenant_id == tenant_id)
|
|
||||||
.order_by(Ticket.ticket_number)
|
|
||||||
)
|
|
||||||
tickets = result.all()
|
|
||||||
|
|
||||||
print(f"Tickets existentes para tenant {tenant_id}:")
|
|
||||||
print("=" * 80)
|
|
||||||
for ticket_number, ticket_id, subject in tickets:
|
|
||||||
print(f" {ticket_number} | {ticket_id} | {subject}")
|
|
||||||
print("=" * 80)
|
|
||||||
print(f"Total: {len(tickets)} tickets")
|
|
||||||
|
|
||||||
if tickets:
|
|
||||||
last_ticket = tickets[-1]
|
|
||||||
last_number = int(last_ticket[0].split('-')[1])
|
|
||||||
next_number = last_number + 1
|
|
||||||
print(f"\nÚltimo número: {last_ticket[0]} (número: {last_number})")
|
|
||||||
print(f"Próximo número debería ser: TK-{next_number:06d}")
|
|
||||||
|
|
||||||
await engine.dispose()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(check_tickets())
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Add parent directory to path so we can import 'app'
|
|
||||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
|
|
||||||
from sqlalchemy import select
|
|
||||||
from app.core.database import AsyncSessionLocal
|
|
||||||
from app.models.tenant import Tenant
|
|
||||||
from app.models.ticket import Ticket
|
|
||||||
from app.models.category import Category # ✅ AÑADIR ESTO
|
|
||||||
from app.models.system import System # ✅ AÑADIR ESTO
|
|
||||||
from app.models.user import User
|
|
||||||
from app.core.security import SecurityUtils
|
|
||||||
|
|
||||||
async def fix_password():
|
|
||||||
async with AsyncSessionLocal() as session:
|
|
||||||
# Find the admin user
|
|
||||||
email = "admin@aduanasoft.com"
|
|
||||||
result = await session.execute(select(User).where(User.email == email))
|
|
||||||
user = result.scalar_one_or_none()
|
|
||||||
|
|
||||||
if user:
|
|
||||||
print(f"User {email} found.")
|
|
||||||
# Reset password to 'admin123'
|
|
||||||
new_password = "admin123"
|
|
||||||
hashed = SecurityUtils.hash_password(new_password)
|
|
||||||
user.password_hash = hashed
|
|
||||||
|
|
||||||
try:
|
|
||||||
await session.commit()
|
|
||||||
print(f"Password for {email} updated successfully!")
|
|
||||||
print(f"New password is: {new_password}")
|
|
||||||
except Exception as e:
|
|
||||||
await session.rollback()
|
|
||||||
print(f"Error updating password: {e}")
|
|
||||||
else:
|
|
||||||
print(f"User {email} not found!")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(fix_password())
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
"""Script para listar todos los usuarios"""
|
|
||||||
import asyncio
|
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
||||||
from sqlalchemy.orm import sessionmaker
|
|
||||||
from sqlalchemy import select
|
|
||||||
from app.models.user import User
|
|
||||||
import os
|
|
||||||
|
|
||||||
async def list_users():
|
|
||||||
database_url = os.getenv('DATABASE_URL', 'postgresql+asyncpg://postgres:postgres@db:5432/servicemanager')
|
|
||||||
engine = create_async_engine(database_url)
|
|
||||||
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
|
||||||
|
|
||||||
async with async_session() as session:
|
|
||||||
result = await session.execute(select(User))
|
|
||||||
users = result.scalars().all()
|
|
||||||
|
|
||||||
if users:
|
|
||||||
print(f'Found {len(users)} users:')
|
|
||||||
for user in users:
|
|
||||||
print(f'\n Email: {user.email}')
|
|
||||||
print(f' Role: {user.role}')
|
|
||||||
print(f' Tenant ID: {user.tenant_id}')
|
|
||||||
print(f' User ID: {user.id}')
|
|
||||||
else:
|
|
||||||
print('No users found')
|
|
||||||
|
|
||||||
await engine.dispose()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(list_users())
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
"""
|
|
||||||
Script para establecer contraseña real al test_user
|
|
||||||
"""
|
|
||||||
import asyncio
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
from sqlalchemy import select
|
|
||||||
from passlib.context import CryptContext
|
|
||||||
|
|
||||||
# Configurar el path
|
|
||||||
backend_path = os.path.join(os.path.dirname(__file__), 'backend')
|
|
||||||
sys.path.insert(0, backend_path)
|
|
||||||
|
|
||||||
from app.core.database import AsyncSessionLocal
|
|
||||||
from app.models.user import User
|
|
||||||
|
|
||||||
# Configurar passlib
|
|
||||||
pwd_context = CryptContext(
|
|
||||||
schemes=["argon2", "bcrypt"],
|
|
||||||
deprecated="auto",
|
|
||||||
argon2__memory_cost=65536,
|
|
||||||
argon2__time_cost=3,
|
|
||||||
argon2__parallelism=4,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def set_test_user_password():
|
|
||||||
"""Establecer contraseña admin123 para test_user"""
|
|
||||||
|
|
||||||
new_password = "admin123"
|
|
||||||
password_hash = pwd_context.hash(new_password)
|
|
||||||
|
|
||||||
async with AsyncSessionLocal() as session:
|
|
||||||
try:
|
|
||||||
# Buscar test_user
|
|
||||||
result = await session.execute(
|
|
||||||
select(User).where(User.email == "test_user@example.com")
|
|
||||||
)
|
|
||||||
user = result.scalar_one_or_none()
|
|
||||||
|
|
||||||
if not user:
|
|
||||||
print("❌ Usuario test_user@example.com no encontrado")
|
|
||||||
return
|
|
||||||
|
|
||||||
print(f"✅ Usuario encontrado: {user.email}")
|
|
||||||
print(f"Hash anterior: {user.password_hash}")
|
|
||||||
|
|
||||||
# Establecer nueva contraseña hash
|
|
||||||
user.password_hash = password_hash
|
|
||||||
await session.commit()
|
|
||||||
|
|
||||||
print(f"🎉 Contraseña establecida: {new_password}")
|
|
||||||
print(f"✅ Nuevo hash: {password_hash[:50]}...")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Error: {e}")
|
|
||||||
await session.rollback()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(set_test_user_password())
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
"""
|
|
||||||
Script para actualizar el password del usuario admin
|
|
||||||
Ejecutar: python fix_admin_password.py
|
|
||||||
"""
|
|
||||||
import asyncio
|
|
||||||
import sys
|
|
||||||
from sqlalchemy import select, update
|
|
||||||
from passlib.context import CryptContext
|
|
||||||
|
|
||||||
# Importar desde el proyecto
|
|
||||||
sys.path.insert(0, '/app')
|
|
||||||
from app.core.database import AsyncSessionLocal
|
|
||||||
from app.models.user import User
|
|
||||||
|
|
||||||
# Configurar passlib igual que en security.py
|
|
||||||
pwd_context = CryptContext(
|
|
||||||
schemes=["argon2", "bcrypt"],
|
|
||||||
deprecated="auto",
|
|
||||||
argon2__memory_cost=65536,
|
|
||||||
argon2__time_cost=3,
|
|
||||||
argon2__parallelism=4,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def fix_admin_password():
|
|
||||||
"""Actualizar password del admin a 'admin123'"""
|
|
||||||
|
|
||||||
# Generar hash del password
|
|
||||||
new_password = "admin123"
|
|
||||||
password_hash = pwd_context.hash(new_password)
|
|
||||||
|
|
||||||
print(f"Nuevo hash generado para password: {new_password}")
|
|
||||||
print(f"Hash: {password_hash[:50]}...")
|
|
||||||
|
|
||||||
async with AsyncSessionLocal() as session:
|
|
||||||
try:
|
|
||||||
# Buscar usuario admin
|
|
||||||
result = await session.execute(
|
|
||||||
select(User).where(User.email == "admin@aduanasoft.com")
|
|
||||||
)
|
|
||||||
user = result.scalar_one_or_none()
|
|
||||||
|
|
||||||
if not user:
|
|
||||||
print("❌ Usuario admin no encontrado")
|
|
||||||
return
|
|
||||||
|
|
||||||
print(f"✅ Usuario encontrado: {user.email} (ID: {user.id})")
|
|
||||||
|
|
||||||
# Actualizar password
|
|
||||||
user.password_hash = password_hash
|
|
||||||
|
|
||||||
await session.commit()
|
|
||||||
|
|
||||||
print("✅ Password actualizado exitosamente")
|
|
||||||
print(f" Email: admin@aduanasoft.com")
|
|
||||||
print(f" Password: admin123")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
await session.rollback()
|
|
||||||
print(f"❌ Error: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
print("=" * 60)
|
|
||||||
print("ACTUALIZAR PASSWORD DEL ADMIN")
|
|
||||||
print("=" * 60)
|
|
||||||
asyncio.run(fix_admin_password())
|
|
||||||
print("=" * 60)
|
|
||||||
262
scripts/db_utils.py
Normal file
262
scripts/db_utils.py
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Database Utilities Script
|
||||||
|
Herramientas administrativas para gestión de base de datos
|
||||||
|
|
||||||
|
Uso:
|
||||||
|
python scripts/db_utils.py list-users [--tenant-id UUID]
|
||||||
|
python scripts/db_utils.py check-user EMAIL
|
||||||
|
python scripts/db_utils.py reset-password EMAIL [--password PASSWORD]
|
||||||
|
python scripts/db_utils.py list-tickets [--tenant-id UUID] [--limit N]
|
||||||
|
python scripts/db_utils.py check-ticket TICKET_ID
|
||||||
|
|
||||||
|
Ejemplos:
|
||||||
|
python scripts/db_utils.py list-users
|
||||||
|
python scripts/db_utils.py check-user admin@example.com
|
||||||
|
python scripts/db_utils.py reset-password admin@example.com --password admin123
|
||||||
|
python scripts/db_utils.py list-tickets --limit 10
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Agregar backend al path para imports
|
||||||
|
backend_path = Path(__file__).parent.parent / "backend"
|
||||||
|
sys.path.insert(0, str(backend_path))
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
from app.models.user import User
|
||||||
|
from app.models.ticket import Ticket
|
||||||
|
from app.models.tenant import Tenant
|
||||||
|
from app.core.security import SecurityUtils
|
||||||
|
|
||||||
|
|
||||||
|
class DBUtils:
|
||||||
|
"""Utilidades de gestión de base de datos"""
|
||||||
|
|
||||||
|
def __init__(self, database_url: Optional[str] = None):
|
||||||
|
self.database_url = database_url or os.getenv(
|
||||||
|
'DATABASE_URL',
|
||||||
|
'postgresql+asyncpg://postgres:postgres@localhost:5432/servicemanager'
|
||||||
|
)
|
||||||
|
self.engine = create_async_engine(self.database_url, echo=False)
|
||||||
|
self.async_session = sessionmaker(
|
||||||
|
self.engine,
|
||||||
|
class_=AsyncSession,
|
||||||
|
expire_on_commit=False
|
||||||
|
)
|
||||||
|
|
||||||
|
async def list_users(self, tenant_id: Optional[str] = None):
|
||||||
|
"""Listar todos los usuarios"""
|
||||||
|
async with self.async_session() as session:
|
||||||
|
query = select(User)
|
||||||
|
if tenant_id:
|
||||||
|
query = query.where(User.tenant_id == tenant_id)
|
||||||
|
|
||||||
|
result = await session.execute(query)
|
||||||
|
users = result.scalars().all()
|
||||||
|
|
||||||
|
if not users:
|
||||||
|
print("❌ No se encontraron usuarios")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"\n{'='*80}")
|
||||||
|
print(f"📋 USUARIOS ({len(users)} encontrados)")
|
||||||
|
print(f"{'='*80}\n")
|
||||||
|
|
||||||
|
for user in users:
|
||||||
|
print(f" Email: {user.email}")
|
||||||
|
print(f" Role: {user.role}")
|
||||||
|
print(f" ID: {user.id}")
|
||||||
|
print(f" Tenant ID: {user.tenant_id}")
|
||||||
|
print(f" Activo: {'✅' if user.is_active else '❌'}")
|
||||||
|
print(f" {'-'*76}")
|
||||||
|
|
||||||
|
async def check_user(self, email: str):
|
||||||
|
"""Verificar información de un usuario específico"""
|
||||||
|
async with self.async_session() as session:
|
||||||
|
result = await session.execute(
|
||||||
|
select(User).where(User.email == email)
|
||||||
|
)
|
||||||
|
user = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
print(f"❌ Usuario '{email}' no encontrado")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"\n{'='*80}")
|
||||||
|
print(f"👤 INFORMACIÓN DEL USUARIO")
|
||||||
|
print(f"{'='*80}\n")
|
||||||
|
print(f" Email: {user.email}")
|
||||||
|
print(f" Nombre: {user.first_name} {user.last_name}")
|
||||||
|
print(f" Role: {user.role}")
|
||||||
|
print(f" ID: {user.id}")
|
||||||
|
print(f" Tenant ID: {user.tenant_id}")
|
||||||
|
print(f" Activo: {'✅' if user.is_active else '❌'}")
|
||||||
|
print(f" 2FA: {'✅ Habilitado' if user.totp_secret else '❌ Deshabilitado'}")
|
||||||
|
print(f" Creado: {user.created_at}")
|
||||||
|
print(f"\n{'='*80}")
|
||||||
|
|
||||||
|
async def reset_password(self, email: str, new_password: str = "admin123"):
|
||||||
|
"""Resetear contraseña de un usuario"""
|
||||||
|
async with self.async_session() as session:
|
||||||
|
result = await session.execute(
|
||||||
|
select(User).where(User.email == email)
|
||||||
|
)
|
||||||
|
user = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
print(f"❌ Usuario '{email}' no encontrado")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Hash nueva contraseña
|
||||||
|
password_hash = SecurityUtils.hash_password(new_password)
|
||||||
|
user.password_hash = password_hash
|
||||||
|
|
||||||
|
try:
|
||||||
|
await session.commit()
|
||||||
|
print(f"\n✅ Contraseña actualizada exitosamente")
|
||||||
|
print(f" Usuario: {email}")
|
||||||
|
print(f" Nueva contraseña: {new_password}")
|
||||||
|
print(f"\n⚠️ IMPORTANTE: Cambia esta contraseña después del primer login")
|
||||||
|
except Exception as e:
|
||||||
|
await session.rollback()
|
||||||
|
print(f"❌ Error al actualizar contraseña: {e}")
|
||||||
|
|
||||||
|
async def list_tickets(self, tenant_id: Optional[str] = None, limit: int = 20):
|
||||||
|
"""Listar tickets"""
|
||||||
|
async with self.async_session() as session:
|
||||||
|
query = select(Ticket).order_by(Ticket.created_at.desc()).limit(limit)
|
||||||
|
if tenant_id:
|
||||||
|
query = query.where(Ticket.tenant_id == tenant_id)
|
||||||
|
|
||||||
|
result = await session.execute(query)
|
||||||
|
tickets = result.scalars().all()
|
||||||
|
|
||||||
|
if not tickets:
|
||||||
|
print("❌ No se encontraron tickets")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"\n{'='*80}")
|
||||||
|
print(f"🎫 TICKETS ({len(tickets)} encontrados, límite: {limit})")
|
||||||
|
print(f"{'='*80}\n")
|
||||||
|
|
||||||
|
for ticket in tickets:
|
||||||
|
print(f" {ticket.ticket_number} | {ticket.status} | {ticket.priority}")
|
||||||
|
print(f" Asunto: {ticket.subject}")
|
||||||
|
print(f" ID: {ticket.id}")
|
||||||
|
print(f" Tenant: {ticket.tenant_id}")
|
||||||
|
print(f" Creado: {ticket.created_at}")
|
||||||
|
print(f" {'-'*76}")
|
||||||
|
|
||||||
|
async def check_ticket(self, ticket_id: str):
|
||||||
|
"""Verificar información de un ticket específico"""
|
||||||
|
async with self.async_session() as session:
|
||||||
|
result = await session.execute(
|
||||||
|
select(Ticket).where(Ticket.id == ticket_id)
|
||||||
|
)
|
||||||
|
ticket = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not ticket:
|
||||||
|
print(f"❌ Ticket '{ticket_id}' no encontrado")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Obtener creador
|
||||||
|
creator_result = await session.execute(
|
||||||
|
select(User).where(User.id == ticket.created_by)
|
||||||
|
)
|
||||||
|
creator = creator_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
# Obtener asignado
|
||||||
|
assigned = None
|
||||||
|
if ticket.assigned_to:
|
||||||
|
assigned_result = await session.execute(
|
||||||
|
select(User).where(User.id == ticket.assigned_to)
|
||||||
|
)
|
||||||
|
assigned = assigned_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
print(f"\n{'='*80}")
|
||||||
|
print(f"🎫 INFORMACIÓN DEL TICKET")
|
||||||
|
print(f"{'='*80}\n")
|
||||||
|
print(f" Número: {ticket.ticket_number}")
|
||||||
|
print(f" Asunto: {ticket.subject}")
|
||||||
|
print(f" Estado: {ticket.status}")
|
||||||
|
print(f" Prioridad: {ticket.priority}")
|
||||||
|
print(f" ID: {ticket.id}")
|
||||||
|
print(f" Tenant ID: {ticket.tenant_id}")
|
||||||
|
if creator:
|
||||||
|
print(f" Creado por: {creator.email} ({creator.role})")
|
||||||
|
if assigned:
|
||||||
|
print(f" Asignado a: {assigned.email} ({assigned.role})")
|
||||||
|
print(f" Creado: {ticket.created_at}")
|
||||||
|
print(f" Actualizado: {ticket.updated_at}")
|
||||||
|
print(f"\n{'='*80}")
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
"""Cerrar conexión"""
|
||||||
|
await self.engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Utilidades de gestión de base de datos',
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog=__doc__
|
||||||
|
)
|
||||||
|
|
||||||
|
subparsers = parser.add_subparsers(dest='command', help='Comando a ejecutar')
|
||||||
|
|
||||||
|
# list-users
|
||||||
|
list_users_parser = subparsers.add_parser('list-users', help='Listar usuarios')
|
||||||
|
list_users_parser.add_argument('--tenant-id', help='Filtrar por tenant ID')
|
||||||
|
|
||||||
|
# check-user
|
||||||
|
check_user_parser = subparsers.add_parser('check-user', help='Verificar usuario')
|
||||||
|
check_user_parser.add_argument('email', help='Email del usuario')
|
||||||
|
|
||||||
|
# reset-password
|
||||||
|
reset_password_parser = subparsers.add_parser('reset-password', help='Resetear contraseña')
|
||||||
|
reset_password_parser.add_argument('email', help='Email del usuario')
|
||||||
|
reset_password_parser.add_argument('--password', default='admin123', help='Nueva contraseña')
|
||||||
|
|
||||||
|
# list-tickets
|
||||||
|
list_tickets_parser = subparsers.add_parser('list-tickets', help='Listar tickets')
|
||||||
|
list_tickets_parser.add_argument('--tenant-id', help='Filtrar por tenant ID')
|
||||||
|
list_tickets_parser.add_argument('--limit', type=int, default=20, help='Límite de resultados')
|
||||||
|
|
||||||
|
# check-ticket
|
||||||
|
check_ticket_parser = subparsers.add_parser('check-ticket', help='Verificar ticket')
|
||||||
|
check_ticket_parser.add_argument('ticket_id', help='ID del ticket')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not args.command:
|
||||||
|
parser.print_help()
|
||||||
|
return
|
||||||
|
|
||||||
|
utils = DBUtils()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if args.command == 'list-users':
|
||||||
|
await utils.list_users(args.tenant_id)
|
||||||
|
elif args.command == 'check-user':
|
||||||
|
await utils.check_user(args.email)
|
||||||
|
elif args.command == 'reset-password':
|
||||||
|
await utils.reset_password(args.email, args.password)
|
||||||
|
elif args.command == 'list-tickets':
|
||||||
|
await utils.list_tickets(args.tenant_id, args.limit)
|
||||||
|
elif args.command == 'check-ticket':
|
||||||
|
await utils.check_ticket(args.ticket_id)
|
||||||
|
finally:
|
||||||
|
await utils.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
Binary file not shown.
@@ -1,34 +0,0 @@
|
|||||||
# Test de Attachments API
|
|
||||||
# Ejecutar desde PowerShell
|
|
||||||
|
|
||||||
# 1. Login
|
|
||||||
$body = '{"email":"admin@aduanasoft.com","password":"admin123","tenant_slug":"aduanasoft"}'
|
|
||||||
$login = Invoke-RestMethod -Uri "http://localhost:8000/v1/auth/login" -Method POST -Body $body -ContentType "application/json"
|
|
||||||
|
|
||||||
$token = $login.data.access_token
|
|
||||||
$tenantId = $login.data.user.tenant_id
|
|
||||||
|
|
||||||
$headers = @{
|
|
||||||
"Authorization" = "Bearer $token"
|
|
||||||
"X-Tenant-ID" = $tenantId
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "Autenticacion exitosa" -ForegroundColor Green
|
|
||||||
|
|
||||||
# 2. Listar tickets
|
|
||||||
$tickets = Invoke-RestMethod -Uri "http://localhost:8000/v1/tickets" -Headers $headers
|
|
||||||
$ticketId = $tickets.data[0].id
|
|
||||||
|
|
||||||
Write-Host "Ticket ID obtenido: $ticketId" -ForegroundColor Green
|
|
||||||
|
|
||||||
# 3. Listar attachments del ticket
|
|
||||||
$attachments = Invoke-RestMethod -Uri "http://localhost:8000/v1/tickets/$ticketId/attachments" -Headers $headers
|
|
||||||
|
|
||||||
Write-Host "Attachments listados: $($attachments.Count) encontrados" -ForegroundColor Green
|
|
||||||
|
|
||||||
if ($attachments.Count -gt 0) {
|
|
||||||
$attachments | Format-Table id, original_filename, file_size, created_at
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "TEST COMPLETADO" -ForegroundColor Cyan
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# Test script para attachments endpoint
|
|
||||||
Write-Host "=== Testing Attachments Endpoint ===" -ForegroundColor Cyan
|
|
||||||
|
|
||||||
# 1. Login
|
|
||||||
$body = '{"email":"admin@aduanasoft.com","password":"admin123","tenant_slug":"aduanasoft"}'
|
|
||||||
try {
|
|
||||||
$login = Invoke-RestMethod -Uri "http://localhost:8000/v1/auth/login" -Method POST -Body $body -ContentType "application/json"
|
|
||||||
Write-Host "[OK] Login exitoso" -ForegroundColor Green
|
|
||||||
|
|
||||||
$token = $login.access_token
|
|
||||||
$tenantId = $login.user.tenant_id
|
|
||||||
|
|
||||||
Write-Host "Token: $($token.Substring(0,20))..." -ForegroundColor Gray
|
|
||||||
Write-Host "Tenant ID: $tenantId" -ForegroundColor Gray
|
|
||||||
|
|
||||||
# 2. Test attachments endpoint
|
|
||||||
$headers = @{
|
|
||||||
"Authorization" = "Bearer $token"
|
|
||||||
"X-Tenant-ID" = $tenantId
|
|
||||||
}
|
|
||||||
|
|
||||||
$url = "http://localhost:8000/v1/tickets/68eaf0fe-b5c3-4d42-9b36-895b439be583/attachments"
|
|
||||||
Write-Host "`nProbando GET $url" -ForegroundColor Yellow
|
|
||||||
|
|
||||||
try {
|
|
||||||
$response = Invoke-WebRequest -Uri $url -Headers $headers -Method GET
|
|
||||||
Write-Host "[OK] Status Code: $($response.StatusCode)" -ForegroundColor Green
|
|
||||||
|
|
||||||
$data = $response.Content | ConvertFrom-Json
|
|
||||||
Write-Host "[OK] Attachments encontrados: $($data.Count)" -ForegroundColor Green
|
|
||||||
|
|
||||||
if ($data.Count -gt 0) {
|
|
||||||
$data | Format-Table id, original_filename, file_size
|
|
||||||
} else {
|
|
||||||
Write-Host " (No hay attachments para este ticket)" -ForegroundColor Gray
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch {
|
|
||||||
Write-Host "[ERROR] En request: $($_.Exception.Message)" -ForegroundColor Red
|
|
||||||
Write-Host "Status Code: $($_.Exception.Response.StatusCode.value__)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch {
|
|
||||||
Write-Host "[ERROR] En login: $($_.Exception.Message)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user