ProtectedRoute mostraba 'Restaurando sesión...' siempre en el primer render,
incluso cuando el usuario nunca tuvo sesión, porque el state inicial siempre
arrancaba sin token. La heurística ahora consulta hasHydrated() de Zustand y
usa el user persistido (que sí sobrevive al reload) para decidir si vale la
pena intentar refresh: si nunca hubo user, redirige directo al login sin
flash.
Tests cubren el cold visit (sin user, no se llama a refresh) y el warm visit
(con user persistido, muestra el restoring y resuelve).
apiClient devolvía null casteado a T en respuestas 204, lo que silenciaba un
error de tipos en tiempo de compilación. La auditoría confirmó que ningún
caller actual pega a un endpoint 204, por lo que el cast era código muerto.
Se separa en dos funciones:
- apiClient<T>: devuelve T y lanza si el server responde 204 sin body.
- apiClientVoid: devuelve void en 204, para endpoints que no retornan body.
Tests cubren la deduplicación del refresh ante 401 concurrentes, el path
200-con-body, el throw en 204 y el path void de apiClientVoid.
El store de auth duplicaba el token en dos campos (token y accessToken) que
quedaban siempre iguales pero con consumers divididos. Se elimina token: el
accessToken queda como única fuente de verdad.
LoginResponse refleja el contrato real del backend (services/backend-api auth
handler emite access_token primario y token como legacy compat). LoginPage lee
response.access_token directo, sin fallback. refresh_token sale del body porque
viaja en cookie HttpOnly, no en el JSON.
Tests pinean el contrato: setSession y logout no escriben token, y los campos
de LoginResponse mantienen los tipos esperados.
Configura el runner de tests inline en vite.config.ts replicando el setup de
frontend-admin (jsdom, setupFiles, restoreMocks, clearMocks). Agrega el archivo
src/test/setup.ts con el mock de storage y la flag IS_REACT_ACT_ENVIRONMENT.
Esto habilita escribir tests RED-GREEN para los cambios de auth, api-client,
ProtectedRoute y access-requests que vienen en los siguientes commits.
- Add static OpenAPI 3.0.3 spec (services/backend-api/static/openapi.yaml)
covering all ~70 endpoints with schemas, security, tags and responses
- Serve Swagger UI at GET /docs (embedded HTML, dark OmniOil theme)
- Serve raw YAML at GET /docs/openapi.yaml (include_str! at compile time)
- Add Tower middleware (api_version.rs) injecting X-API-Version: 1 and
X-Powered-By: OmniOil/1.0 on every response
- Replace static /health handler with enhanced health_check that probes
DB latency, tracks uptime via OnceLock, returns structured HealthResponse
- Expand API Explorer frontend: 70+ endpoints in 10 groups, PATCH color
coding (purple), group filter pills, count badge, Swagger UI links
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- log_cleaner.rs: elimina archivos edge-agent.log.YYYY-MM-DD con
más de 30 días al arrancar y cada 24h (evita llenado de disco 24/7)
- persistence_manager: cleanup_old_queue_entries() borra telemetría
no enviada con más de 7 días (protege ante outages prolongados)
- main.rs + service_manager.rs: spawns de ambas tareas de limpieza
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
HIGH-5: Rate limiting por IP con governor (Docker-compatible via X-Real-IP)
- Login/refresh: 10 req/min; Register: 5 req/min
- services/backend-api/src/rate_limiter.rs (nuevo módulo)
HIGH-6: HSTS + X-Forwarded-Proto en Nginx; TLS terminado por Dokploy
- infrastructure/nginx/gateway.conf
HIGH-8: Refresh token movido a cookie HttpOnly; access token solo en memoria
- Backend: login emite cookie, refresh rota cookie, logout la borra
- Frontend: Zustand v3 sin tokens en localStorage; credentials: include
MED-1: Eliminado PBKDF2 legacy; Argon2id m=65536 t=3 p=1 en todos los hashes
- services/shared-lib/src/overlay.rs (imports limpiados)
- services/backend-api/src/handlers/auth.rs (hash_password helper)
MED-5: Rotación atómica de refresh tokens con detección de replay
SECURITY_AUDIT_REPORT.md: riesgo reducido a 🟡 MEDIO
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>