diff --git a/backend/app/api/v1/endpoints/tickets.py b/backend/app/api/v1/endpoints/tickets.py index 48f534f..56d64bb 100644 --- a/backend/app/api/v1/endpoints/tickets.py +++ b/backend/app/api/v1/endpoints/tickets.py @@ -79,12 +79,23 @@ async def create_ticket( Crear un nuevo ticket """ try: - # Generar número de ticket único + # Generar número de ticket único basado en el máximo existente result = await db.execute( - select(func.count(Ticket.id)).where(Ticket.tenant_id == current_user.tenant_id) + select(Ticket.ticket_number) + .where(Ticket.tenant_id == current_user.tenant_id) + .order_by(Ticket.ticket_number.desc()) + .limit(1) ) - count = result.scalar() or 0 - ticket_number = f"TK-{count + 1:06d}" + last_ticket_number = result.scalar_one_or_none() + + if last_ticket_number: + # Extraer el número del formato TK-XXXXXX + last_number = int(last_ticket_number.split('-')[1]) + next_number = last_number + 1 + else: + next_number = 1 + + ticket_number = f"TK-{next_number:06d}" # Convertir IDs de string a UUID si son proporcionados category_uuid = uuid.UUID(ticket.category_id) if ticket.category_id else None @@ -374,6 +385,8 @@ async def get_ticket( ): """ Obtener un ticket específico + Roles ADMIN/SUPPORT_MANAGER/AGENT: Pueden ver todos los tickets del tenant + Roles CLIENT_USER/CLIENT_ADMIN: Solo pueden ver sus propios tickets """ try: ticket_uuid = uuid.UUID(ticket_id) @@ -383,12 +396,16 @@ async def get_ticket( detail="Invalid ticket ID format" ) + # Construir query basado en el rol del usuario query = select(Ticket).where( Ticket.id == ticket_uuid, - Ticket.tenant_id == current_user.tenant_id, - Ticket.created_by == current_user.id + Ticket.tenant_id == current_user.tenant_id ) + # Si es cliente, solo puede ver sus propios tickets + if current_user.role in ["CLIENT_USER", "CLIENT_ADMIN"]: + query = query.where(Ticket.created_by == current_user.id) + result = await db.execute(query) ticket = result.scalars().first() @@ -425,6 +442,8 @@ async def update_ticket( ): """ Actualizar un ticket + Roles ADMIN/SUPPORT_MANAGER/AGENT: Pueden actualizar cualquier ticket del tenant + Roles CLIENT_USER/CLIENT_ADMIN: Solo pueden actualizar sus propios tickets """ try: ticket_uuid = uuid.UUID(ticket_id) @@ -434,12 +453,16 @@ async def update_ticket( detail="Invalid ticket ID format" ) + # Construir query basado en el rol del usuario query = select(Ticket).where( Ticket.id == ticket_uuid, - Ticket.tenant_id == current_user.tenant_id, - Ticket.created_by == current_user.id + Ticket.tenant_id == current_user.tenant_id ) + # Si es cliente, solo puede actualizar sus propios tickets + if current_user.role in ["CLIENT_USER", "CLIENT_ADMIN"]: + query = query.where(Ticket.created_by == current_user.id) + result = await db.execute(query) db_ticket = result.scalars().first() @@ -501,6 +524,8 @@ async def close_ticket( ): """ Cerrar un ticket + Roles ADMIN/SUPPORT_MANAGER/AGENT: Pueden cerrar cualquier ticket del tenant + Roles CLIENT_USER/CLIENT_ADMIN: Solo pueden cerrar sus propios tickets """ try: ticket_uuid = uuid.UUID(ticket_id) @@ -510,12 +535,16 @@ async def close_ticket( detail="Invalid ticket ID format" ) + # Construir query basado en el rol del usuario query = select(Ticket).where( Ticket.id == ticket_uuid, - Ticket.tenant_id == current_user.tenant_id, - Ticket.created_by == current_user.id + Ticket.tenant_id == current_user.tenant_id ) + # Si es cliente, solo puede cerrar sus propios tickets + if current_user.role in ["CLIENT_USER", "CLIENT_ADMIN"]: + query = query.where(Ticket.created_by == current_user.id) + result = await db.execute(query) db_ticket = result.scalars().first() diff --git a/backend/check_admin.py b/backend/check_admin.py new file mode 100644 index 0000000..fd2f3e6 --- /dev/null +++ b/backend/check_admin.py @@ -0,0 +1,32 @@ +"""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()) diff --git a/backend/check_test_user.py b/backend/check_test_user.py new file mode 100644 index 0000000..8c287d4 --- /dev/null +++ b/backend/check_test_user.py @@ -0,0 +1,32 @@ +"""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()) diff --git a/backend/check_ticket.py b/backend/check_ticket.py new file mode 100644 index 0000000..541219c --- /dev/null +++ b/backend/check_ticket.py @@ -0,0 +1,47 @@ +"""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()) diff --git a/backend/check_ticket_numbers.py b/backend/check_ticket_numbers.py new file mode 100644 index 0000000..5ad4539 --- /dev/null +++ b/backend/check_ticket_numbers.py @@ -0,0 +1,41 @@ +"""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()) diff --git a/backend/list_all_users.py b/backend/list_all_users.py new file mode 100644 index 0000000..ce01e5d --- /dev/null +++ b/backend/list_all_users.py @@ -0,0 +1,31 @@ +"""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()) diff --git a/backend/test_ticket_access.py b/backend/test_ticket_access.py new file mode 100644 index 0000000..00bc78d --- /dev/null +++ b/backend/test_ticket_access.py @@ -0,0 +1,84 @@ +"""Script para verificar el acceso a tickets con diferentes usuarios""" +import asyncio +import httpx +import os + +BASE_URL = "http://localhost:8000/api/v1" +TICKET_ID = "2bd79718-440d-4144-b660-c0c6051fcf73" + +async def login(email: str, password: str, tenant_slug: str = "aduanasoft"): + """Login y obtener token""" + async with httpx.AsyncClient() as client: + response = await client.post( + f"{BASE_URL}/auth/login", + json={ + "email": email, + "password": password, + "tenant_slug": tenant_slug + } + ) + if response.status_code == 200: + data = response.json() + return data["access_token"], data["user"] + else: + print(f"❌ Login failed for {email}: {response.text}") + return None, None + +async def get_ticket(ticket_id: str, token: str, tenant_id: str): + """Intentar obtener un ticket""" + async with httpx.AsyncClient() as client: + response = await client.get( + f"{BASE_URL}/tickets/{ticket_id}", + headers={ + "Authorization": f"Bearer {token}", + "X-Tenant-ID": tenant_id + } + ) + return response.status_code, response.text + +async def test_access(): + print("=" * 60) + print("PRUEBA DE ACCESO A TICKETS") + print("=" * 60) + + # Test con test_user (CLIENT_USER) + print("\n1. Probando con test_user (CLIENT_USER)...") + token, user = await login("test_user@example.com", "TestPassword123!") + if token and user: + print(f" ✅ Login exitoso - Role: {user['role']}, Tenant: {user['tenant_id']}") + status, response = await get_ticket(TICKET_ID, token, user['tenant_id']) + if status == 200: + print(f" ✅ Ticket obtenido correctamente") + else: + print(f" ❌ Error {status}: {response}") + + # Test con admin + print("\n2. Probando con admin (ADMIN)...") + token, user = await login("admin@aduanasoft.com", "Admin123!") + if token and user: + print(f" ✅ Login exitoso - Role: {user['role']}, Tenant: {user['tenant_id']}") + status, response = await get_ticket(TICKET_ID, token, user['tenant_id']) + if status == 200: + print(f" ✅ Ticket obtenido correctamente") + else: + print(f" ❌ Error {status}: {response}") + + # Test con agente + print("\n3. Probando con agente (AGENT)...") + token, user = await login("agente@aduanasoft.com", "Agente123!") + if token and user: + print(f" ✅ Login exitoso - Role: {user['role']}, Tenant: {user['tenant_id']}") + status, response = await get_ticket(TICKET_ID, token, user['tenant_id']) + if status == 200: + print(f" ✅ Ticket obtenido correctamente") + else: + print(f" ❌ Error {status}: {response}") + + print("\n" + "=" * 60) + print("Nota: Este ticket fue creado por test_user@example.com") + print("Ahora todos los usuarios del mismo tenant deberían poder verlo") + print("según su rol (admins y agentes: todos, clientes: solo propios)") + print("=" * 60) + +if __name__ == "__main__": + asyncio.run(test_access()) diff --git a/frontend-internal/src/lib/utils/api.ts b/frontend-internal/src/lib/utils/api.ts index 509a0ac..849719b 100644 --- a/frontend-internal/src/lib/utils/api.ts +++ b/frontend-internal/src/lib/utils/api.ts @@ -25,11 +25,15 @@ async function request(endpoint: string, options: RequestOptions = {}): Promi const authState = get(auth); const token = authState.token || (typeof window !== 'undefined' ? localStorage.getItem('internal_auth_token') : null); + const user = authState.user || (typeof window !== 'undefined' ? JSON.parse(localStorage.getItem('internal_auth_user') || 'null') : null); const headers = new Headers(init.headers); if (token) { headers.set('Authorization', `Bearer ${token}`); } + if (user && user.tenant_id) { + headers.set('X-Tenant-ID', user.tenant_id); + } if (!headers.has('Content-Type')) { headers.set('Content-Type', 'application/json'); } @@ -65,11 +69,15 @@ async function request(endpoint: string, options: RequestOptions = {}): Promi async function downloadFile(endpoint: string, filename: string): Promise { const authState = get(auth); const token = authState.token || (typeof window !== 'undefined' ? localStorage.getItem('internal_auth_token') : null); + const user = authState.user || (typeof window !== 'undefined' ? JSON.parse(localStorage.getItem('internal_auth_user') || 'null') : null); const headers = new Headers(); if (token) { headers.set('Authorization', `Bearer ${token}`); } + if (user && user.tenant_id) { + headers.set('X-Tenant-ID', user.tenant_id); + } const response = await fetch(`${API_BASE}${endpoint}`, { method: 'GET',