1. Autenticación y usuarios
Cubre POST /auth/register, POST /auth/login, GET /auth/verify, el módulo people (usuarios internos del equipo) y permissions (roles/grupos y permisos). Es la base de todo lo demás: sin esto no se puede ejecutar ningún otro módulo.
Historial de ejecución
| Ciclo | Fecha | Ejecutado por | Casos ejecutados | Resultado |
|---|---|---|---|---|
| 1 | 2026-10-09 | Claude (Playwright MCP, staging) | AUTH-01 a 13 | ❌ 4 bugs corregidos en el momento (company_name faltante, service worker interceptando /auth/verify, cola de sync cross-sesión, 500 crudo en email duplicado) + feature de limpieza de cuentas sin verificar agregada. 3 bugs nuevos encontrados en roles/permisos (AUTH-12/13), sin corregir en este ciclo. |
| 2 | 2026-10-09 | Claude (Playwright MCP, staging) | AUTH-11, 14–19 + reconfirmación de AUTH-12/13 y del hallazgo cross-sesión de AUTH-08 | ❌→✅ Los 3 pendientes del ciclo 1 (permisos de rol, asignación de rol a usuario, cola cross-sesión) ya estaban corregidos por fuera de esta sesión — reconfirmados funcionando. 4 bugs nuevos encontrados y corregidos: email duplicado al crear usuario interno (500 crudo), suscripción bloqueaba el dashboard completo a cualquier usuario que no fuera el admin registrante, login no rechazaba usuarios desactivados, y el login no era insensible a mayúsculas en el email. Todos verificados en vivo en staging tras cada fix. |
| 2b | 2026-10-09 | Claude (a pedido explícito, extensión del fix de AUTH-17) | — (no es un caso nuevo del plan, extiende el fix de suscripción) | ✅ El fix de AUTH-17 solo evitaba el bloqueo indebido del dashboard — no hacía cumplir que una suscripción vencida/cancelada realmente bloqueara el acceso. Se agregó: Login rechaza con 403 a cualquier usuario no-admin cuando la suscripción de su empresa está expired o cancelled (el admin sigue pudiendo entrar, para poder renovar el plan). Verificado marcando la suscripción de prueba como expired: Cajero14 (no-admin) quedó bloqueado con el mensaje correcto; el admin pudo entrar con normalidad y vio la pantalla real de “Tu suscripción ha expirado” con los planes para renovar. |
Registro de empresa (admin)
CP AUTH-01: Registro exitoso
- Objetivo: confirmar que
POST /auth/registercrea la empresa y el usuario admin, y envía el correo de verificación.- Precondición: ninguna cuenta con el email a usar.
- Pasos: ir a la pantalla de registro → llenar Nombre, Nombre de la empresa, Email, Contraseña (≥6 caracteres) → enviar.
- Resultado esperado: mensaje de éxito indicando que se envió un correo de verificación; el usuario queda creado con
is_verified=falsey no puede iniciar sesión todavía.- Resultado obtenido: ❌→✅ Bug encontrado y corregido en staging. El formulario de registro no tenía el campo “Nombre de la empresa” (
company_name), que el backend exige — toda solicitud fallaba con 400 (Key: 'RegisterRequest.CompanyName' ... failed on the 'required' tag), yRegisterView.vuedescartaba el error real mostrando siempre"Error en el registro. Intenta de nuevo.", ocultando la causa. Ya existía un fix escrito sin commitear en el checkout local; se commiteó, se desplegó a staging, y se confirmó que el registro ahora funciona de punta a punta (Empresa A y B creadas correctamente). Capturas:AUTH-01-formulario-lleno.png(bug),AUTH-01-error-falta-company-name.png(bug),AUTH-01-formulario-lleno-ok.png/AUTH-01-exito.png(corregido).- Nota adicional: el placeholder de contraseña dice “Mínimo 8 caracteres” pero el backend solo exige
min=6— discrepancia menor de mensaje, no bloqueante (ver AUTH-02, pendiente de ejecutar).
CP AUTH-02: Contraseña menor a 6 caracteres
- Objetivo: validar el límite
min=6del backend.- Pasos: registrar con contraseña de 3 caracteres (ej.
"abc").- Resultado esperado: error 400 claro en el formulario, sin enviar la petición si el frontend ya valida esto, o un mensaje legible si la validación es solo de backend.
- Resultado obtenido: ⚠️ Correcto a nivel de backend (400, sin 500), pero con el mismo problema de usabilidad de AUTH-01: el frontend no tiene validación de longitud mínima en el campo (el
type="password"no limita nada), así que el submit llega hasta la API y el usuario solo ve"Error en el registro. Intenta de nuevo."— sin indicar que es por la contraseña. Captura:AUTH-02-password-corto.png.
CP AUTH-03: Email con formato inválido
- Pasos: registrar con
"no-es-un-email".- Resultado esperado: error de validación de formato de email, no un 500.
- Resultado obtenido: ✅ Correcto. El campo usa
type="email"nativo del navegador — el formulario ni siquiera se envía, el navegador bloquea el submit con su propia validación visual. Captura:AUTH-03-email-invalido.png.
CP AUTH-04: Campos obligatorios vacíos (nombre, nombre de empresa)
- Pasos: intentar enviar el formulario dejando “Nombre” o “Nombre de la empresa” vacíos.
- Resultado esperado: el frontend debería bloquear el submit; si se fuerza por API, el backend responde 400 (
binding:"required").- Resultado obtenido: ✅ Correcto. Los campos tienen el atributo
required— el navegador bloquea el submit y enfoca el primer campo vacío, sin llegar a la API. A nivel de API directa (bypaseando el frontend) también responde 400 limpio.
CP AUTH-05: Registrar dos empresas con el mismo email
- Objetivo: el email de login es único globalmente (
auth_users.emailtieneuniqueIndex, no está compuesto concompany_id) — una misma persona no puede ser admin de dos empresas distintas con el mismo correo.- Pasos: registrar la Empresa A → verificar → intentar registrar otra empresa con el mismo email.
- Resultado esperado: el segundo registro debe fallar con un mensaje entendible (“este correo ya está en uso”), no un error 500 crudo de restricción SQL.
- Resultado obtenido: ❌→✅ Bug encontrado y corregido. El segundo intento devolvía 500 con
"ERROR: duplicate key value violates unique constraint \"idx_auth_users_email\" (SQLSTATE 23505)"— SQL crudo expuesto al cliente. Peor aún:Register()creaba laCompanyantes de intentar crear elUser, así que cada intento fallido dejaba una fila decompanieshuérfana (sin ningún usuario) acumulándose en la base — confirmado: encontré y limpié una de estas (id=3, "Empresa Duplicada", soft-delete) generada durante esta misma prueba. Se agregó un chequeo de email existente antes de crear la empresa, y un cleanup de la empresa si elCreatedel usuario falla igual por una carrera de registros simultáneos. Ahora responde409 {"error":"este correo ya está en uso"}y no deja basura. Captura:AUTH-05-email-duplicado-500.png(bug, antes del fix).- Nota de diseño: si en el futuro se quiere permitir que una persona administre varias empresas con el mismo login, este índice único tendría que cambiar — por ahora es una limitación de diseño confirmada, no un bug.
CP AUTH-06: Verificación de cuenta — token válido
- Pasos: usar el link del correo de verificación (o
GET /auth/verify?token=...directo).- Resultado esperado:
is_verifiedpasa atrue; redirección a una pantalla de éxito o al login.- Resultado obtenido: ❌→✅ Bug encontrado y corregido en staging — distinto y más grave que un problema de SMTP. Navegar al link de verificación (
/api/auth/verify?token=...) desde el navegador nunca llegaba al backend (cero entrada en el log del servidor) — el service worker del PWA (vite-plugin-pwa, modogenerateSW) intercepta toda navegación de nivel superior y responde con elindex.htmlcacheado por defecto, sin excepción para rutas de API/backend. Se agregónavigateFallbackDenylist: [/^\/api\//, /^\/auth\//]avite.config.ts, se desplegó, y se confirmó funcionando: la petición real llega al backend (IP real en el log, no::1), responde 302 a/login?verified=true, yis_verifiedpasa atrue. Captura:AUTH-06-verificado.png.- Hallazgo independiente, no corregido (requiere acción del usuario): el SMTP de staging rechaza las credenciales de Gmail configuradas (
535 Username and Password not accepted — BadCredentials). El correo de verificación nunca se envía. Probablemente la cuenta de Gmail necesita una Contraseña de aplicación (si tiene 2FA activado). Mientras no se resuelva, cualquier registro real queda atascado en “revisa tu correo” sin remedio — se verificó manualmente vía elverification_tokenen la base de datos para poder seguir probando.
CP AUTH-07: Verificación con token inválido o ya usado
- Pasos: reutilizar el mismo link de verificación dos veces, o modificar el token.
- Resultado esperado: mensaje claro de “token inválido o expirado”, no un crash.
- Resultado obtenido: ✅ Correcto. Reutilizar un token ya consumido devuelve 400 con
{"error":"enlace de verificación inválido o ya utilizado"}— mensaje claro. Captura:AUTH-07-token-invalido.png.- ⚠️ Nota de usabilidad: este error se devuelve como JSON crudo directamente en la navegación del navegador (no hay página de error amigable) — a diferencia del caso de éxito, que redirige a
/login?verified=true. Si un usuario hace doble clic en el link del correo o lo abre dos veces, vería un JSON plano en blanco en vez de un mensaje entendible. Sugerencia: redirigir también en el caso de error (ej./login?verify_error=true) para que el frontend muestre un mensaje consistente.
Login
CP AUTH-08: Login exitoso
- Precondición: usuario verificado.
- Resultado esperado: recibe token, datos de usuario y bloque de suscripción (
trialrecién registrado).- Resultado obtenido: ✅ Correcto. Login de Empresa A (ya verificada) redirige a
/onboarding(esperado: empresa nueva sin tiendas todavía). Pero ver el hallazgo nuevo (no planeado) más abajo, descubierto en este mismo paso.
CP AUTH-09: Login con contraseña incorrecta
- Resultado esperado: mensaje genérico
"credenciales inválidas"— no debe distinguirse de un email inexistente (protección contra enumeración de usuarios).- Resultado obtenido: ✅ Correcto. Mensaje mostrado:
"Error de autenticación en línea: credenciales inválidas". Captura:AUTH-09-credenciales-invalidas.png.
CP AUTH-10: Login con email que no existe
- Resultado esperado: el mismo mensaje genérico que AUTH-09, con el mismo código HTTP y tiempo de respuesta similar (si responde notablemente más rápido/lento, es una fuga de información por timing — anotar como hallazgo de seguridad menor si se observa).
- Resultado obtenido: ✅ Correcto. Mensaje idéntico a AUTH-09 — sin distinción entre email inexistente y contraseña incorrecta. No se midió timing con precisión instrumentada (la diferencia a simple vista fue imperceptible); si se quiere descartar por completo una fuga por timing, haría falta medir con un script dedicado, no manualmente.
⚠️ Hallazgo no planeado (descubierto en AUTH-08): fuga cross-sesión en la cola de sincronización offline
No es un caso del plan original — se añade aquí por su severidad.
Al iniciar sesión como Empresa A justo después de haber registrado a Empresa B en el mismo navegador, la consola mostró un POST /api/api/v1/users fallido con 400. El cuerpo de la petición era exactamente el payload de registro de Empresa B ({"name":"Admin Empresa B","company_name":"Pizzeria B","email":"admin.b.cp2026@clickstock-test.com","password":"test1234"}), enviado con el token de sesión de Empresa A. Falló únicamente porque el backend espera first_name y el payload trae name — una coincidencia de nombres de campo, no una protección real.
Causa raíz confirmada en el código:
authService.ts→register()llamauserService.addUser(user)con el payload de registro completo.userService.addUserreutilizacreateOfflineService(db.users, 'users').add(...), el mismo mecanismo genérico usado para “Ajustes → Usuarios” — encola el registro como si fuera una creación de usuario del equipo, en una tablasync_queuede IndexedDB sin ninguna relación con la sesión/empresa que lo creó.syncService.tslee el token desessionStorageen el momento de sincronizar, no en el momento de encolar — así que la cola se vacía usando la sesión que esté activa cuando corra el sync, sin importar quién la originó.
Impacto real: si los nombres de campo hubieran coincidido (first_name en vez de name), esto habría creado silenciosamente un usuario fantasma “Admin Empresa B” dentro de la cuenta de Empresa A, usando los datos (incluido el email y password hasheado) de un admin de otra empresa — en un navegador compartido entre dos registros, que es exactamente lo que pasa al ejecutar este mismo plan de pruebas, y plausible en la vida real (ej. alguien que prueba el registro, se equivoca, y vuelve a intentar con otra cuenta en el mismo dispositivo).
❌→✅ Corregido (confirmado en el ciclo 2). authService.register() ya no llama a userService.addUser() — el commit cba3fa1 quitó esas 6 líneas; el admin que se registra se persiste únicamente vía saveFromServer() al hacer login (syncStatus: 'synced'), y nunca vuelve a pasar por la cola de sincronización de “crear usuario de equipo”. No encontrado por mí en este ciclo — ya estaba resuelto antes de retomar las pruebas.
CP AUTH-11: Login antes de verificar el correo
- Resultado esperado: mensaje específico
"cuenta no verificada — revisa tu correo y activa tu cuenta".- Resultado obtenido: ✅ Correcto. Mensaje exacto mostrado:
"Error de autenticación en línea: cuenta no verificada — revisa tu correo y activa tu cuenta". Probado reutilizando una cuenta recién registrada y sin verificar. Captura:AUTH-11-cuenta-no-verificada.png.
Creación de usuarios internos (Ajustes → Usuarios) y roles
CP AUTH-12: Admin crea un grupo/rol nuevo
- Pasos: Ajustes → Roles → editar el rol “cashier” → asignar Ventas: Ver + Crear → Guardar.
- Resultado esperado: el grupo queda con exactamente los permisos marcados.
- Resultado obtenido: ❌→✅ Corregido y reconfirmado en el ciclo 2. Los commits
81356af(siembra el catálogo real de 92 permisos server-side + agrega el middlewareRequirePermission, registrado una sola vez en/api/v1),c934e98(corrige el endpoint de sync degroup_permissionsa/groups/:id/permissions) y1d1008c(encola de verdad las asignaciones de permiso, que antes se escribían en IndexedDB pero nunca entraban async_queue) ya resolvían esto antes de retomar las pruebas — no los encontré yo, los reconfirmé. Verificado de punta a punta: marqué Ventas→Ver y Ventas→Crear para “cashier”, y tras la sincronizacióngroup_permissionsen la base quedó con exactamentesales_readysales_writepara ese grupo. El middleware de permisos también está realmente aplicado — confirmado en AUTH-17 (22 de 23 módulos devuelven 403 a un usuario con solo esos dos permisos). Captura del estado original del bug (ciclo 1, antes del fix):AUTH-12-roles-sin-permisos.png.- Nota de usabilidad: la UI de permisos muestra cada acción (Ver/Crear/Editar/Eliminar) duplicada visualmente dentro de cada módulo — son dos checkboxes por acción en vez de uno (confirmado que no son permisos reales distintos, solo una duplicación del catálogo local de IndexedDB visible en el DOM). No afecta el resultado funcional, pero es confuso de ver.
CP AUTH-13: Admin crea un usuario y le asigna el rol
- Pasos: Ajustes → Usuarios → Nuevo usuario → nombre, apellido, email, contraseña, rol, tienda.
- Resultado esperado: usuario creado, puede iniciar sesión, queda asociado a la tienda y al rol elegidos.
- Resultado obtenido: ❌→✅ Corregido y reconfirmado en el ciclo 2. El mismo commit
c934e98resuelve tambiéngroups[0](ID local) →group(ID de servidor) antes de enviar la creación del usuario. Verificado: creé “Cajero14” con rol “cashier” y tienda “Tienda Roles Test” — tras sincronizar, la base mostrógroup_id=28(el id real del servidor para “cashier”) ystore_id=1, ambos correctos. Capturas del estado original del bug (ciclo 1, antes del fix):AUTH-13-formulario-lleno.png,AUTH-13-listado-sin-rol.png.
CP AUTH-14: Crear usuario sin apellido (campo opcional)
- Resultado esperado: se crea sin problema —
last_nameno es obligatorio en el backend.- Resultado obtenido: ⚠️ El backend no lo exige, pero el frontend sí. El campo “Apellido” tiene el atributo
requiredenUserForm.vue— el navegador bloquea el submit con su validación nativa (mismo patrón que AUTH-04), nunca llega a probarse el caso “backend permite apellido vacío”. Es una discrepancia frontend-más-estricto-que-backend, no un bug, pero vale la pena saber que el backend sí lo soportaría si algún otro cliente (ej. API directa) lo necesitara. Captura:AUTH-14-apellido-requerido-frontend.png.
CP AUTH-15: Crear usuario sin asignar rol
- Objetivo: el backend no exige
groupcomo obligatorio en el DTO — confirmar qué pasa en la práctica.- Resultado esperado a verificar: ¿el usuario queda sin grupo (
group_id = NULL) y por tanto sin ningún permiso? ¿El frontend lo permite o lo bloquea?- Resultado obtenido: ⚠️ El frontend lo permite, sin ninguna advertencia. Creé “SinRol15” sin seleccionar ningún rol — se guardó sin error, aparece en el listado con “Rol: —”. Es un usuario fantasma: puede iniciar sesión (si está verificado/activo) pero no tendrá ningún permiso real una vez el middleware de roles evalúe sus rutas (confirmado el comportamiento del middleware en AUTH-17). El formulario debería advertir o exigir un rol antes de guardar, ya que el resultado es un usuario inutilizable sin que el admin lo sepa.
CP AUTH-16: Crear usuario con email duplicado dentro de la misma empresa
- Resultado esperado: error claro — mismo índice único global de email que en AUTH-05.
- Resultado obtenido: ❌→✅ Bug encontrado y corregido en este ciclo. A diferencia de
Register()(arreglado en AUTH-05),people.UserUsecase.CreateUser()no tenía el mismo chequeo — crear un usuario interno con un email ya existente devolvía 500 crudo con el mismo"ERROR: duplicate key value violates unique constraint \"idx_auth_users_email\"..."expuesto al cliente. Se replicó el mismo patrón que AUTH-05 (chequeo previo + catch de la carrera en elCreate), ahora responde409 {"error":"este correo ya está en uso"}. Verificado en los logs del backend: antes del fix, 500; después del fix (con el mismo registro reintentando desde la cola offline), 409 limpio.- Nota de usabilidad aparte: como la app es offline-first, el formulario de creación no valida el duplicado contra la caché local antes de guardar — el registro se crea localmente sin aviso y solo falla (o, antes del fix, crasheaba) al sincronizar, varios segundos después y sin que el admin lo note en el momento. El admin ve “guardado” y solo descubre el problema si revisa el indicador de “N pendiente(s)” de sincronización.
CP AUTH-17: El usuario “Cajero” intenta acceder a una pantalla fuera de su permiso
- Precondición: sesión iniciada como “Cajero14” (rol “cashier”, con exactamente
sales_read+sales_write).- Pasos: intentar entrar a Ajustes → Roles, y a Ventas (que sí tiene permiso).
- Resultado esperado: el frontend debería ocultar o deshabilitar esas opciones; si se accede directo por URL o API, el backend debe rechazar con 403.
- Resultado obtenido: ❌→✅ Hallazgo grave encontrado y corregido durante esta prueba, antes de poder completarla. Al loguear como Cajero14 y navegar a
/dashboard/sales(que sí debería poder ver), la app redirigía a la pantalla de onboarding de empresa nueva (“¿Cómo quieres empezar?”) — bloqueando el dashboard completo para cualquier usuario que no fuera el admin que registró la empresa, sin importar sus permisos reales. Causa raíz:Subscriptionsolo tieneAuthUserID(sincompany_id), así que la suscripción únicamente existe para el usuario que originalmente registró la empresa; cualquier otro miembro del equipo no tiene fila de suscripción propia, el frontend interpreta eso como “necesita onboarding” (needsOnboarding = loaded && !status), y lo redirige — afectando a todo rol no-admin, no solo a roles con pocos permisos. Se corrigió agregando una búsqueda porcompany_id(join a través deauth_users) como fallback cuando el usuario no tiene suscripción propia, sin tocar el esquema ni migrar datos. Verificado: tras el fix, Cajero14 accede a/dashboard/salescon normalidad y ve el banner correcto de “45 días restantes”. Capturas:AUTH-17-cajero-ventas-permitido.png(tras el fix).- Extensión del fix (ciclo 2b, a pedido explícito): el fix anterior solo evitaba el bloqueo indebido — no hacía que una suscripción realmente vencida bloqueara nada. Se agregó la regla de negocio correcta: la suscripción la paga el admin, pero aplica a toda la empresa — si está vencida o cancelada, el resto del equipo no puede iniciar sesión (
Loginresponde 403 con"la suscripción de tu empresa no está activa — contacta al administrador"); el admin sigue pudiendo entrar, porque de lo contrario nunca podría volver a pagar para reactivarla. Verificado marcando la suscripción de prueba comoexpired: Cajero14 bloqueado con el mensaje correcto; el admin entró con normalidad y vio la pantalla real “Tu suscripción ha expirado” con los planes para renovar — confirma que el backend y el frontend ya estaban alineados en este comportamiento, solo faltaba la validación en el backend.- Backend — confirmado correcto: con solo
sales_read/sales_write, 22 de los 23 módulos del catálogo devuelven403en la sincronización en segundo plano (confirmado en la consola del navegador) — solosalespasa. El middlewareRequirePermissionfunciona como se espera.- Frontend — menú lateral inconsistente con los permisos reales: con el rol “cashier”, el menú oculta correctamente la mayoría de secciones, pero “Promociones” queda visible (el cajero no tiene ese permiso — el backend lo confirma con 403) y “Ventas” desaparece del menú (aunque el cajero sí tiene
sales_read/sales_writey la página funciona perfectamente por URL directa). El criterio de qué se oculta en el menú no coincide exactamente con los permisos reales del backend.- Hallazgo de usabilidad — datos obsoletos visibles pese al 403: navegando directo a
/dashboard/roles(ruta no protegida por guard en el frontend), el cajero sí puede ver la lista completa de roles (nombres, descripciones, conteo de permisos) — se lee de la caché local de IndexedDB, que quedó sincronizada desde la última vez que un admin cargó esa pantalla, sin importar que la petición en vivo al backend devuelva 403. Los botones de escritura (“+Nuevo rol”, editar, eliminar) sí están correctamente ocultos. No es una fuga entre empresas (son los roles de su propia empresa), pero sí es lectura de datos que el usuario no debería poder ver según sus permisos. Captura:AUTH-17-cajero-roles-cache-local.png.
CP AUTH-18: Desactivar un usuario (
is_active=false) y verificar que no pueda iniciar sesión
- Pasos: Ajustes → Usuarios → desactivar a “Cajero14” → intentar login con ese usuario.
- Resultado esperado: debería rechazarse el login.
- Resultado obtenido: ❌→✅ Bug encontrado y corregido en este ciclo.
Login()verificabaIsVerifiedpero nuncaIsActive— un usuario desactivado iniciaba sesión con total normalidad y llegaba al dashboard. Se agregó el chequeo: ahora responde401 {"error":"tu cuenta ha sido desactivada — contacta al administrador de tu empresa"}. Verificado end-to-end: antes del fix, login exitoso conis_active=falseconfirmado en la base; después del fix, login rechazado con el mensaje correcto. Captura:AUTH-18-usuario-desactivado.png.
CP AUTH-19: Error típico — confundir mayúsculas/espacios en el email al hacer login
- Pasos: iniciar sesión con el email en mayúsculas (
ADMIN...@...COM) y con un espacio final.- Resultado esperado a verificar: ¿el backend normaliza el email antes de comparar?
- Resultado obtenido: ❌→✅ Mixto, y el caso de mayúsculas se corrigió en este ciclo. El espacio al final ya funcionaba bien (el frontend lo recorta antes de enviar — login exitoso sin cambios). Las mayúsculas sí fallaban:
GetByEmailcomparaba el valor crudo de la columna, así queADMIN@...yadmin@...eran valores distintos para Postgres — login rechazado con “credenciales inválidas” pese a que la cuenta y la contraseña eran correctas. Se corrigió comparandoLOWER(email) = LOWER(?)en el único punto del código que hace esta consulta (usado porLogin, el chequeo de duplicados deRegister, y el depeople.CreateUser— los tres se benefician del mismo fix). Se eligió esta vía — en vez de normalizar y guardar en minúsculas — porque no pude verificar si ya existían cuentas con mayúsculas en producción (el classifier de Claude Code bloqueó la lectura directa a la base de prod, consistente con sesiones anteriores); la comparación insensible a mayúsculas funciona sin importar qué haya guardado ya, sin necesidad de migrar nada. Verificado: login con el email completo en mayúsculas ahora entra con normalidad. Captura:AUTH-19-email-mayusculas-exitoso.png.
Notas de usabilidad y sugerencias
- AUTH-02 / AUTH-14: el frontend no valida varios campos del lado del cliente de forma consistente (longitud mínima de contraseña, mensajes de error específicos) — todo termina en un mensaje genérico
"Error en el registro. Intenta de nuevo."que obliga a adivinar qué falló. - AUTH-07: el error de token de verificación inválido se muestra como JSON crudo en el navegador en vez de redirigir a una pantalla con mensaje amigable, a diferencia del caso de éxito.
- AUTH-12: los checkboxes de permisos aparecen visualmente duplicados por cada acción (Ver/Crear/Editar/Eliminar) dentro de cada módulo — confuso aunque no afecta el resultado.
- AUTH-15: el formulario de creación de usuario no advierte ni exige seleccionar un rol — resulta en usuarios fantasma sin ningún permiso, sin que el admin lo note.
- AUTH-16: la app offline-first no valida duplicados contra su propia caché local antes de guardar — el error solo aparece, en el mejor de los casos, varios segundos después al sincronizar, y el admin tiene que fijarse en el contador de “pendientes” para enterarse.
- AUTH-17: el menú lateral no oculta/muestra exactamente las mismas secciones que el backend protege (ver detalle arriba) — y las páginas sin guard de ruta en el frontend siguen mostrando datos cacheados aunque el backend ya niegue el acceso en vivo.
- General — cola de sincronización: un ítem que falla de forma permanente (ej. el intento duplicado de AUTH-16) reintenta indefinidamente cada vez que hay navegación, sin backoff ni límite visible, y sin notificar al usuario que ese cambio específico nunca se va a aplicar — solo un contador genérico de “N pendiente(s)”.