Handlers y enrutamiento
Cada handler, sus acciones, y cómo Router.bx conecta las URLs con ellos.
En esta página
Handlers y enrutamiento
Mapa de handlers
| Handler | Base | Propósito |
|---|---|---|
AuditLog.bx | BaseSecureHandler | Exploración, exportación y purga del registro de auditoría |
Assets.bx | EventHandler | Transmite los avatares de usuario y el logo de marca |
Auth.bx | EventHandler | Inicio de sesión, registro, invitaciones, restablecimiento de contraseña - todo público |
BaseSecureHandler.bx | RestHandler | Clase base para cada handler de administración |
Dashboard.bx | BaseSecureHandler | La página de aterrizaje autenticada |
Main.bx | EventHandler | Handler de eventos implícitos - consulta Arquitectura |
Permissions.bx | BaseSecureHandler | CRUD de slugs de permisos |
Profile.bx | BaseSecureHandler | Perfil de autoservicio, contraseña, tokens de API, passkeys |
Roles.bx | BaseSecureHandler | CRUD de roles + asignación de usuarios |
Settings.bx | BaseSecureHandler | Registro de ajustes de la aplicación |
Users.bx | BaseSecureHandler | Administración de usuarios |
BaseSecureHandler
Cada handler protegido extiende BaseSecureHandler, cuyo preHandler verifica CSRF en cada solicitud que cambia estado, fuerza el layout Admin, y redirige a profile/passkey-required cuando cbRequirePasskey está activado y el usuario no tiene ninguna. También provee helpers compartidos (getApiResults(), ensureSortDirection(), getPagination()):
component extends="coldbox.system.RestHandler" {
function preHandler( event, rc, prc ){
// ...CSRF verification, deny-by-default...
event.setLayout( "Admin" );
// ...passkey enforcement...
}
}
Construir un nuevo handler protegido comienza de la misma manera siempre:
component extends="BaseSecureHandler" secured {
function index( event, rc, prc ){
prc.pageTitle = "My Page";
event.setView( "myhandler/index" );
}
}
AuditLog
@secured("auditlog:admin,auditlog:read") a nivel de clase; toda acción excepto index es @remote:
index,search,show- explora y filtra el registro de auditoríaexport-@secured("auditlog:admin,auditlog:export"), transmite CSVpurge-@secured("auditlog:admin,auditlog:delete"), elimina entradas anteriores a una fecha de corteclear-@secured("auditlog:admin"), elimina todas las entradas
Assets
Sin anotación @secured a nivel de clase - transmite archivos binarios desde el disco privado assets de cbfs (consulta Base de datos y ORM y app/config/modules/cbfs.bx), el cual se encuentra fuera del webroot y de otro modo es inalcanzable:
avatar-@secured(cualquier usuario autenticado), transmite la variante JPEG de avatarsm/lgde un usuariologo- público, transmite la variante PNG de logo de marcasm/lgpara que la pantalla de inicio de sesión y otras páginas de invitado puedan renderizarlo
Ambas acciones devuelven 404 (en lugar de un error) ante una forma no reconocida de userId/size o cuando el archivo solicitado simplemente no existe, de modo que un llamante no pueda distinguir "sin avatar" de "usuario inexistente" solo por la forma de la respuesta. El redimensionado, recorte, y almacenamiento pasan por ImageService (app/models/system/ImageService.bx), invocado vía getInstance() dentro de cada acción en lugar de una propiedad @inject - consulta el docblock en Assets.bx para saber por qué (una peculiaridad del orden de arranque de WireBox con la construcción de singleton disparada por handler).
Auth
Sin anotación @secured - estas acciones deben permanecer accesibles para invitados:
login/doLogin(GET/POST) - verificado por CSRF, llama asecurityService.login(), admiterememberMeregister/doRegister- controlado por el ajustecbAllowRegistrationcheckEmailAvailability- endpoint JSON para comprobaciones de disponibilidad de correo electrónico en vivoverifyRegistration- consume un token de acciónPURPOSE_REGISTRATIONactivateInvitation/doActivateInvitation- establece una contraseña para un usuario invitado, creado por un administradorforgotPassword/doForgotPassword- controlado porcbAllowForgotPasswordresetPassword/doResetPassword- valida el token de restablecimiento, establece una nueva contraseñaverifyEmailChange- consume un token de acciónPURPOSE_EMAIL_CHANGElogout- llama asecurityService.logout()
preHandler redirige a un visitante ya autenticado directamente al panel de control, y establece el layout a partir de prc.settings.cbLoginLayout (AuthSplit por defecto - consulta guides/security.md); verifyEmailChange y logout están exentos de esa redirección para que permanezcan accesibles esté o no el visitante ya autenticado.
Dashboard
@secured (cualquier usuario autenticado, sin requerir un permiso específico):
index- la página de inicio del panel de controlnotAuthorized- el objetivo deinvalidAuthorizationEvent, mostrado cuando a un usuario autenticado le falta un permiso requerido
Permissions
@secured("permissions:admin,permissions:read") a nivel de clase:
indexcreate-@secured("permissions:admin,permissions:write")update/delete-@remote, mismos permisos de escritura/eliminación
Profile
Acciones de autoservicio @secured para el usuario actual, todas endpoints AJAX @remote excepto index:
index,passkeyRequiredsave,doPasswordChangerequestEmailChange/cancelEmailChange- inicia/cancela un cambio de correo electrónico pendiente, confirmado a través deAuth.verifyEmailChangelistTokens/createToken/updateToken/deleteToken- tokens de APIlistPasskeys/updatePasskey/deletePasskeyuploadAvatar/deleteAvatar- acepta la imagen como un URI de datos en base64 enrc.avatar(BoxLang no tiene un analizador multipart/form-data, así que las subidas viajan como JSON), decodificado víaBaseSecureHandler.decodeDataUri(); transmitido de vuelta porAssets.avatar
Cada una de estas es verificada por CSRF por BaseSecureHandler a menos que se alcance a través de un método HTTP seguro - consulta Verificación de CSRF.
Roles
@secured("roles:admin,roles:read") a nivel de clase; toda acción excepto index es @remote:
indexcreate/update/delete-@secured("roles:admin,roles:write"/"...:delete")users/availableUsers- lista usuarios asignados/disponibles para un roladdUser/removeUser-@secured("roles:admin")
Settings
@secured("settings:admin,settings:read") a nivel de clase:
indexregistry/registrySearch- registro de ajustes paginadocreateRegistry/updateRegistry/toggleRegistryStatus/deleteRegistry-settings:admin,settings:writesave- guardado masivo de ajustes centralesuploadLogo/deleteLogo-settings:admin,settings:write, misma convención de URI de datos en base64 queProfile.uploadAvatar; almacena/restaura el ajustecbAppLogoy transmite de vuelta víaAssets.logo- Utilidades de administración (todas
settings:admin):clearTemplateCache,clearSessionsCache,revokeRememberTokens,flushSettingsCache
Users
@secured("users:admin,users:read") a nivel de clase:
index,searchcreate/update/delete/resendInvitation-users:admin,users:write/...:deleteshow-users:read- Solo administrador (
users:admin):updateProfile,setStatus,resetPassword,verify,revokeRememberTokens,addRole/removeRole,addPermission/removePermission,savePreferences,revokeToken/revokeAllTokens
ensureNotSelf() protege varias de estas para impedir que un administrador se degrade o se elimine sus propios roles.

Verificación de CSRF
app/config/modules/cbsecurity.bx establece csrf.enableAutoVerifier: false, así que no hay interceptor global. En su lugar, BaseSecureHandler.preHandler() verifica CSRF con denegación por defecto para cada handler que lo extiende:
static {
// The safe methods of RFC 9110, exempt from CSRF verification below.
SAFE_HTTP_METHODS = "GET,HEAD,OPTIONS"
}
function preHandler( event, rc, prc ) {
if (
!static.SAFE_HTTP_METHODS.listFindNoCase( event.getHTTPMethod() )
&& !csrfVerify( rc.csrf ?: "" )
) {
return onInvalidCSRF( argumentCollection = arguments )
}
// ...
}
Qué significa esto cuando extiendes un handler protegido:
- No te suscribes voluntariamente. Cualquier acción alcanzada vía
POST,PUT,PATCH, oDELETEdebe llevar unrc.csrfválido, desde el día en que la agregas. No hay una lista por handler que recordar actualizar. - Los métodos seguros están exentos.
GET,HEAD, yOPTIONSno deben cambiar el estado, así que no conllevan riesgo de CSRF, yOPTIONS(preflight de CORS) no puede llevar un token en absoluto. Si un método seguro en tu código sí cambia el estado, ese es el error a corregir. onInvalidCSRF()es sobrescribible. La implementación base aborta con un fallo de autorización, que es lo que quieren los endpoints JSON/AJAX - cada mutación enPermissionsahora es una de esas, enviada a través defetchWithCsrf()(consulta Frontend), que se recupera de un token caducado sin necesitar una redirección.Settingsaún la sobrescribe para mostrar un mensaje flash y redirigir sus envíos de formulario nativo, así que un formulario del navegador obtiene una página en lugar de un 403 sin adornos. Sobrescríbela en tu propio handler cuando renderice HTML en lugar de JSON.
Auth extiende coldbox.system.EventHandler, no BaseSecureHandler, porque sus acciones se ejecutan para visitantes no autenticados y por lo tanto no pueden heredar la comprobación anterior. Cada acción que cambia el estado verifica su propio token: doLogin, doRegister, doActivateInvitation, doForgotPassword, doResetPassword, y logout.
Mapa de rutas (app/config/Router.bx)
Todas las rutas se declaran en una sola función configure():
route( "/healthcheck" ).to( () => "Ok!" );
get( "dashboard" ).to( "Dashboard.index" );
resources( "permissions", parameterName = "permissionId" );
route( "roles/:roleId/available-users" ).to( "Roles.availableUsers" );
route( "roles/:roleId/users" ).toAction( { POST: "addUser" } );
route( "roles/:roleId/users/:userId" ).toAction( { DELETE: "removeUser" } );
resources( "roles", parameterName = "roleId" );
resources( "users", parameterName = "userId" );
route( "profile" ).toAction( { GET: "index", POST: "save" } );
// @app_routes@ ← insertion point for module/scaffold-generated routes
route( ":handler/:action?" ).end(); // conventions-based catch-all
Consulta Referencia: Mapa de rutas para la tabla completa de cada método, URL, acción objetivo, y permiso requerido.