Frontend

Vistas BXM renderizadas en el servidor, pequeños componentes de Alpine.js, y un pipeline SCSS/JS compilado con Vite.

En esta página

Frontend

Cómo encaja todo

El frontend es una aplicación híbrida, renderizada en el servidor + Alpine.js - sin SPA, sin enrutador del lado del cliente:

1
Los layouts de ColdBox proporcionan la estructura

Admin.bxm, AuthSplit.bxm, y afines en app/layouts/ renderizan el marco HTML.

2
Las plantillas BXM se renderizan en el servidor

Las vistas en app/views/ se renderizan con los datos de rc/prc ya resueltos por el handler.

3
Alpine.js añade interactividad

Pequeños componentes x-data manejan formularios, modales, cajones y interruptores - sin necesidad de un paso de compilación por componente.

4
Vite compila los assets

SCSS + JS de resources/assets/ se compilan en public/includes/, servidos con el prefijo ASSET_URL.

Arquitectura de Alpine.js

App.js (entry)
  ├── Registers all Alpine stores + components
  ├── Imports Bootstrap JS + Phosphor icons + Tippy.js
  │
  ├── Stores ($store.*)
  │   ├── theme.js     → dark/light mode, syncs data-bs-theme + localStorage
  │   └── sidebar.js   → collapse/open, mobile overlay, localStorage persistence
  │
  └── Components (x-data)
      ├── auth/        → AuthForm, RegisterForm, ForgotPasswordForm, PasswordResetForm
    ├── security/     → AuditLogForm, PermissionsForm, RolesForm, UserDetailForm, UsersForm
    ├── profile/      → PasskeyOnboarding, PreferencesForm, ProfileForm
    ├── settings/     → SettingsForm, SettingsRegistryForm
    └── ui/           → Drawer, GlobalProgress, GlobalToast, Logo, MessageBox, PasswordMeter, PasswordStrength, Switch

Cada componente es un módulo independiente que devuelve un objeto x-data de Alpine:

export default () => ( {
    visible: true,
    init() {
        setTimeout( () => this.visible = false, 5000 );
    }
} );
<div x-data="messageBox" x-show="visible" x-transition>
    <!-- alert content -->
</div>

Estructura SCSS

app.scss
  ├── _variables.scss   Bootstrap variable overrides
  ├── bootstrap          Full Bootstrap 5.3 import
  ├── _base.scss         CSS custom properties (light/dark theme)
  ├── components/        9 component partials
  ├── layouts/            Admin + Auth layout partials
  └── views/              Page-specific styles

Configuración de Vite

vite.config.mjs usa el plugin coldbox() de coldbox-vite-plugin:

  • Puntos de entrada: resources/assets/scss/app.scss y resources/assets/js/App.js
  • refresh: appRefreshPaths — recarga completa automática en cambios de handler/vista
  • publicDirectory: "public/includes" — dónde aterrizan los assets compilados
  • Preprocesador SCSS con marcas silenceDeprecations para las versiones más nuevas de Dart Sass (import, global-builtin, color-functions, if-function)
npm run dev        # Vite dev server with HMR
npm run build      # Production build → public/includes/
npm run lint       # ESLint check on resources/assets/js
npm run lint:fix   # ESLint auto-fix
npm run lint:scss  # Stylelint on resources/assets/scss
ASSET_URL

En producción, las URLs de los assets compilados llevan el prefijo de la variable de entorno ASSET_URL (.env.example la predetermina a /includes) - consulta Configuración.

Componentes de vista renderizados en el servidor

Estos parciales BXM viven bajo app/views/_components/ y se renderizan con el helper view() de ColdBox. Están intencionalmente enfocados en presentación: pasa valores a través del struct args y mantén la lógica de negocio en handlers o servicios.

Estructura de la aplicación

ParcialPropósito y entradas
_components/app/includesMetadatos del documento, prevención de FOUC de tema/barra lateral, script de passkey, y CSS/JS de Vite. title opcional. Inclúyelo una vez en <head>.
_components/app/sidebarNavegación de administración, enlaces de Usuarios/Roles/Permisos/Registro de Auditoría conscientes de permisos, submenú de configuración, y pie de la barra lateral. Lee prc.authUser; inclúyelo desde Admin.bxm.
_components/app/sidebar-brandEnlace del logo/nombre de la aplicación usado por la barra lateral.
_components/app/sidebar-footerResumen del usuario autenticado y acciones de perfil/cierre de sesión usadas por la barra lateral.
_components/app/topbarAlternador de la barra lateral, alternador de tema, migas de pan, menú de usuario, y acción de cierre de sesión. Lee prc.authUser y prc.title.
_components/app/topbar-breadcrumbsMiga de pan del panel de control renderizada dentro de la barra superior. Extiéndela al agregar navegación más profunda.
_components/app/topbar-notificationsRanura/componente de notificaciones de la barra superior para notificaciones de la aplicación.
_components/app/footerCopyright y enlaces de pie de página. classes opcional. Lee prc.settings.cbCopyrightNotice.

Parciales de autenticación

ParcialPropósito y entradas
_components/auth/footerPie de página usado por los layouts de autenticación.
_components/auth/passwordInputCampo de contraseña reutilizable con alternador de visibilidad y afordancias de fortaleza de contraseña.

Parciales de interfaz

ParcialPropósito y entradas
_components/ui/modalDiálogo genérico de Alpine que renderiza una vista anidada opcional. El id requerido debe ser único; admite title, openExpression, closeExpression, contentView, y contentArgs.
_components/ui/drawerDiálogo con trampa de foco lateral derecho, con cierre por fondo/Escape y contentView/contentArgs opcionales; también inicializa drawer().
_components/ui/confirmDiálogo de confirmación con mensaje estático o vinculado a Alpine, expresiones de confirmar/cancelar, etiquetas, ícono, clase de botón, y expresión de deshabilitado.
_components/ui/messageboxAlerta descartable de información/éxito/advertencia/error. Admite message/title estáticos o messageExpression/typeExpression/dismissAction dinámicos, más autoDismiss y classes.
_components/ui/globalProgressBarra de progreso global accesible. Inclúyela una vez por layout; controlada por $progress.start()`, `$progress.set(), y $progress.stop().
_components/ui/globalToastPila global de notificaciones toast. Inclúyela una vez por layout; acepta duration, position, y maxVisible, y recibe notificaciones de $toast().
_components/ui/avatarRenderiza la imagen de avatar de un usuario cuando hasAvatar es verdadero, recurriendo a initials en caso contrario. Visualización de solo lectura usada por la barra lateral, la barra superior, el listado de Usuarios, y la página de detalle de Usuarios — consulta Avatares y logo de marca.
_components/ui/logoParcial reutilizable de logo/marca de la aplicación.
_components/ui/passwordMeterMedidor de política de contraseñas usado junto a los campos de contraseña.
_components/ui/progressbarParcial de barra de progreso en línea para un valor numérico local.
_components/ui/switchParcial de control de interruptor accesible para ajustes booleanos.

Componentes y stores de Alpine

resources/assets/js/App.js registra los siguientes nombres globalmente con Alpine. Úsalos como x-data="name" o x-data="name(...)" en las vistas BXM. Los componentes de formulario hacen solicitudes remotas a las rutas de handler correspondientes y esperan el token CSRF suministrado por su vista, enviado a través de fetchWithCsrf() (consulta CSRF en solicitudes que modifican datos).

Estructura de la aplicación y autenticación

Nombre de AlpineFuenteResponsabilidad
adminBodycomponents/app/AdminBody.jsComportamiento de la estructura de la página de administración y eventos globales de layout.
sidebarBrandcomponents/app/SidebarBrand.jsInteracciones de la marca en la barra lateral.
footercomponents/app/Footer.jsEstado del pie de página y comportamiento del año actual.
authFormcomponents/auth/AuthForm.jsEnvío de inicio de sesión, validación, recordarme, y errores.
registerFormcomponents/auth/RegisterForm.jsValidación de registro, disponibilidad de correo electrónico, y envío.
forgotPasswordFormcomponents/auth/ForgotPasswordForm.jsEstado y retroalimentación de la solicitud de contraseña olvidada.
passwordResetFormcomponents/auth/PasswordResetForm.jsEnvío y validación del token de restablecimiento de contraseña.

Formularios de administración y perfil

Nombre de AlpineFuenteResponsabilidad
usersFormcomponents/security/UsersForm.jsListado de usuarios, búsqueda, paginación, invitación, estado, y acciones de administración.
userDetailFormcomponents/security/UserDetailForm.jsPerfil de usuario, rol, permiso, preferencia, token, y acciones de verificación.
rolesFormcomponents/security/RolesForm.jsCRUD de roles y asignación/eliminación de usuarios y permisos.
permissionsFormcomponents/security/PermissionsForm.jsListado de permisos y operaciones CRUD.
auditLogFormcomponents/security/AuditLogForm.jsFiltrado de auditoría, paginación, cajón de detalle, exportación CSV, purga, y acciones de limpieza.
settingsFormcomponents/settings/SettingsForm.jsEdición de ajustes centrales de la aplicación y retroalimentación relacionada con la caché.
logoUploadercomponents/settings/LogoUploader.jsSubida/eliminación del logo de marca para el campo "App Logo Path", junto a su entrada de URL manual existente y vista previa en vivo — consulta Avatares y logo de marca.
settingsRegistryFormcomponents/settings/SettingsRegistryForm.jsBúsqueda de registro, paginación, creación/actualización, habilitar/deshabilitar, y acciones de eliminación.
profileFormcomponents/profile/ProfileForm.jsCampos de perfil, política de contraseñas, gestión de tokens de API, el subformulario de solicitud/cancelación de cambio de correo electrónico, y subida/eliminación de avatar.
preferencesFormcomponents/profile/PreferencesForm.jsPersistencia de preferencias de usuario.
passkeyOnboardingcomponents/profile/PasskeyOnboarding.jsRegistro de passkey e incorporación de passkey requerida.

Componentes de interfaz y APIs globales

Nombre de AlpineFuenteResponsabilidad
messageBoxcomponents/ui/MessageBox.jsVisibilidad de alertas y descarte temporizado opcional.
passwordMetercomponents/ui/PasswordMeter.jsVisualización de requisitos y fortaleza de contraseña.
passwordStrengthcomponents/ui/PasswordStrength.jsCálculo de fortaleza de contraseña y etiquetas.
switchComponentcomponents/ui/Switch.jsEstado de alternancia y manejo de cambios.
drawercomponents/ui/Drawer.jsCiclo de vida del cajón y comportamiento de foco.
globalProgresscomponents/ui/GlobalProgress.jsEventos de progreso y valor de progreso actual.
globalToastcomponents/ui/GlobalToast.jsCola de toasts, descarte, mapeo de tipos, y límites de la pila.

El código fuente también contiene Header.js, Sidebar.js, TopBarNotifications.js, y Logo.js. Sus exportaciones están disponibles para importaciones locales, pero actualmente no están registradas por App.js; regístralas con Alpine.data() antes de usarlas como componentes globales x-data.

Stores, utilidades, y propiedades mágicas

APIFuenteUso
$store.themestores/theme.jsModo claro/oscuro, data-bs-theme, y persistencia en localStorage.
$store.sidebarstores/sidebar.jsColapso en escritorio, apertura/cierre en móvil, y persistencia en localStorage.
$formatDate`, `$formatDateTime, $relativeDateutils/dateFormat.jsVisualización de fecha consistente con valores de respaldo.
$countLabelutils/countLabel.jsEtiquetas de conteo singular/plural.
$sortClass`, `$sortIconutils/sort.jsEncabezados de tabla ordenables e indicadores.
$passwordMeetsPolicyutils/passwordPolicy.jsVerifica los requisitos de contraseña configurados.
$isEmailApp.jsVerificación ligera de formato de correo electrónico.
$toast` / `$progresscomponents/ui/GlobalToast.js, GlobalProgress.jsAPIs globales de notificación y progreso.
$focus` / `$copyApp.jsEnfoca un descendiente después de que Alpine se actualiza; copia texto a través de la API del portapapeles del navegador.
createRemoteListing()utils/listing.jsEstado compartido de listado remoto, carga, paginación, y manejo de errores.
fetchWithCsrf(), refreshCsrfToken()utils/csrf.jsEnvía una solicitud que modifica datos con el token CSRF del componente, recuperándose una vez de un token caducado.

AlpinePlugins.js instala Collapse, Focus, Mask, y Persist. passkeys.js provee la integración WebAuthn del lado del navegador. Mantén documentadas aquí las nuevas APIs reutilizables del navegador y añade su registro/importación a App.js cuando sean globales.

CSRF en solicitudes que modifican datos

Cada acción de componente que envía una solicitud no-GET pasa por fetchWithCsrf() (utils/csrf.js) en lugar de llamar a fetch() directamente. Este es el único lugar encapsulado donde se construyen las solicitudes que modifican datos, de modo que el comportamiento de recuperación de tokens -y cualquier cosa que se le añada después (hooks de solicitud/respuesta, encabezados globales, telemetría)- solo tiene que cambiar aquí en lugar de en cada componente que resulte modificar el estado.

Por qué tiene que recuperarse en absoluto. El csrfToken de un componente se incrusta una sola vez, cuando se renderiza su vista. El servidor puede invalidarlo mientras la página sigue abierta, de dos maneras que la propia documentación de cbcsrf señala: csrfField() (el mixin detrás de cada input oculto csrf) fuerza la rotación del token de la sesión en su primer uso por solicitud, así que cualquier página que lo renderice -Configuración, la página de passkey requerido, las páginas de autenticación- invalida silenciosamente el token que está en cada otra pestaña abierta; y un token expira un tiempo fijo después de que fue creado, no después de que la página se cargó, así que una página renderizada tarde en la vida de un token puede recibir uno con solo segundos restantes. De cualquier manera, el token incrustado de un componente puede caducar antes de que el usuario termine de escribir.

El contrato:

export async function fetchWithCsrf( component, url, method, buildRequest ) { /* ... */ }
export async function refreshCsrfToken( component ) { /* ... */ }
  • component es la instancia del componente Alpine (pasa this). Debe exponer una propiedad csrfToken mutable - fetchWithCsrf() la lee para construir la solicitud y, en un reintento por token caducado, la sobrescribe con el token actual de la sesión vía refreshCsrfToken().
  • buildRequest( csrfToken ) devuelve los campos de RequestInit específicos del método (headers, body, credentials, etc.) para el token dado. Se vuelve a llamar en el reintento, así que debe construir el cuerpo de nuevo cada vez en lugar de capturar un valor calculado una sola vez - esto es lo que permite que el mismo helper cubra por igual cuerpos URLSearchParams, JSON.stringify(), y FormData.
  • Ante un 403, fetchWithCsrf() llama a refreshCsrfToken() y, si obtuvo un token genuinamente nuevo, repite la solicitud una vez con buildRequest() llamado de nuevo. Un segundo 403 (por ejemplo, un fallo de autorización real, o una sesión que ha expirado por completo) se devuelve tal cual - los llamantes aún necesitan su manejo de errores normal para ese caso.
const response = await fetchWithCsrf( this, "/permissions", "POST", ( csrf ) => ( {
	headers : { "Content-Type": "application/x-www-form-urlencoded" },
	body    : new URLSearchParams( { permission: this.form.permission, csrf } ),
} ) );
const response = await fetchWithCsrf( this, form.action, "POST", ( csrf ) => {
	const formData = new FormData( form );
	formData.set( "csrf", csrf );
	return { body: formData, credentials: "same-origin", headers: { Accept: "application/json" } };
} );

Solo las lecturas GET/HEAD se saltan fetchWithCsrf() y llaman a fetch() directamente - no llevan token CSRF y no pueden devolver 403 por ese motivo. Un puñado de endpoints del módulo cbSecurity (las rutas de la ceremonia WebAuthn de passkey) también se llaman con fetch() simple: se autentican a través de la propia ceremonia WebAuthn, no con el token CSRF de esta aplicación, así que quedan fuera del alcance de este helper. Toda otra mutación en resources/assets/js/components/ pasa por fetchWithCsrf(); mantén los nuevos componentes de formulario consistentes con eso cuando agreguen una solicitud que cambie el estado del servidor.

Avatares y logo de marca

Los avatares de usuario y el logo de marca de la aplicación se almacenan en el disco privado assets de cbfs (consulta Configuración) y son transmitidos por Assets.bx (consulta Handlers y enrutamiento) en lugar de servirse como archivos estáticos.

The Profile page, showing the avatar upload and assigned role
La página de Perfil, mostrando la subida de avatar y el rol asignado.
  • La visualización pasa por el parcial _components/ui/avatar: renderiza <img src="/avatars/:userId/:size"> cuando hasAvatar es verdadero, y recurre a un <span> de iniciales en caso contrario. Está conectado a la barra lateral, la barra superior, y la tabla de listado de Usuarios (campo hasAvatar proyectado por el servidor), y en línea en la página de detalle de Usuarios (alternando x-show/x-cloak sobre user.hasAvatar, ya que el avatar de esa página se encuentra dentro de una tarjeta de resumen manejada por Alpine en lugar de un parcial estático).
  • La subida/eliminación del propio avatar del usuario actual vive en la página de Perfil, gestionada por profileForm (ProfileForm.js): una entrada de archivo oculta lee la imagen seleccionada como un URI de datos en base64 (readFileAsDataUrl()) y lo publica en POST /profile/avatar; DELETE /profile/avatar lo elimina. Ambos incrementan un contador version usado como parámetro de consulta anti-caché en la URL transmitida, ya que la ruta del archivo en sí no cambia entre subidas.
  • El logo de marca recibe el mismo tratamiento de subida/eliminación en la página de Configuración, a través del componente logoUploader (LogoUploader.js) contra POST/DELETE /settings/logo. Reemplaza el valor de la entrada de texto del ajuste cbAppLogo con la ruta transmitida (/branding/logo/lg) al subir, y restaura el valor predeterminado configurado al eliminar - la entrada de texto de URL manual y la vista previa en vivo de <img> siguen funcionando exactamente igual para quien quiera apuntar cbAppLogo a una URL externa en su lugar.
  • Ambos endpoints de subida aceptan las mismas formas: las imágenes se decodifican del lado del servidor con BaseSecureHandler.decodeDataUri(), y luego se redimensionan/recortan en variantes JPEG (avatar) sm/lg o PNG (logo) mediante ImageService (app/models/system/ImageService.bx).
Editar esta página Descargar Markdown Última actualización Oct 1, 2026, 11:06:51 AM