Auditoria de plataforma: rutas rotas y errores por rol
Recorrido dinamico de las 26 rutas con los 10 roles (260 visitas con navegador real) mas revision estatica de endpoints FE vs BE. Hallazgos y correcciones: - frontend.py: expedientes_recientes no tenia @http.route (el endpoint /expedientes-recientes respondia 404). Se expone con GET + lectura. - auth.py: MEDICO_BLOCKED_PREFIXES hacia match por startswith, asi que /sales_goal quedaba bloqueado para medicos (403 en consola en todas las pantallas por el widget de meta del mes). Ahora el match exige frontera de path (/sales bloquea /sales/x pero no /sales_goal). - Reportes: getCommissions (endpoint admin-only) iba dentro de un Promise.all; el 403 tumbaba todo el tab General para recepcion, gerencia, enfermeria, psicologa, farmacia y lectura. Ahora va en peticion aislada y solo se llama para admin; la seccion de comisiones del tab General solo se renderiza para admin. - Matriz de menus guardada: medico habia quedado fuera de /pacientes y /visitas (los defaults del codigo lo incluyen y los medicos lo necesitan para ver clientes e historial). Restaurado via PUT /roles/permissions (queda en skeen_config_audit). Verificado con navegador: medico entra a /pacientes y /visitas sin 403; recepcion y gerencia cargan /reportes sin error; admin sigue viendo la seccion de comisiones. Usuarios temporales de auditoria eliminados.
This commit is contained in:
@@ -36,6 +36,7 @@ import { Card, Button, Input, PageHeader, Skeleton, EmptyState, toast } from '..
|
||||
import { odooApi } from '../services/odoo';
|
||||
import type { CommissionRow } from '../services/odoo';
|
||||
import { exportToExcel } from '../lib/exporter';
|
||||
import { useAuth } from '../lib/auth';
|
||||
import {
|
||||
REPORT_PRESETS,
|
||||
RangeOverrideHint,
|
||||
@@ -144,6 +145,8 @@ const ReportesContent: FC = () => {
|
||||
const familiaKey = familiaDe(tab);
|
||||
const familia = FAMILIAS.find((f) => f.key === familiaKey) || FAMILIAS[0];
|
||||
const { favs, toggle, isFav } = useReportFavorites();
|
||||
const { hasRole } = useAuth();
|
||||
const esAdmin = hasRole('admin');
|
||||
// Rango global del módulo (persistente, compartido por todas las pestañas)
|
||||
const { start, end, preset, setRange } = useGlobalReportRange();
|
||||
const [today, setToday] = useState(() => new Date().toISOString().split('T')[0]);
|
||||
@@ -164,11 +167,10 @@ const ReportesContent: FC = () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const [salesRes, aptRes, cashRes, commRes] = await Promise.all([
|
||||
const [salesRes, aptRes, cashRes] = await Promise.all([
|
||||
odooApi.getSalesReport(start, end),
|
||||
odooApi.getAppointmentsReport(start, end),
|
||||
odooApi.getCashReport(today),
|
||||
odooApi.getCommissions(start, end),
|
||||
]);
|
||||
if (salesRes.status === 'success')
|
||||
setSalesReport({
|
||||
@@ -181,10 +183,22 @@ const ReportesContent: FC = () => {
|
||||
setAppointmentsReport({ total: aptRes.total, by_state: aptRes.by_state });
|
||||
if (cashRes.status === 'success')
|
||||
setCashReport({ total: cashRes.total, by_method: cashRes.by_method });
|
||||
// Comisiones viajan en petición aparte y solo para admin: el endpoint
|
||||
// exige rol admin y un 403 no debe tumbar (ni ensuciar la consola del)
|
||||
// resto del tab General para los demás roles.
|
||||
if (esAdmin) {
|
||||
odooApi.getCommissions(start, end)
|
||||
.then((commRes) => {
|
||||
if (commRes.status === 'success') {
|
||||
setCommissions(commRes.commissions || []);
|
||||
setCommTotals({ base: commRes.total_base || 0, commission: commRes.total_commission || 0 });
|
||||
}
|
||||
})
|
||||
.catch(() => { /* sin permiso: sección de comisiones queda vacía */ });
|
||||
} else {
|
||||
setCommissions([]);
|
||||
setCommTotals({ base: 0, commission: 0 });
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Error al cargar reportes');
|
||||
toast.error('Error al cargar reportes');
|
||||
@@ -194,7 +208,7 @@ const ReportesContent: FC = () => {
|
||||
}
|
||||
};
|
||||
load();
|
||||
}, [start, end, today, tab]);
|
||||
}, [start, end, today, tab, esAdmin]);
|
||||
|
||||
const aptData = appointmentsReport
|
||||
? Object.entries(appointmentsReport.by_state).map(([name, value]) => ({ name, value }))
|
||||
@@ -523,6 +537,7 @@ const ReportesContent: FC = () => {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{esAdmin && (
|
||||
<Card className="mt-4 sm:mt-6">
|
||||
<Card.Body>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 mb-4">
|
||||
@@ -572,6 +587,7 @@ const ReportesContent: FC = () => {
|
||||
)}
|
||||
</Card.Body>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Layout>
|
||||
|
||||
@@ -152,7 +152,11 @@ def require_role(min_role='lectura'):
|
||||
return _json({'status': 'error', 'message': 'No autenticado'}, 401)
|
||||
if user.role_rank() < required:
|
||||
return _json({'status': 'error', 'message': 'Sin permiso'}, 403)
|
||||
if user.role == 'medico' and request.httprequest.path.startswith(MEDICO_BLOCKED_PREFIXES):
|
||||
if user.role == 'medico' and any(
|
||||
request.httprequest.path == p or request.httprequest.path.startswith(
|
||||
p if p.endswith('/') else p + '/')
|
||||
for p in MEDICO_BLOCKED_PREFIXES
|
||||
):
|
||||
return _json({'status': 'error', 'message': 'Sin permiso'}, 403)
|
||||
request.frontend_user = user
|
||||
return func(self, *args, **kw)
|
||||
|
||||
@@ -2797,6 +2797,7 @@ class SkeenFrontendController(http.Controller):
|
||||
'remaining_count': max(0, count - pagados) if count else 0,
|
||||
}
|
||||
|
||||
@http.route('/skeen/frontend/v1/expedientes-recientes', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False)
|
||||
@require_role('lectura')
|
||||
def expedientes_recientes(self, **kw):
|
||||
"""Pacientes con visitas recientes (últimos 30 días por default)"""
|
||||
|
||||
Reference in New Issue
Block a user