Caso Real: Portal del Conductor — Albaranes con QR para Camiones con VB.NET y Vue 3
Web para conductores de camión de COVAP: login con la matrícula, albaranes desde ayer con QR al PDF, edición y devolución con documento nuevo vía orquestación del ERP, y panel de administración. API REST en VB.NET + SPA en Vue 3, Oracle en solo lectura y tokens opacos con PBKDF2.
Tabla de contenidos
Resumen
Desarrollé el Portal del Conductor para COVAP: una aplicación web donde cada conductor de camión accede con su matrícula, consulta sus albaranes (decas) desde ayer en adelante y abre el PDF de cada uno escaneando su código QR. Desde la ficha puede editar el albarán o hacer una devolución (ambos generan un documento nuevo), y los administradores gestionan conductores desde un panel admin con alta masiva. Todo con una API REST en VB.NET y una SPA en Vue 3, Oracle en solo lectura y autenticación por tokens opacos. Login con rate limiting y anti-enumeración, 13 endpoints propios y despliegue en IIS sin CORS (front en / + API en /api).
El problema
Los conductores necesitaban sus albaranes del día sin depender de papel ni de llamadas a oficina. La situación de partida era:
- Albarán en papel — el conductor dependía del documento físico para cada entrega
- Llamadas a oficina — cualquier duda sobre un albarán implicaba llamar por teléfono
- Sin autonomía — no había forma de consultar “qué tengo asignado hoy” desde el móvil
- Devoluciones lentas — corregir un albarán o devolver mercancía era un trámite manual
- Gestión de accesos dispersa — altas y bloqueos de conductores sin una herramienta central
Lo que me pidieron: “Queremos que cada conductor entre con su matrícula, vea sus albaranes del día, abra el PDF con el QR y pueda corregir o devolver desde el móvil. Y un panel para gestionar las cuentas.”
La arquitectura que elegí
┌──────────────────────────────────────────────────────────────────┐
│ NAVEGADOR (móvil u ordenador) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ SPA VUE 3 + VUE ROUTER 4 + VITE 6 │ │
│ │ │ │
│ │ /login → /albaranes → /deca/{id} → /admin → /cuenta │ │
│ │ Tarjetas con QR (qrcode.vue) + buscador + paginación │ │
│ │ Token + matrícula + rol en localStorage │ │
│ └──────────────────────┬──────────────────────────────────────┘ │
└─────────────────────────┼────────────────────────────────────────┘
│ HTTP /api + Authorization: Bearer
▼
┌──────────────────────────────────────────────────────────────────┐
│ IIS (mismo dominio → sin CORS) │
│ front en / + API en /api │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ API REST — ASP.NET WEB API 2 (VB.NET, .NET 4.7.2) │ │
│ │ AuthController · DecasController · AdminController │ │
│ │ FiltroToken (Bearer) · FiltroAdmin (rol) · FiltroExcepciones│ │
│ └──────┬──────────────────────────────┬────────────────────────┘ │
└─────────┼──────────────────────────────┼──────────────────────────┘
│ solo EXECUTE sobre SPs │ solo lectura (OPENQUERY
│ (login mínimo privilegio) │ dentro del SP, nunca
▼ │ desde .NET)
┌────────────────────┐ ┌─────────────┴─────────────────────────┐
│ SQL SERVER DB_APP │ │ ERP ORACLE (solo lectura) │
│ Usuarios, tokens │ │ T_DOCUMENTOCONTROL + LINEAS │
│ y overlays de │ │ │
│ edición/devolución│ │ Documento nuevo vía ORQUESTACIÓN │
└────────────────────┘ │ ORCH_INSERT_DOC (Basic Auth, ~1 s) │
└───────────────────────────────────────┘
Separación radical: el front no toca la base de datos
La API es la única que accede a datos. El front es una SPA que consume HTTP con Authorization: Bearer. Esto me permitió desplegar ambas piezas de forma independiente bajo el mismo IIS y eliminar CORS por completo.
Oracle solo se lee, y solo desde un stored procedure
Regla de oro del proyecto: el código .NET jamás consulta Oracle directamente. Toda lectura de albaranes pasa por un SP de DB_APP que usa OPENQUERY contra el linked server. Si mañana cambia el ERP, solo se tocan los SPs.
Los documentos nuevos los crea la orquestación del ERP
Editar o devolver no modifica Oracle: se llama a ORCH_INSERT_DOC, que genera el QR + PDF nuevos, y el vínculo (original → documento nuevo) se guarda en DB_APP. El albarán original se conserva siempre.
Autenticación con tokens opacos (adiós Session)
Me aparté conscientemente de la convención COVAP (WebForms + Session) y usé tokens opacos Bearer:
- Token de 256 bits, caducidad de 60 minutos con renovación deslizante; en BD solo vive su hash SHA-256 (
clsToken.MINUTOS = 60) - Contraseñas PBKDF2 con 100.000 iteraciones, formato
iteraciones:saltHex:hashHex, mínimo 8 caracteres - Rate limiting en memoria: 5 fallos por matrícula en 15 minutos → HTTP
429 - Anti-enumeración: el login verifica siempre un hash PBKDF2 (real o señuelo) en tiempo constante, para no revelar si la matrícula existe o está activa
- La matrícula del token manda: la autorización usa siempre la matrícula de la sesión validada, nunca la que envíe el cliente
En el front, el guard del router lo deja claro:
// router/index.js — guard de autenticación y de admin
router.beforeEach((to) => {
const { token, esAdmin } = auth.estado();
if (to.meta.requiereAuth && !token) return '/login';
if (to.meta.requiereAdmin && !esAdmin) return '/albaranes';
});
Sin token → /login. Ruta admin sin rol → de vuelta al listado. Simple y sin fisuras.
Edición y devolución sin escribir en Oracle
Desde la ficha del albarán (/deca/{ID_DOCO_CRIPTED}), el conductor puede:
- Editar — matrícula, fecha, origen, destino, cantidad, kg y descripción. El resto de campos se heredan del original.
- Devolver — intercambia origen/destino y permite editar descripción, cantidad y kg. Una devolución no se vuelve a editar.
En ambos casos se genera un QR y PDF nuevos, la app vuelve al listado y resalta la tarjeta del documento nuevo (etiqueta “nuevo”, salto a su página y scroll hasta ella). Las versiones previas siguen disponibles en “Ver versiones anteriores”.
Validaciones estrictas: fecha de ayer en adelante, matrícula en mayúsculas con formato, cantidad y kg enteros, descripción de 200 caracteres como máximo. Las Observaciones no se editan. Y si se cambia la matrícula, el albarán se reasigna (deja de verse con la original).
Detalle técnico: la URL y el QR usan ID_DOCO_CRIPTED (hex), porque el ID_DOCO real contiene | y +, no aptos para URLs.
Panel de administración con protecciones anti-bloqueo
El rol admin (EsAdmin = 1) accede a /admin: alta individual y masiva (pegando varias matrículas con una contraseña común), activar/desactivar, marcar o quitar admin, resetear contraseñas y eliminar cuentas. Desactivar revoca los tokens; eliminar borra también sus tokens.
Lo interesante son las protecciones contra el auto-bloqueo, en código y en base de datos:
- Un admin no puede quitarse el rol, desactivarse ni eliminarse (
AUTO_ADMIN,AUTO_BAJA,AUTO_ELIMINAR) - Siempre debe quedar al menos un administrador activo (
ULTIMO_ADMIN) - Sin rol admin, cualquier
api/admin/*responde403
Problemas reales que encontré (y cómo los resolví)
1. El ERP es de solo lectura y no acepta escrituras
No había forma de modificar un albarán en Oracle directamente.
Solución: patrón overlay. La edición/devolución llama a la orquestación ORCH_INSERT_DOC (síncrona, ~1 s; si falla, no se guarda nada) y el resultado se persiste en T_DecasEdicion / T_DecasDevolucion. Oracle intacto, trazabilidad total.
2. El identificador del albarán no sirve para URLs
El ID_DOCO (KCOO|DCTO|DOCO|NUM) contiene | y +, que rompen rutas y QRs.
Solución: usar ID_DOCO_CRIPTED (hex) en rutas, QR y API. Para el documento nuevo, el DOCO_ENCRIPTADO que devuelve la orquestación.
3. Un camión no debe ver albaranes ajenos bajo ningún concepto
Bastaría manipular un parámetro para espiar otra matrícula.
Solución: la autorización usa siempre la matrícula del token validado por FiltroToken. Lo que envíe el cliente se ignora. El listado, además, está fijado a “desde ayer en adelante”.
4. El login no debe revelar si una matrícula existe
Un atacante podría enumerar matrículas válidas midiendo tiempos o respuestas.
Solución: verificación PBKDF2 siempre (hash real o señuelo) en tiempo constante + rate limiting de 5 fallos/15 min por matrícula → 429. Mismo coste, misma respuesta.
5. El proxy/WAF solo permite GET y POST
PATCH y DELETE no atraviesan la infraestructura de producción.
Solución: todas las operaciones mutantes (actualizar, eliminar, cambiar contraseña) usan POST. Decisión documentada y aplicada en los 13 endpoints.
6. Recuar permisos sin romper nada al re-desplegar SPs
Los GRANT EXECUTE del login de mínimo privilegio se pierden si un SP se recrea con DROP + CREATE.
Solución: todos los SPs se definen con CREATE OR ALTER, que conserva los permisos. Y documentado: si algún día se usa DROP, hay que re-ejecutar el script 009.
Funcionalidades clave
| Módulo | Funcionalidad | Detalle |
|---|---|---|
| Acceso | Registro + login por matrícula | Normalizada a mayúsculas, contraseña mín. 8 |
| Listado | Albaranes desde ayer | Buscador + paginación en cliente (6/12/24) |
| Tarjeta | QR por albarán | Codifica la URL del visor externo del PDF |
| Detalle | Ficha + PDF | PDF embebido en base64 bajo demanda |
| Edición | 7 campos editables | Genera QR + PDF nuevos vía orquestación |
| Devolución | Origen/destino invertidos | No re-editable, con su propio documento |
| Versiones | Historial completo | Modal Original + cada modificación |
| Resaltado | Tarjeta “nuevo” | Salto de página + scroll tras guardar |
| Admin | Gestión de conductores | Alta masiva, roles, reseteo, borrado |
| Cuenta | Cambio de contraseña | Verificando la actual |
| API | 13 endpoints | Códigos de error propios en JSON |
Despliegue y seguridad
- Servidor: IIS con front en
/y API en/api(mismo dominio, sin CORS), fallback SPA aindex.html - Cabeceras:
X-Content-Type-Options,X-Frame-Options: DENY,Referrer-Policy,Strict-Transport-Security,Content-Security-PolicyyPermissions-Policyen front y API - Mínimo privilegio: el login
usuario_apisolo tieneEXECUTEsobre los SPs; sin acceso directo a tablas nisa - Secretos como plantilla:
connectionStrings.configyorquestacion.configse versionan como template, con los valores reales solo en servidor - Desarrollo: Visual Studio (IIS Express
:52794) + Vite (:5173) con proxy/api;VITE_URL_ALBARANconfigurable para el visor del PDF - Base de datos: 11 scripts SQL en orden (
001…011); el primer admin se marca con elUPDATEcomentado en007
Números del proyecto
| Métrica | Valor |
|---|---|
| Backend | VB.NET + ASP.NET Web API 2 (.NET 4.7.2) |
| Frontend | Vue 3 + Vue Router 4 + Vite 6 |
| Controladores | 3 (Auth, Decas, Admin) |
| Endpoints | 13 (todos mutantes en POST) |
| Tablas DB_APP | 4 (usuarios, tokens, ediciones, devoluciones) |
| Scripts SQL | 11 (SPs con CREATE OR ALTER) |
| Vistas SPA | 6 (Login, Registro, Albaranes, Detalle, Admin, Cuenta) |
| Token | Opaco 256 bits, SHA-256 en BD, 60 min deslizantes |
| Hash contraseña | PBKDF2 100.000 iteraciones |
| Rate limiting | 5 fallos / 15 min → 429 |
| Orquestación ERP | Síncrona (~1 s), sin escritura directa en Oracle |
| Tiempo de desarrollo | 2 meses |
Lo que aprendí
- El token opaco simplifica todo — frente a Session, un Bearer con hash SHA-256 y renovación deslizante es más fácil de razonar, de revocar y de desplegar en granja.
- La autorización debe salir del token, nunca del cliente — una sola regla (“la matrícula del token manda”) elimina familias enteras de vulnerabilidades IDOR.
- El hash señuelo es barato y eficaz — verificar PBKDF2 siempre, exista o no el usuario, cierra la enumeración sin coste perceptible.
CREATE OR ALTERprotege los permisos — un detalle de SQL Server que evita romper el mínimo privilegio en cada despliegue.- POST-only no es un hack, es compatibilidad — adaptarse al proxy/WAF desde el diseño evita sorpresas en producción.
- Las protecciones anti-bloqueo se ponen el día uno — impedir el auto-borrado del último admin parece obvio hasta que alguien lo hace.
¿Necesitas algo similar?
Si tu empresa necesita un portal para conductores o personal de campo con accesos por identificador propio, documentos con QR, integración de solo lectura con un ERP existente o un panel de administración con roles, hablemos.
Más casos reales: