Frontend

Serverseitig gerenderte BXM-Views, kleine Alpine.js-Komponenten und eine mit Vite kompilierte SCSS/JS-Pipeline.

Auf dieser Seite

Frontend

Wie es zusammenpasst

Das Frontend ist eine hybride, serverseitig gerenderte + Alpine.js-Anwendung - keine SPA, kein clientseitiger Router:

1
ColdBox-Layouts liefern das Gerüst

Admin.bxm, AuthSplit.bxm und ähnliche in app/layouts/ rendern den HTML-Rahmen.

2
BXM-Templates rendern serverseitig

Views in app/views/ rendern mit rc/prc-Daten, die der Handler bereits aufgelöst hat.

3
Alpine.js fügt Interaktivität hinzu

Kleine x-data-Komponenten übernehmen Formulare, Modals, Drawer und Umschalter - kein Build-Schritt pro Komponente nötig.

4
Vite kompiliert die Assets

SCSS + JS aus resources/assets/ werden zu public/includes/ kompiliert, ausgeliefert unter dem ASSET_URL-Präfix.

Alpine.js-Architektur

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

Jede Komponente ist ein eigenständiges Modul, das ein Alpine-x-data-Objekt zurückgibt:

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

SCSS-Struktur

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

Vite-Konfiguration

vite.config.mjs verwendet coldbox-vite-plugins coldbox()-Plugin:

  • Einstiegspunkte: resources/assets/scss/app.scss und resources/assets/js/App.js
  • refresh: appRefreshPaths — automatischer vollständiger Reload bei Handler-/View-Ă„nderungen
  • publicDirectory: "public/includes" — wohin gebaute Assets gelangen
  • SCSS-Präprozessor mit silenceDeprecations-Flags fĂĽr neueres 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

In der Produktion werden kompilierte Asset-URLs mit der Umgebungsvariable ASSET_URL vorangestellt (.env.example setzt standardmäßig /includes) - siehe Konfiguration.

Serverseitig gerenderte View-Komponenten

Diese BXM-Partials liegen unter app/views/_components/ und werden mit ColdBoxs view()-Helfer gerendert. Sie sind bewusst präsentationsfokussiert: Werte über den args-Struct durchreichen und Geschäftslogik in Handlern oder Services belassen.

AnwendungsgerĂĽst

PartialZweck und Eingaben
_components/app/includesDokument-Metadaten, Theme-/Sidebar-FOUC-Vermeidung, Passkey-Skript und Vite-CSS/JS. Optionales title. Einmal im <head> einbinden.
_components/app/sidebarAdmin-Navigation, berechtigungsbewusste Links zu Users/Roles/Permissions/Audit Log, Einstellungs-UntermenĂĽ und Sidebar-Footer. Liest prc.authUser; wird aus Admin.bxm eingebunden.
_components/app/sidebar-brandAnwendungslogo-/Namens-Link, verwendet von der Sidebar.
_components/app/sidebar-footerZusammenfassung des authentifizierten Benutzers sowie Profil-/Abmelde-Aktionen, verwendet von der Sidebar.
_components/app/topbarSidebar-Umschalter, Theme-Umschalter, Breadcrumbs, BenutzermenĂĽ und Abmelde-Aktion. Liest prc.authUser und prc.title.
_components/app/topbar-breadcrumbsDashboard-Breadcrumb, gerendert innerhalb der Topbar. Erweitern beim HinzufĂĽgen tieferer Navigation.
_components/app/topbar-notificationsTopbar-Benachrichtigungsslot/-Komponente fĂĽr Anwendungsbenachrichtigungen.
_components/app/footerCopyright und Footer-Links. Optionale classes. Liest prc.settings.cbCopyrightNotice.

Authentifizierungs-Partials

PartialZweck und Eingaben
_components/auth/footerFooter, verwendet von den Authentifizierungs-Layouts.
_components/auth/passwordInputWiederverwendbares Passwortfeld mit Sichtbarkeits-Umschalter und Passwortstärke-Hinweisen.

UI-Partials

PartialZweck und Eingaben
_components/ui/modalGenerischer Alpine-Dialog, der optional eine verschachtelte View rendert. Erforderliches id sollte eindeutig sein; unterstĂĽtzt title, openExpression, closeExpression, contentView und contentArgs.
_components/ui/drawerRechtsseitiger, fokusgesperrter Dialog mit Backdrop-/Escape-SchlieĂźen und optionalem contentView/contentArgs; initialisiert auĂźerdem drawer().
_components/ui/confirmBestätigungsdialog mit statischer oder Alpine-gebundener Nachricht, Bestätigen-/Abbrechen-Ausdrücken, Beschriftungen, Icon, Button-Klasse und Disabled-Ausdruck.
_components/ui/messageboxSchlieĂźbarer Info-/Erfolgs-/Warn-/Fehler-Hinweis. UnterstĂĽtzt statische message/title oder dynamische messageExpression/typeExpression/dismissAction, plus autoDismiss und classes.
_components/ui/globalProgressGlobal zugänglicher Fortschrittsbalken. Einmal pro Layout einbinden; gesteuert über $progress.start()`, `$progress.set() und $progress.stop().
_components/ui/globalToastGlobaler Toast-Stapel. Einmal pro Layout einbinden; akzeptiert duration, position und maxVisible, und erhält Benachrichtigungen von $toast().
_components/ui/avatarRendert das Avatarbild eines Benutzers, wenn hasAvatar wahr ist, andernfalls einen Fallback auf initials. Reine Anzeige-Komponente, verwendet von Sidebar, Topbar, Users-Auflistung und Users-Detailseite — siehe Avatare & Branding-Logo.
_components/ui/logoWiederverwendbares Anwendungslogo-/Branding-Partial.
_components/ui/passwordMeterPasswortrichtlinien-Messgerät neben Passwortfeldern.
_components/ui/progressbarInline-Fortschrittsbalken-Partial fĂĽr einen lokalen numerischen Wert.
_components/ui/switchBarrierefreies Switch-Steuerelement-Partial fĂĽr boolesche Einstellungen.

Alpine-Komponenten und Stores

resources/assets/js/App.js registriert die folgenden Namen global bei Alpine. Verwende sie als x-data="name" oder x-data="name(...)" in BXM-Views. Formular-Komponenten senden Remote-Requests an die passenden Handler-Routen und erwarten das CSRF-Token, das von ihrer View geliefert wird, gesendet ĂĽber fetchWithCsrf() (siehe CSRF bei mutierenden Requests).

AnwendungsgerĂĽst und Authentifizierung

Alpine-NameQuelleVerantwortung
adminBodycomponents/app/AdminBody.jsVerhalten des Admin-SeitengerĂĽsts und globale Layout-Events.
sidebarBrandcomponents/app/SidebarBrand.jsInteraktionen der Sidebar-Marke.
footercomponents/app/Footer.jsFooter-Status und aktuelles-Jahr-Verhalten.
authFormcomponents/auth/AuthForm.jsLogin-Ăśbermittlung, Validierung, Remember-me und Fehler.
registerFormcomponents/auth/RegisterForm.jsRegistrierungsvalidierung, E-Mail-VerfĂĽgbarkeit und Ăśbermittlung.
forgotPasswordFormcomponents/auth/ForgotPasswordForm.jsStatus und Feedback fĂĽr die Passwort-vergessen-Anfrage.
passwordResetFormcomponents/auth/PasswordResetForm.jsĂśbermittlung und Validierung des Passwort-Reset-Tokens.

Admin- und Profil-Formulare

Alpine-NameQuelleVerantwortung
usersFormcomponents/security/UsersForm.jsBenutzerauflistung, Suche, Pagination, Einladung, Status und Admin-Aktionen.
userDetailFormcomponents/security/UserDetailForm.jsBenutzerprofil, Rolle, Berechtigung, Präferenz, Token und Verifizierungsaktionen.
rolesFormcomponents/security/RolesForm.jsRollen-CRUD und Zuweisen/Entfernen von Benutzern und Berechtigungen.
permissionsFormcomponents/security/PermissionsForm.jsBerechtigungsauflistung und CRUD-Operationen.
auditLogFormcomponents/security/AuditLogForm.jsAudit-Filterung, Pagination, Detail-Drawer, CSV-Export, Bereinigung und Löschaktionen.
settingsFormcomponents/settings/SettingsForm.jsBearbeitung der zentralen Anwendungseinstellungen und Cache-bezogenes Feedback.
logoUploadercomponents/settings/LogoUploader.jsUpload/Entfernen des Branding-Logos für das Feld "App Logo Path", neben dessen bestehendem manuellen URL-Eingabefeld und Live-Vorschau — siehe Avatare & Branding-Logo.
settingsRegistryFormcomponents/settings/SettingsRegistryForm.jsRegistry-Suche, Pagination, Erstellen/Aktualisieren, Aktivieren/Deaktivieren und Löschaktionen.
profileFormcomponents/profile/ProfileForm.jsProfilfelder, Passwortrichtlinie, API-Token-Verwaltung, das Unterformular für Anfrage/Abbruch der E-Mail-Änderung sowie Avatar-Upload/-Entfernen.
preferencesFormcomponents/profile/PreferencesForm.jsPersistierung der Benutzerpräferenzen.
passkeyOnboardingcomponents/profile/PasskeyOnboarding.jsPasskey-Registrierung und erforderliches Passkey-Onboarding.

UI-Komponenten und globale APIs

Alpine-NameQuelleVerantwortung
messageBoxcomponents/ui/MessageBox.jsSichtbarkeit von Hinweisen und optionale zeitgesteuerte Ausblendung.
passwordMetercomponents/ui/PasswordMeter.jsAnzeige von Passwortanforderung und -stärke.
passwordStrengthcomponents/ui/PasswordStrength.jsBerechnung der Passwortstärke und Beschriftungen.
switchComponentcomponents/ui/Switch.jsUmschalt-Status und Änderungsbehandlung.
drawercomponents/ui/Drawer.jsDrawer-Lebenszyklus und Fokusverhalten.
globalProgresscomponents/ui/GlobalProgress.jsFortschritts-Events und aktueller Fortschrittswert.
globalToastcomponents/ui/GlobalToast.jsToast-Warteschlange, Ausblenden, Typ-Zuordnung und Stapelgrenzen.

Der Quellcode enthält außerdem Header.js, Sidebar.js, TopBarNotifications.js und Logo.js. Ihre Exporte stehen für lokale Imports zur Verfügung, sind aber derzeit nicht bei App.js registriert; registriere sie mit Alpine.data(), bevor du sie als globale x-data-Komponenten verwendest.

Stores, Hilfsfunktionen und magische Eigenschaften

APIQuelleVerwendung
$store.themestores/theme.jsHell-/Dunkelmodus, data-bs-theme und localStorage-Persistenz.
$store.sidebarstores/sidebar.jsDesktop-Einklappen, mobiles Ă–ffnen/SchlieĂźen und localStorage-Persistenz.
$formatDate`, `$formatDateTime, $relativeDateutils/dateFormat.jsKonsistente Datumsanzeige mit Fallbacks.
$countLabelutils/countLabel.jsBeschriftungen fĂĽr Singular/Plural-Anzahl.
$sortClass`, `$sortIconutils/sort.jsSortierbare Tabellenkopfzeilen und Indikatoren.
$passwordMeetsPolicyutils/passwordPolicy.jsPrĂĽft die konfigurierten Passwortanforderungen.
$isEmailApp.jsLeichtgewichtige PrĂĽfung des E-Mail-Formats.
$toast` / `$progresscomponents/ui/GlobalToast.js, GlobalProgress.jsGlobale Benachrichtigungs- und Fortschritts-APIs.
$focus` / `$copyApp.jsFokussiert ein Nachfahren-Element nach Alpine-Updates; kopiert Text ĂĽber die Browser-Zwischenablage-API.
createRemoteListing()utils/listing.jsGemeinsamer Zustand fĂĽr Remote-Auflistungen, Ladezustand, Pagination und Fehlerbehandlung.
fetchWithCsrf(), refreshCsrfToken()utils/csrf.jsSendet einen mutierenden Request mit dem CSRF-Token der Komponente und erholt sich einmalig von einem veralteten Token.

AlpinePlugins.js installiert Collapse, Focus, Mask und Persist. passkeys.js stellt die browserseitige WebAuthn-Integration bereit. Halte neue wiederverwendbare Browser-APIs hier dokumentiert und fĂĽge ihre Registrierung/ihren Import zu App.js hinzu, wenn sie global sind.

CSRF bei mutierenden Requests

Jede Komponenten-Aktion, die einen nicht-GET-Request sendet, geht über fetchWithCsrf() (utils/csrf.js), statt fetch() direkt aufzurufen. Dies ist der eine gekapselte Ort, an dem mutierende Requests gebaut werden, sodass sich das Token-Wiederherstellungsverhalten - und alles, was später hinzugefügt wird (Request-/Response-Hooks, globale Header, Telemetrie) - nur hier ändern muss, statt in jeder Komponente, die zufällig den Status ändert.

Warum sich überhaupt erholt werden muss. Der csrfToken einer Komponente wird einmalig eingebettet, wenn ihre View gerendert wird. Der Server kann ihn ungültig machen, während die Seite noch geöffnet ist, auf zwei Arten, die cbcsrfs eigene Dokumentation nennt: csrfField() (der Mixin hinter jedem versteckten csrf-Eingabefeld) erzwingt bei seiner ersten Verwendung pro Request eine Rotation des Session-Tokens, sodass jede Seite, die es rendert - Settings, die Passkey-erforderlich-Seite, die Auth-Seiten - stillschweigend das Token in jedem anderen geöffneten Tab ungültig macht; und ein Token läuft eine feste Zeit nach seiner Erstellung ab, nicht nach dem Laden der Seite, sodass eine spät im Leben eines Tokens gerenderte Seite eines mit nur noch Sekunden Restlaufzeit ausgeliefert bekommen kann. So oder so kann das eingebettete Token einer Komponente veralten, bevor der Benutzer mit dem Tippen fertig ist.

Der Vertrag:

export async function fetchWithCsrf( component, url, method, buildRequest ) { /* ... */ }
export async function refreshCsrfToken( component ) { /* ... */ }
  • component ist die Alpine-Komponenteninstanz (ĂĽbergib this). Sie muss eine veränderliche csrfToken-Eigenschaft bereitstellen - fetchWithCsrf() liest sie, um den Request zu bauen, und ĂĽberschreibt sie bei einem Retry wegen abgelaufenem Token mit dem aktuellen Token der Session via refreshCsrfToken().
  • buildRequest( csrfToken ) liefert die methodenspezifischen RequestInit-Felder (headers, body, credentials usw.) fĂĽr das gegebene Token. Sie wird beim Retry erneut aufgerufen, muss also den Body jedes Mal frisch aufbauen, statt einen einmal berechneten Wert einzuschlieĂźen - das erlaubt es demselben Helfer, URLSearchParams-, JSON.stringify()- und FormData-Bodys gleichermaĂźen abzudecken.
  • Bei einem 403 ruft fetchWithCsrf() refreshCsrfToken() auf und wiederholt, falls es tatsächlich ein neues Token erhalten hat, den Request einmal mit erneut aufgerufenem buildRequest(). Ein zweiter 403 (z. B. ein echter Autorisierungsfehler oder eine vollständig abgelaufene Session) wird unverändert zurĂĽckgegeben - Aufrufer brauchen dafĂĽr weiterhin ihre normale Fehlerbehandlung.
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" } };
} );

Nur GET/HEAD-Lesezugriffe überspringen fetchWithCsrf() und rufen fetch() direkt auf - sie tragen kein CSRF-Token und können deshalb keinen 403 dafür bekommen. Eine Handvoll cbSecurity-Modul-Endpunkte (die WebAuthn-Passkey-Zeremonie-Routen) werden ebenfalls mit reinem fetch() aufgerufen: Sie authentifizieren sich über die WebAuthn-Zeremonie selbst, nicht über das CSRF-Token dieser App, und liegen daher außerhalb des Geltungsbereichs dieses Helfers. Jede andere Mutation in resources/assets/js/components/ geht über fetchWithCsrf(); halte neue Formular-Komponenten damit konsistent, wenn sie einen Request hinzufügen, der den Server-Status ändert.

Benutzer-Avatare und das Branding-Logo der Anwendung werden auf der privaten cbfs-assets-Disk gespeichert (siehe Konfiguration) und von Assets.bx ausgeliefert (siehe Handler & Routing), statt als statische Dateien bereitgestellt zu werden.

The Profile page, showing the avatar upload and assigned role
Die Profilseite, mit Avatar-Upload und zugewiesener Rolle.
  • Anzeige läuft ĂĽber das Partial _components/ui/avatar: Es rendert <img src="/avatars/:userId/:size">, wenn hasAvatar wahr ist, und fällt andernfalls auf ein Initialen-<span> zurĂĽck. Es ist in Sidebar, Topbar und die Users-Auflistungstabelle eingebunden (serverseitig projiziertes hasAvatar-Feld) sowie inline auf der Users-Detailseite (x-show/x-cloak-Umschaltung basierend auf user.hasAvatar, da der Avatar dieser Seite in einer Alpine-gesteuerten Zusammenfassungskarte statt in einem statischen Partial sitzt).
  • Upload/Entfernen des eigenen Avatars des aktuellen Benutzers liegt auf der Profilseite, verwaltet von profileForm (ProfileForm.js): Ein verstecktes Datei-Eingabefeld liest das gewählte Bild als base64-Data-URI (readFileAsDataUrl()) und sendet es per POST an POST /profile/avatar; DELETE /profile/avatar entfernt ihn. Beide erhöhen einen version-Zähler, der als Cache-Busting-Query-Parameter fĂĽr die ausgelieferte URL verwendet wird, da sich der Dateipfad selbst zwischen Uploads nicht ändert.
  • Das Branding-Logo erhält dieselbe Upload-/Entfernen-Behandlung auf der Settings-Seite, ĂĽber die logoUploader-Komponente (LogoUploader.js) gegen POST/DELETE /settings/logo. Beim Upload ersetzt es den Textfeld-Wert der Einstellung cbAppLogo durch den ausgelieferten Pfad (/branding/logo/lg), und stellt beim Entfernen den konfigurierten Standard wieder her — das manuelle URL-Textfeld und die Live-<img>-Vorschau funktionieren weiterhin genau wie zuvor fĂĽr jeden, der cbAppLogo stattdessen auf eine externe URL zeigen lassen möchte.
  • Beide Upload-Endpunkte akzeptieren dieselben Formen: Bilder werden serverseitig mit BaseSecureHandler.decodeDataUri() dekodiert und dann von ImageService (app/models/system/ImageService.bx) in sm/lg-JPEG-Varianten (Avatar) oder PNG-Varianten (Logo) skaliert/zugeschnitten.
Diese Seite bearbeiten Markdown herunterladen Zuletzt aktualisiert Oct 1, 2026, 11:06:51 AM