From 94ca24e00e65beaac60118ce7094fa4aaca86161 Mon Sep 17 00:00:00 2001 From: icamarillo Date: Tue, 24 Mar 2026 13:57:54 -0600 Subject: [PATCH] Permiso vistas implementados --- backend/app/api/v1/endpoints/permissions.py | 130 ++++++ backend/app/api/v1/router.py | 9 +- backend/app/models/permission.py | 79 +++- .../src/lib/components/Header.svelte | 69 ++- frontend-client/src/lib/routes-registry.ts | 11 + frontend-client/src/lib/stores/app.ts | 15 +- .../src/lib/stores/navPermissions.ts | 36 ++ .../src/routes/organization/+page.svelte | 89 ++-- .../src/routes/profile/+page.svelte | 94 +++- .../src/routes/tickets/+page.svelte | 171 ++++--- .../src/routes/usuarios/+page.svelte | 436 +++++++++++++----- .../src/lib/components/Sidebar.svelte | 115 ++--- frontend-internal/src/lib/routes-registry.ts | 18 + .../src/lib/stores/navPermissions.ts | 21 + .../src/routes/users/+page.svelte | 149 ++++-- 15 files changed, 1037 insertions(+), 405 deletions(-) create mode 100644 backend/app/api/v1/endpoints/permissions.py create mode 100644 frontend-client/src/lib/routes-registry.ts create mode 100644 frontend-client/src/lib/stores/navPermissions.ts create mode 100644 frontend-internal/src/lib/routes-registry.ts create mode 100644 frontend-internal/src/lib/stores/navPermissions.ts diff --git a/backend/app/api/v1/endpoints/permissions.py b/backend/app/api/v1/endpoints/permissions.py new file mode 100644 index 0000000..222b03e --- /dev/null +++ b/backend/app/api/v1/endpoints/permissions.py @@ -0,0 +1,130 @@ +"""Permissions endpoints - ServiceManagerWeb""" +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, delete +from typing import Dict +import uuid + +from app.core.database import get_db +from app.api.deps import get_current_user +from app.models.user import User, UserRole +from app.models.permission import TenantPermission, DEFAULT_PERMISSIONS, CLIENT_PERMISSIONS + +router = APIRouter() + + +@router.get("/my") +async def get_my_permissions( + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +) -> Dict[str, bool]: + """Obtener permisos efectivos del usuario actual.""" + if current_user.role.is_global: + # Staff interno — devolver permisos del rol por defecto + role_defaults = DEFAULT_PERMISSIONS.get(current_user.role.value, {}) + return role_defaults + + # Buscar overrides individuales primero + result = await db.execute( + select(TenantPermission).where( + TenantPermission.tenant_id == current_user.tenant_id, + TenantPermission.user_id == current_user.id, + ) + ) + user_overrides = {p.permission: p.granted for p in result.scalars().all()} + + # Buscar defaults del rol + result = await db.execute( + select(TenantPermission).where( + TenantPermission.tenant_id == current_user.tenant_id, + TenantPermission.user_id == None, + TenantPermission.role == current_user.role.value, + ) + ) + role_defaults = {p.permission: p.granted for p in result.scalars().all()} + + # Merge: override individual > default del rol > system default + system_defaults = DEFAULT_PERMISSIONS.get(current_user.role.value, {}) + effective = {**system_defaults, **role_defaults, **user_overrides} + return effective + + +@router.get("/user/{user_id}") +async def get_user_permissions( + user_id: uuid.UUID, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +) -> Dict[str, bool]: + """Obtener permisos efectivos de un usuario específico.""" + if current_user.role not in (UserRole.ADMIN, UserRole.SUPPORT_MANAGER, UserRole.CLIENT_ADMIN): + raise HTTPException(status_code=403, detail="Sin permisos") + + target = await db.get(User, user_id) + if not target: + raise HTTPException(status_code=404, detail="Usuario no encontrado") + + # Buscar overrides individuales + result = await db.execute( + select(TenantPermission).where( + TenantPermission.user_id == user_id, + ) + ) + user_overrides = {p.permission: p.granted for p in result.scalars().all()} + + # Buscar defaults del rol en el tenant + result = await db.execute( + select(TenantPermission).where( + TenantPermission.tenant_id == target.tenant_id, + TenantPermission.user_id == None, + TenantPermission.role == target.role.value, + ) + ) + role_defaults = {p.permission: p.granted for p in result.scalars().all()} + + system_defaults = DEFAULT_PERMISSIONS.get(target.role.value, {}) + effective = {**system_defaults, **role_defaults, **user_overrides} + return effective + + +@router.put("/user/{user_id}") +async def update_user_permissions( + user_id: uuid.UUID, + data: Dict[str, bool], + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + """ + Actualizar permisos individuales de un usuario. + Solo ADMIN, SUPPORT_MANAGER o CLIENT_ADMIN pueden hacerlo. + """ + if current_user.role not in (UserRole.ADMIN, UserRole.SUPPORT_MANAGER, UserRole.CLIENT_ADMIN): + raise HTTPException(status_code=403, detail="Sin permisos") + + target = await db.get(User, user_id) + if not target: + raise HTTPException(status_code=404, detail="Usuario no encontrado") + + # Borrar overrides existentes del usuario + await db.execute( + delete(TenantPermission).where( + TenantPermission.user_id == user_id, + ) + ) + + # Insertar nuevos overrides + from datetime import datetime + now = datetime.utcnow() + for permission, granted in data.items(): + db.add(TenantPermission( + id=uuid.uuid4(), + tenant_id=target.tenant_id, + user_id=user_id, + role=target.role.value, + permission=permission, + granted=granted, + created_at=now, + updated_at=now, + )) + + await db.commit() + return {"message": "Permisos actualizados"} \ No newline at end of file diff --git a/backend/app/api/v1/router.py b/backend/app/api/v1/router.py index 56fd3e2..3164236 100644 --- a/backend/app/api/v1/router.py +++ b/backend/app/api/v1/router.py @@ -6,7 +6,7 @@ Router principal para la API v1 from fastapi import APIRouter -from app.api.v1.endpoints import auth, health, tenants, users, systems, categories, tickets, client_profile, audit, sla, reports, participants +from app.api.v1.endpoints import auth, health, tenants, users, systems, categories, tickets, client_profile, audit, sla, reports, participants, permissions api_router = APIRouter() @@ -86,4 +86,11 @@ api_router.include_router( reports.router, prefix="/reports", tags=["reports"] +) + +# Permissions routes +api_router.include_router( + permissions.router, + prefix="/permissions", + tags=["permissions"] ) \ No newline at end of file diff --git a/backend/app/models/permission.py b/backend/app/models/permission.py index e242709..c7c6f42 100644 --- a/backend/app/models/permission.py +++ b/backend/app/models/permission.py @@ -14,24 +14,99 @@ from app.core.database import Base, GUID CLIENT_PERMISSIONS = [ + # Permisos funcionales "view_tickets", "create_tickets", "close_tickets", "view_reports", "manage_tenant_users", "create_issues", + # Permisos de navegación - cliente + "nav:tickets", + "nav:organization", + "nav:usuarios", +] + +INTERNAL_PERMISSIONS = [ + # Permisos de navegación - interno + "nav:tickets", + "nav:users", + "nav:tenants", + "nav:categories", + "nav:systems", + "nav:sla", + "nav:audit", + "nav:reports", ] DEFAULT_PERMISSIONS = { - "CLIENT_ADMIN": {p: True for p in CLIENT_PERMISSIONS}, + "CLIENT_ADMIN": { + # Funcionales — todo ON + "view_tickets": True, + "create_tickets": True, + "close_tickets": True, + "view_reports": True, + "manage_tenant_users": True, + "create_issues": True, + # Navegación cliente — todo ON + "nav:tickets": True, + "nav:organization": True, + "nav:usuarios": True, + }, "CLIENT_USER": { + # Funcionales "view_tickets": True, "create_tickets": True, "close_tickets": False, "view_reports": False, "manage_tenant_users": False, "create_issues": False, - } + # Navegación cliente + "nav:tickets": True, + "nav:organization": False, + "nav:usuarios": False, + }, + "ADMIN": { + # Navegación interna — todo ON + "nav:tickets": True, + "nav:users": True, + "nav:tenants": True, + "nav:categories": True, + "nav:systems": True, + "nav:sla": True, + "nav:audit": True, + "nav:reports": True, + }, + "SUPPORT_MANAGER": { + "nav:tickets": True, + "nav:users": True, + "nav:tenants": False, + "nav:categories": True, + "nav:systems": True, + "nav:sla": True, + "nav:audit": True, + "nav:reports": True, + }, + "AGENT": { + "nav:tickets": True, + "nav:users": False, + "nav:tenants": False, + "nav:categories": False, + "nav:systems": False, + "nav:sla": False, + "nav:audit": False, + "nav:reports": False, + }, + "AUDITOR": { + "nav:tickets": True, + "nav:users": False, + "nav:tenants": False, + "nav:categories": False, + "nav:systems": False, + "nav:sla": False, + "nav:audit": True, + "nav:reports": True, + }, } diff --git a/frontend-client/src/lib/components/Header.svelte b/frontend-client/src/lib/components/Header.svelte index a86b810..23cf338 100644 --- a/frontend-client/src/lib/components/Header.svelte +++ b/frontend-client/src/lib/components/Header.svelte @@ -1,5 +1,7 @@
- {#if isMenuOpen}
(isMenuOpen = false)} /> {/if} diff --git a/frontend-client/src/lib/routes-registry.ts b/frontend-client/src/lib/routes-registry.ts new file mode 100644 index 0000000..6e3ffc1 --- /dev/null +++ b/frontend-client/src/lib/routes-registry.ts @@ -0,0 +1,11 @@ +export interface RouteDefinition { + key: string; + label: string; + path: string; +} + +export const CLIENT_ROUTES: RouteDefinition[] = [ + { key: 'nav:tickets', label: 'Mis Tickets', path: '/tickets' }, + { key: 'nav:organization', label: 'Organización', path: '/organization' }, + { key: 'nav:usuarios', label: 'Usuarios', path: '/usuarios' }, +] \ No newline at end of file diff --git a/frontend-client/src/lib/stores/app.ts b/frontend-client/src/lib/stores/app.ts index 7e70a94..de10a88 100644 --- a/frontend-client/src/lib/stores/app.ts +++ b/frontend-client/src/lib/stores/app.ts @@ -1,7 +1,6 @@ -import { writable } from 'svelte/store'; -import { auth } from './auth.js'; -import { get } from 'svelte/store'; import type { Writable } from 'svelte/store'; +import { get, writable } from 'svelte/store'; +import { auth } from './auth.js'; // Types export interface Category { @@ -60,15 +59,15 @@ function createAppStore() { // Load categories loadCategories: async () => { update(state => ({ ...state, isLoading: true, error: null })); - + try { const categories = await apiCall('/categories/'); update(state => ({ ...state, categories, isLoading: false })); } catch (error) { - update(state => ({ - ...state, - isLoading: false, - error: error instanceof Error ? error.message : 'Failed to load categories' + update(state => ({ + ...state, + isLoading: false, + error: error instanceof Error ? error.message : 'Failed to load categories' })); } }, diff --git a/frontend-client/src/lib/stores/navPermissions.ts b/frontend-client/src/lib/stores/navPermissions.ts new file mode 100644 index 0000000..114376e --- /dev/null +++ b/frontend-client/src/lib/stores/navPermissions.ts @@ -0,0 +1,36 @@ +import { auth } from '$lib/stores/auth'; +import { get, writable } from 'svelte/store'; + +function createNavPermissionsStore() { + const { subscribe, set } = writable>({}); + + return { + subscribe, + load: async () => { + try { + const authState = get(auth); + const slug = authState.user?.tenant_slug || authState.user?.tenant_id || ''; + const headers: Record = { + 'X-App': 'client', + 'X-Tenant-Slug': slug, + }; + if (authState.token) { + headers['Authorization'] = `Bearer ${authState.token}`; + } + const res = await fetch('/api/v1/permissions/my', { + credentials: 'include', + headers + }); + if (res.ok) { + const perms = await res.json(); + set(perms); + } + } catch (e) { + set({}); + } + }, + clear: () => set({}) + }; +} + +export const navPerms = createNavPermissionsStore(); \ No newline at end of file diff --git a/frontend-client/src/routes/organization/+page.svelte b/frontend-client/src/routes/organization/+page.svelte index a49c749..32fe90c 100644 --- a/frontend-client/src/routes/organization/+page.svelte +++ b/frontend-client/src/routes/organization/+page.svelte @@ -1,8 +1,8 @@