Revisión multi-revisor con 5 dimensiones de calidad · Informe consolidado
src/api/auth/jwt_handler.py:14
SECRET_KEY = "nutritrack_secret_2026" está hardcodeada directamente en el módulo. Cualquier persona con acceso al repositorio puede firmar tokens arbitrarios y suplantar a cualquier usuario.
SECRET_KEY = os.environ["JWT_SECRET_KEY"]. Añadir validación al arrancar que falle si no está definida. Rotar el secreto inmediatamente en producción. Revisar si el secreto fue expuesto en algún commit anterior con git log -S "nutritrack_secret".
src/db/queries/patient_queries.py:67
f"WHERE patient_id={patient_id} AND date BETWEEN '{start}' AND '{end}'". Los parámetros start y end provienen del query string de la request sin sanitizar.
cursor.execute("WHERE patient_id = %s AND date BETWEEN %s AND %s", (patient_id, start, end)). Validar el formato de fechas con datetime.fromisoformat() antes de pasarlas a la query. Añadir test de regresión de inyección.
src/api/patients/export.py:31
GET /patients/{id}/export valida que el token JWT sea válido pero no comprueba que el paciente id pertenezca a la clínica del usuario autenticado. Un dietista de la clínica A puede exportar datos de pacientes de la clínica B.
assert patient.clinic_id == current_user.clinic_id antes de procesar la exportación. Incluir test de autorización cruzada entre clínicas.src/api/patients/export.py:78–102
StreamingResponse de FastAPI + generador que lea en chunks de 100 rows. Para PDF, generar mediante ReportLab con paginación, no cargar todo en buffer.src/api/auth/jwt_handler.py:88–110
refresh_tokens con invalidated_at. Extraer lógica a AuthService.tests/test_export.py — cobertura: 34%
test_export_empty_patient, test_export_invalid_date_range, test_export_cross_clinic_forbidden, test_export_db_timeout_returns_503. Objetivo mínimo: 80% cobertura en export.py.src/db/queries/patient_queries.py:112–130
SELECT nr.*, fc.name as category FROM nutrition_records nr JOIN food_categories fc ON nr.category_id = fc.id.src/models/patient.py
Patient (solo datos), PatientGDPRService (anonimización/derecho al olvido), PatientRepository (persistencia).src/components/ExportModal.tsx:8, 45
role="dialog", aria-modal="true" y aria-labelledby. El foco no queda atrapado dentro del modal al abrirlo, permitiendo navegar con Tab a elementos del fondo. Viola WCAG 2.1 AA (4.1.2, 2.1.2).
role="dialog" aria-modal="true" aria-labelledby="export-modal-title". Implementar focus trap con @radix-ui/react-dialog o custom hook useFocusTrap.src/api/patients/export.py:55
logger.debug(f"Exporting {records} for patient {patient.full_name}, BMI={patient.bmi}") — datos personales y de salud en logs de aplicación. Incumple minimización de datos GDPR.
logger.info("export_started patient_id=%s records=%d", patient.id, len(records)). Implementar política de retención de logs ≤30 días.tests/test_export.py
Patient puede alterar el esquema de exportación silenciosamente y romper integraciones downstream de las clínicas.
test_export_csv_schema que parsea el CSV resultante y verifica las columnas esperadas contra una lista fija definida como constante.src/components/ExportModal.tsx:89, 97
aria-label. Lectores de pantalla anuncian el botón como vacío.
aria-label="Cancelar exportación" y aria-label="Confirmar exportación", o incluir texto oculto con clase sr-only.patientId, startDate en export.py mezclan camelCase con el resto del codebase que usa patient_id, start_date. Aplicar Ruff/Black con config consistente.
export.py carecen de docstrings. Añadir Google-style docstrings con tipos de parámetros y retorno.
#888 sobre fondo blanco. Ratio 3.8:1 — por debajo del mínimo WCAG AA de 4.5:1. Cambiar a #767676 o más oscuro.
JWT_EXPIRY = 3600 definida en jwt_handler.py:8 y también en router.py:5. Centralizar en settings.py.
from typing import Optional
Low
Architecture
src/models/patient.py:3 importa Optional sin usarlo. Eliminar. Configurar Ruff con F401 en CI para prevenir regresiones.
| Dimensión | Critical | High | Medium | Low | Total |
|---|---|---|---|---|---|
| Security | 2 | 2 | 1 | 0 | 5 |
| Performance | 1* | 1 | 1 | 0 | 3 |
| Architecture | 0 | 1* | 1 | 4 | 6 |
| Testing | 0 | 1 | 1 | 0 | 2 |
| Accessibility | 0 | 0 | 2 | 1 | 3 |
| Total | 2 | 4 | 6 | 5 | 17 |
* Hallazgo contabilizado en dimensión primaria. Merged findings contribuyen a ambas dimensiones pero cuentan una sola vez en Total.
export.py al 80% antes del siguiente release. Añadir tests de seguridad (autorización cruzada, SQL injection) al pipeline CI.@radix-ui/react-dialog que provee accesibilidad out-of-the-box, resolviendo ME-003 y ME-006 simultáneamente.