Login-Seite individualisierbar (Branding, Texte, Hintergrund)
Some checks failed
CI / PHP Lint (push) Has been cancelled
CI / PHP Lint-1 (push) Has been cancelled
CI / Unit Tests & Static Analysis (push) Has been cancelled
CI / PHP Lint (pull_request) Has been cancelled
CI / PHP Lint-1 (pull_request) Has been cancelled
CI / Unit Tests & Static Analysis (pull_request) Has been cancelled

Neuer Einstellungs-Tab "Login-Seite" (Administration → Einstellungen):

- Firmenname und eigenes Logo für die Anmeldeseite
- Überschrift, Beschreibungstext, Stichpunkte (eine Zeile je Punkt) und
  Fußzeile frei setzbar
- Hintergrundbild für die linke Bildspalte, alternativ Farbverlauf mit
  Farbwähler (Start-/Endfarbe) sowie einstellbarer Abdunklung für die
  Lesbarkeit auf hellen Bildern
- Bildspalte abschaltbar: dann zentrierte Anmeldekarte auf dem
  App-Hintergrund
- "Vorschau öffnen" zeigt die Login-Seite als angemeldeter Admin
  (login.php?preview=1), ohne die Sitzung zu beenden

Alle Felder sind optional; leere Werte fallen auf die bisherigen
Standardtexte und -farben zurück. Bestehende Installationen sehen
unverändert aus, es ist keine Migration nötig.

README um den Abschnitt "Login-Seite individualisieren" mit Beispiel-
Screenshots ergänzt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Friederich Loheide 2026-09-22 20:40:48 +00:00
parent 498c7e28e0
commit 9a4b2e1cd4
10 changed files with 308 additions and 16 deletions

View file

@ -46,6 +46,7 @@
- 🐳 **Docker** Dockerfile + docker-compose (MariaDB) - 🐳 **Docker** Dockerfile + docker-compose (MariaDB)
- 🌍 **Öffentlicher Modus** optional ohne Login nutzbar (mit CSRF-Schutz & Throttle) - 🌍 **Öffentlicher Modus** optional ohne Login nutzbar (mit CSRF-Schutz & Throttle)
- 🎨 **Einheitliches Design-System** ein Stylesheet für Frontend, Login und Backend (Tokens, Komponenten, Light/Dark) - 🎨 **Einheitliches Design-System** ein Stylesheet für Frontend, Login und Backend (Tokens, Komponenten, Light/Dark)
- 🏷️ **Login-Seite individualisierbar** Firmenname, Logo, Texte, Hintergrundbild bzw. Farbverlauf
- 🌗 **Dark Mode** umschaltbar, Einstellung wird im Browser gespeichert - 🌗 **Dark Mode** umschaltbar, Einstellung wird im Browser gespeichert
- 🌐 **Mehrsprachig** Deutsch / Englisch per Umschalter (`lang/`) - 🌐 **Mehrsprachig** Deutsch / Englisch per Umschalter (`lang/`)
- 📱 **Responsive Admin-Layout** mit Hamburger-Menü & Sidebar-Overlay - 📱 **Responsive Admin-Layout** mit Hamburger-Menü & Sidebar-Overlay
@ -66,7 +67,12 @@
<div align="center"> <div align="center">
<img src="docs/screenshots/login.png" alt="Anmeldung" width="48%"> <img src="docs/screenshots/login.png" alt="Anmeldung" width="48%">
<img src="docs/screenshots/login-branding.png" alt="Anmeldung mit eigenem Branding" width="48%">
</div>
<div align="center">
<img src="docs/screenshots/voucher-form.png" alt="Voucher erstellen" width="48%"> <img src="docs/screenshots/voucher-form.png" alt="Voucher erstellen" width="48%">
<img src="docs/screenshots/settings-login.png" alt="Einstellungen der Login-Seite" width="48%">
</div> </div>
<div align="center"> <div align="center">
@ -259,6 +265,27 @@ eigenen CSS-Datei oder direkt in `assets/global.css`:
Logo und Favicon werden nicht über CSS, sondern unter Logo und Favicon werden nicht über CSS, sondern unter
**Administration → Einstellungen → Allgemein** gesetzt. **Administration → Einstellungen → Allgemein** gesetzt.
### Login-Seite individualisieren
Unter **Administration → Einstellungen → Login-Seite** lässt sich die Anmeldeseite
ohne Code-Änderung an das eigene Haus anpassen. Leere Felder verwenden jeweils den
Standardwert eine frische Installation sieht also unverändert aus.
| Einstellung | Wirkung |
|---|---|
| Linke Bildspalte anzeigen | Split-Screen an/aus. Aus = zentrierte Anmeldekarte |
| Firmenname | Name neben dem Logo bzw. in der Fußzeile (leer = Anwendungstitel) |
| Logo (URL) | Eigenes Logo in der Bildspalte (leer = allgemeines Logo) |
| Überschrift / Beschreibungstext | Claim in der Bildspalte |
| Stichpunkte | Liste mit Haken ein Stichpunkt pro Zeile, leer = keine Liste |
| Fußzeile | z. B. `© 2026 Muster GmbH · Datenschutz · Impressum` |
| Hintergrundbild (URL) | Formatfüllendes Bild der Bildspalte |
| Verlauf Start-/Endfarbe | Farbverlauf, wenn kein Bild gesetzt ist |
| Abdunklung (%) | Dunkle Ebene über dem Bild, damit der Text lesbar bleibt |
Über **„Vorschau öffnen"** lässt sich die Login-Seite als angemeldeter Administrator
ansehen (`login.php?preview=1`), ohne sich abzumelden.
--- ---
## 🛡️ Sicherheit ## 🛡️ Sicherheit

View file

@ -58,6 +58,21 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_settings'])) {
$settings['public_access'] = isset($_POST['public_access']) ? '1' : '0'; $settings['public_access'] = isset($_POST['public_access']) ? '1' : '0';
} }
if ($formType === 'login') {
$settings['login_panel_enabled'] = isset($_POST['login_panel_enabled']) ? '1' : '0';
$settings['login_brand_name'] = trim($_POST['login_brand_name'] ?? '');
$settings['login_logo_url'] = trim($_POST['login_logo_url'] ?? '');
$settings['login_claim_title'] = trim($_POST['login_claim_title'] ?? '');
$settings['login_claim_text'] = trim($_POST['login_claim_text'] ?? '');
$settings['login_features'] = trim($_POST['login_features'] ?? '');
$settings['login_footer'] = trim($_POST['login_footer'] ?? '');
$settings['login_bg_image'] = trim($_POST['login_bg_image'] ?? '');
$settings['login_bg_from'] = trim($_POST['login_bg_from'] ?? '');
$settings['login_bg_to'] = trim($_POST['login_bg_to'] ?? '');
$overlay = (int)($_POST['login_bg_overlay'] ?? 40);
$settings['login_bg_overlay'] = (string)max(0, min(90, $overlay));
}
if ($formType === 'defaults') { if ($formType === 'defaults') {
$expMin = (int)($_POST['default_expire_minutes'] ?? 480); $expMin = (int)($_POST['default_expire_minutes'] ?? 480);
$defDev = (int)($_POST['default_max_uses'] ?? 1); $defDev = (int)($_POST['default_max_uses'] ?? 1);
@ -190,6 +205,17 @@ $cs = [
'email_user_notification_body' => $db->getSetting('email_user_notification_body', "Hallo {USER_NAME},\n\n{CHANGES}"), 'email_user_notification_body' => $db->getSetting('email_user_notification_body', "Hallo {USER_NAME},\n\n{CHANGES}"),
'tinymce_api_key' => $db->getSetting('tinymce_api_key', ''), 'tinymce_api_key' => $db->getSetting('tinymce_api_key', ''),
'print_template' => $db->getSetting('print_template', '<div style="text-align:center;padding:40px"><h1>{APP_TITLE}</h1><h2>WLAN Code</h2><div style="font-size:48px;font-weight:bold;margin:30px 0;font-family:monospace">{VOUCHER_CODE}</div><p><strong>Gültig bis:</strong> {EXPIRY_DATE} {EXPIRY_TIME}</p><p><strong>Site:</strong> {SITE_NAME}</p><p><strong>Geräte:</strong> {MAX_USES}</p><hr style="margin:30px 0"><div>{INSTRUCTIONS}</div></div>'), 'print_template' => $db->getSetting('print_template', '<div style="text-align:center;padding:40px"><h1>{APP_TITLE}</h1><h2>WLAN Code</h2><div style="font-size:48px;font-weight:bold;margin:30px 0;font-family:monospace">{VOUCHER_CODE}</div><p><strong>Gültig bis:</strong> {EXPIRY_DATE} {EXPIRY_TIME}</p><p><strong>Site:</strong> {SITE_NAME}</p><p><strong>Geräte:</strong> {MAX_USES}</p><hr style="margin:30px 0"><div>{INSTRUCTIONS}</div></div>'),
'login_panel_enabled' => $db->getSetting('login_panel_enabled', '1'),
'login_brand_name' => $db->getSetting('login_brand_name', ''),
'login_logo_url' => $db->getSetting('login_logo_url', ''),
'login_claim_title' => $db->getSetting('login_claim_title', ''),
'login_claim_text' => $db->getSetting('login_claim_text', ''),
'login_features' => $db->getSetting('login_features', ''),
'login_footer' => $db->getSetting('login_footer', ''),
'login_bg_image' => $db->getSetting('login_bg_image', ''),
'login_bg_from' => $db->getSetting('login_bg_from', '#3b2f8f'),
'login_bg_to' => $db->getSetting('login_bg_to', '#6d5ce7'),
'login_bg_overlay' => $db->getSetting('login_bg_overlay', '40'),
'cron_token' => $db->getSetting('cron_token', ''), 'cron_token' => $db->getSetting('cron_token', ''),
'last_cron_sync' => $db->getSetting('last_cron_sync', ''), 'last_cron_sync' => $db->getSetting('last_cron_sync', ''),
]; ];
@ -232,6 +258,7 @@ $adminBase = '';
<div class="tab-navigation" id="tabNav"> <div class="tab-navigation" id="tabNav">
<button class="tab-button active" data-tab="general"><i class="fas fa-sliders-h"></i> <?= __('settings_tab_general') ?></button> <button class="tab-button active" data-tab="general"><i class="fas fa-sliders-h"></i> <?= __('settings_tab_general') ?></button>
<button class="tab-button" data-tab="defaults"><i class="fas fa-sliders-h"></i> <?= __('settings_tab_defaults') ?></button> <button class="tab-button" data-tab="defaults"><i class="fas fa-sliders-h"></i> <?= __('settings_tab_defaults') ?></button>
<button class="tab-button" data-tab="login"><i class="fas fa-right-to-bracket"></i> <?= __('settings_tab_login') ?></button>
<button class="tab-button" data-tab="cron"><i class="fas fa-clock"></i> <?= __('settings_tab_cron') ?></button> <button class="tab-button" data-tab="cron"><i class="fas fa-clock"></i> <?= __('settings_tab_cron') ?></button>
<button class="tab-button" data-tab="m365"><i class="fab fa-microsoft"></i> <?= __('settings_tab_m365') ?></button> <button class="tab-button" data-tab="m365"><i class="fab fa-microsoft"></i> <?= __('settings_tab_m365') ?></button>
<button class="tab-button" data-tab="smtp"><i class="fas fa-envelope"></i> <?= __('settings_tab_smtp') ?></button> <button class="tab-button" data-tab="smtp"><i class="fas fa-envelope"></i> <?= __('settings_tab_smtp') ?></button>
@ -295,6 +322,92 @@ $adminBase = '';
</form> </form>
</div> </div>
<!-- Login-Seite -->
<div id="tab-login" class="tab-content">
<h2 style="margin-bottom: 8px; color: var(--text-primary);"><i class="fas fa-right-to-bracket"></i> <?= __('settings_tab_login') ?></h2>
<p style="color: var(--text-muted); font-size: 14px; margin-bottom: 24px;"><?= __('settings_login_intro') ?></p>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
<input type="hidden" name="form_type" value="login">
<div class="checkbox-group" style="margin-bottom: 20px;">
<input type="checkbox" name="login_panel_enabled" id="login_panel_enabled" <?= $cs['login_panel_enabled'] === '1' ? 'checked' : '' ?>>
<label for="login_panel_enabled" style="margin:0;"><?= __('settings_login_panel') ?></label>
</div>
<div class="form-grid">
<div class="form-group">
<label><?= __('settings_login_brand') ?></label>
<input type="text" name="login_brand_name" value="<?= htmlspecialchars($cs['login_brand_name']) ?>" placeholder="<?= htmlspecialchars($cs['app_title']) ?>">
<div class="help-text"><?= __('settings_login_brand_hint') ?></div>
</div>
<div class="form-group">
<label><?= __('settings_login_logo') ?></label>
<input type="url" name="login_logo_url" value="<?= htmlspecialchars($cs['login_logo_url']) ?>" placeholder="https://example.com/logo.svg">
<div class="help-text"><?= __('settings_login_logo_hint') ?></div>
</div>
</div>
<hr class="section-divider">
<div class="form-group">
<label><?= __('settings_login_claim_title') ?></label>
<input type="text" name="login_claim_title" value="<?= htmlspecialchars($cs['login_claim_title']) ?>" placeholder="<?= htmlspecialchars(__('auth_claim_title')) ?>">
</div>
<div class="form-group">
<label><?= __('settings_login_claim_text') ?></label>
<textarea name="login_claim_text" rows="3" placeholder="<?= htmlspecialchars(__('auth_claim_text')) ?>"><?= htmlspecialchars($cs['login_claim_text']) ?></textarea>
</div>
<div class="form-group">
<label><?= __('settings_login_features') ?></label>
<textarea name="login_features" rows="4" placeholder="<?= htmlspecialchars(__('auth_feature_1') . "\n" . __('auth_feature_2') . "\n" . __('auth_feature_3')) ?>"><?= htmlspecialchars($cs['login_features']) ?></textarea>
<div class="help-text"><?= __('settings_login_features_hint') ?></div>
</div>
<div class="form-group">
<label><?= __('settings_login_footer') ?></label>
<input type="text" name="login_footer" value="<?= htmlspecialchars($cs['login_footer']) ?>" placeholder="© <?= date('Y') ?> <?= htmlspecialchars($cs['app_title']) ?>">
</div>
<hr class="section-divider">
<div class="form-group">
<label><?= __('settings_login_bg_image') ?></label>
<input type="url" name="login_bg_image" value="<?= htmlspecialchars($cs['login_bg_image']) ?>" placeholder="https://example.com/empfang.jpg">
<div class="help-text"><?= __('settings_login_bg_image_hint') ?></div>
</div>
<div class="form-grid">
<div class="form-group">
<label><?= __('settings_login_bg_from') ?></label>
<div class="color-field">
<input type="color" class="color-swatch" data-target="login_bg_from"
value="<?= preg_match('/^#[0-9a-fA-F]{6}$/', $cs['login_bg_from']) ? htmlspecialchars($cs['login_bg_from']) : '#3b2f8f' ?>">
<input type="text" id="login_bg_from" name="login_bg_from" value="<?= htmlspecialchars($cs['login_bg_from']) ?>" placeholder="#3b2f8f">
</div>
</div>
<div class="form-group">
<label><?= __('settings_login_bg_to') ?></label>
<div class="color-field">
<input type="color" class="color-swatch" data-target="login_bg_to"
value="<?= preg_match('/^#[0-9a-fA-F]{6}$/', $cs['login_bg_to']) ? htmlspecialchars($cs['login_bg_to']) : '#6d5ce7' ?>">
<input type="text" id="login_bg_to" name="login_bg_to" value="<?= htmlspecialchars($cs['login_bg_to']) ?>" placeholder="#6d5ce7">
</div>
</div>
<div class="form-group">
<label><?= __('settings_login_overlay') ?></label>
<input type="number" name="login_bg_overlay" min="0" max="90" value="<?= (int)$cs['login_bg_overlay'] ?>">
<div class="help-text"><?= __('settings_login_overlay_hint') ?></div>
</div>
</div>
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:16px;">
<button type="submit" name="save_settings" class="btn btn-primary"><i class="fas fa-save"></i> <?= __('btn_save') ?></button>
<a href="../login.php?preview=1" target="_blank" rel="noopener" class="btn btn-secondary">
<i class="fas fa-arrow-up-right-from-square"></i> <?= __('settings_login_preview') ?>
</a>
</div>
</form>
</div>
<!-- Cron-Sync --> <!-- Cron-Sync -->
<div id="tab-cron" class="tab-content"> <div id="tab-cron" class="tab-content">
<h2 style="margin-bottom: 20px; color: var(--text-primary);"><i class="fas fa-clock"></i> <?= __('settings_tab_cron') ?></h2> <h2 style="margin-bottom: 20px; color: var(--text-primary);"><i class="fas fa-clock"></i> <?= __('settings_tab_cron') ?></h2>
@ -466,6 +579,16 @@ document.querySelectorAll('.tab-button').forEach(btn => {
}); });
}); });
// Farbwähler und Hex-Feld synchron halten
document.querySelectorAll('.color-swatch').forEach(swatch => {
const field = document.getElementById(swatch.dataset.target);
if (!field) return;
swatch.addEventListener('input', () => { field.value = swatch.value; });
field.addEventListener('input', () => {
if (/^#[0-9a-fA-F]{6}$/.test(field.value.trim())) swatch.value = field.value.trim();
});
});
// Restore tab from hash // Restore tab from hash
window.addEventListener('DOMContentLoaded', function() { window.addEventListener('DOMContentLoaded', function() {
const hash = location.hash.substring(1); const hash = location.hash.substring(1);

View file

@ -999,6 +999,11 @@ input[type="file"] { font-size: 13px; color: var(--text-secondary); }
background: var(--bg-card); background: var(--bg-card);
} }
.auth-visual { .auth-visual {
/* Über die Einstellungen überschreibbar (login_bg_from/-to/-image/-overlay). */
--login-from: #3b2f8f;
--login-to: #6d5ce7;
--login-overlay: 0.4;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
padding: 56px; padding: 56px;
@ -1007,9 +1012,9 @@ input[type="file"] { font-size: 13px; color: var(--text-secondary); }
justify-content: space-between; justify-content: space-between;
color: #fff; color: #fff;
background: background:
radial-gradient(900px 480px at 12% 8%, rgba(167,139,250,.55), transparent 60%), radial-gradient(900px 480px at 12% 8%, rgba(255,255,255,.18), transparent 60%),
radial-gradient(700px 520px at 88% 92%, rgba(56,189,248,.35), transparent 62%), radial-gradient(700px 520px at 88% 92%, rgba(0,0,0,.18), transparent 62%),
linear-gradient(150deg, #3b2f8f 0%, #4b3ec4 45%, #6d5ce7 100%); linear-gradient(150deg, var(--login-from) 0%, var(--login-to) 100%);
} }
.auth-visual::after { .auth-visual::after {
content: ''; content: '';
@ -1022,6 +1027,59 @@ input[type="file"] { font-size: 13px; color: var(--text-secondary); }
-webkit-mask-image: radial-gradient(70% 70% at 50% 40%, #000 30%, transparent 100%); -webkit-mask-image: radial-gradient(70% 70% at 50% 40%, #000 30%, transparent 100%);
pointer-events: none; pointer-events: none;
} }
/* Eigenes Hintergrundbild: Bild füllt die Spalte, darüber liegt eine
abdunkelnde Ebene, damit der Text lesbar bleibt. */
.auth-visual.has-image {
background: var(--login-image) center / cover no-repeat, var(--login-from);
}
.auth-visual.has-image::after { display: none; }
.auth-visual.has-image::before {
content: '';
position: absolute; inset: 0;
background: linear-gradient(160deg,
rgba(0,0,0,calc(var(--login-overlay) * 0.7)) 0%,
rgba(0,0,0,var(--login-overlay)) 100%);
pointer-events: none;
}
.auth-brand-logo { max-height: 42px; max-width: 220px; width: auto; }
/* Ohne Bildspalte: zentrierte Karte auf dem App-Hintergrund. */
.auth-body-single {
grid-template-columns: 1fr;
background:
radial-gradient(760px 420px at 8% -6%, var(--accent-soft), transparent 60%),
radial-gradient(640px 400px at 102% 4%, rgba(139,92,246,.10), transparent 62%),
var(--bg-body);
}
.auth-body-single .auth-panel { background: transparent; }
.auth-body-single .login-container {
max-width: 420px;
padding: 34px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--r-xl);
box-shadow: var(--shadow-xl);
}
/* Farbwähler mit Hex-Eingabe */
.color-field { display: flex; align-items: center; gap: 8px; }
.color-field input[type="color"] {
width: 42px; height: 38px; flex-shrink: 0;
padding: 3px;
border: 1px solid var(--input-border);
border-radius: var(--r-md);
background: var(--bg-input);
cursor: pointer;
}
.color-field input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
.color-field input[type="color"]::-webkit-color-swatch { border: none; border-radius: 6px; }
.color-field input[type="text"] { font-family: var(--font-mono); font-size: 13px; }
.auth-mark {
display: flex; align-items: center; gap: 11px;
margin-bottom: 20px;
font-size: 15px; font-weight: 620;
color: var(--text-primary);
}
.auth-visual > * { position: relative; z-index: 1; } .auth-visual > * { position: relative; z-index: 1; }
.auth-brand { display: flex; align-items: center; gap: 12px; font-size: 15px; font-weight: 600; } .auth-brand { display: flex; align-items: center; gap: 12px; font-size: 15px; font-weight: 600; }
.auth-brand .brand-mark { background: rgba(255,255,255,.16); backdrop-filter: blur(6px); box-shadow: none; } .auth-brand .brand-mark { background: rgba(255,255,255,.16); backdrop-filter: blur(6px); box-shadow: none; }

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1 MiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 KiB

After

Width:  |  Height:  |  Size: 260 KiB

Before After
Before After

View file

@ -220,6 +220,25 @@ return [
// Settings // Settings
'settings_title' => 'Einstellungen', 'settings_title' => 'Einstellungen',
'settings_subtitle' => 'System-Konfiguration und Personalisierung', 'settings_subtitle' => 'System-Konfiguration und Personalisierung',
'settings_tab_login' => 'Login-Seite',
'settings_login_intro' => 'Aussehen und Texte der Anmeldeseite. Leere Felder verwenden die Standardwerte.',
'settings_login_panel' => 'Linke Bildspalte (Split-Screen) anzeigen',
'settings_login_brand' => 'Firmenname',
'settings_login_brand_hint' => 'Leer = Anwendungstitel wird verwendet.',
'settings_login_logo' => 'Logo (URL)',
'settings_login_logo_hint' => 'Leer = allgemeines Logo aus dem Tab „Allgemein".',
'settings_login_claim_title' => 'Überschrift',
'settings_login_claim_text' => 'Beschreibungstext',
'settings_login_features' => 'Stichpunkte',
'settings_login_features_hint'=> 'Ein Stichpunkt pro Zeile. Leer lassen, um keine Liste anzuzeigen.',
'settings_login_footer' => 'Fußzeile',
'settings_login_bg_image' => 'Hintergrundbild (URL)',
'settings_login_bg_image_hint'=> 'Optional. Wird formatfüllend angezeigt; ohne Bild gilt der Farbverlauf.',
'settings_login_bg_from' => 'Verlauf: Startfarbe',
'settings_login_bg_to' => 'Verlauf: Endfarbe',
'settings_login_overlay' => 'Abdunklung des Bildes (%)',
'settings_login_overlay_hint' => '090 %. Höhere Werte machen den Text auf hellen Bildern besser lesbar.',
'settings_login_preview' => 'Vorschau öffnen',
'settings_tab_general' => 'Allgemein', 'settings_tab_general' => 'Allgemein',
'settings_tab_defaults' => 'Voucher-Standards', 'settings_tab_defaults' => 'Voucher-Standards',
'settings_tab_cron' => 'Cron-Sync', 'settings_tab_cron' => 'Cron-Sync',

View file

@ -220,6 +220,25 @@ return [
// Settings // Settings
'settings_title' => 'Settings', 'settings_title' => 'Settings',
'settings_subtitle' => 'System configuration and customization', 'settings_subtitle' => 'System configuration and customization',
'settings_tab_login' => 'Login page',
'settings_login_intro' => 'Appearance and wording of the sign-in page. Empty fields fall back to the defaults.',
'settings_login_panel' => 'Show left image column (split screen)',
'settings_login_brand' => 'Company name',
'settings_login_brand_hint' => 'Empty = application title is used.',
'settings_login_logo' => 'Logo (URL)',
'settings_login_logo_hint' => 'Empty = general logo from the "General" tab.',
'settings_login_claim_title' => 'Headline',
'settings_login_claim_text' => 'Description',
'settings_login_features' => 'Bullet points',
'settings_login_features_hint'=> 'One bullet per line. Leave empty to hide the list.',
'settings_login_footer' => 'Footer',
'settings_login_bg_image' => 'Background image (URL)',
'settings_login_bg_image_hint'=> 'Optional. Displayed full-bleed; without an image the gradient is used.',
'settings_login_bg_from' => 'Gradient: start colour',
'settings_login_bg_to' => 'Gradient: end colour',
'settings_login_overlay' => 'Image dimming (%)',
'settings_login_overlay_hint' => '090%. Higher values keep text readable on bright images.',
'settings_login_preview' => 'Open preview',
'settings_tab_general' => 'General', 'settings_tab_general' => 'General',
'settings_tab_defaults' => 'Voucher Defaults', 'settings_tab_defaults' => 'Voucher Defaults',
'settings_tab_cron' => 'Cron Sync', 'settings_tab_cron' => 'Cron Sync',

View file

@ -10,7 +10,10 @@ require_once __DIR__ . '/includes/I18n.php';
try { try {
$auth = new Auth(); $auth = new Auth();
if ($auth->isLoggedIn()) { header('Location: index.php'); exit; } // Angemeldete Admins koennen die Login-Seite mit ?preview=1 ansehen
// (Vorschau aus den Einstellungen), ohne abgemeldet zu werden.
$isPreview = isset($_GET['preview']) && $auth->isLoggedIn() && $auth->isAdmin();
if ($auth->isLoggedIn() && !$isPreview) { header('Location: index.php'); exit; }
} catch (Exception $e) { } catch (Exception $e) {
die('Fehler beim Initialisieren: ' . $e->getMessage()); die('Fehler beim Initialisieren: ' . $e->getMessage());
} }
@ -117,6 +120,34 @@ try {
$showLocalLogin = isset($_GET['local']) && $_GET['local'] === '1'; $showLocalLogin = isset($_GET['local']) && $_GET['local'] === '1';
// --- Individualisierung der Login-Seite -------------------------------
// Alle Werte sind optional; leer bedeutet "Standard verwenden".
$loginBrand = $db->getSetting('login_brand_name', '') ?: $appTitle;
$loginLogo = $db->getSetting('login_logo_url', '') ?: $logoUrl;
$showPanel = $db->getSetting('login_panel_enabled', '1') === '1';
$claimTitle = $db->getSetting('login_claim_title', '') ?: __('auth_claim_title');
$claimText = $db->getSetting('login_claim_text', '') ?: __('auth_claim_text');
$featureRaw = trim((string)$db->getSetting('login_features', ''));
if ($featureRaw !== '') {
$loginFeatures = array_values(array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', $featureRaw))));
} else {
$loginFeatures = [__('auth_feature_1'), __('auth_feature_2'), __('auth_feature_3')];
}
$loginFooter = $db->getSetting('login_footer', '') ?: ('© ' . date('Y') . ' ' . $loginBrand);
$loginBgImage = trim((string)$db->getSetting('login_bg_image', ''));
$loginBgFrom = $db->getSetting('login_bg_from', '') ?: '#3b2f8f';
$loginBgTo = $db->getSetting('login_bg_to', '') ?: '#6d5ce7';
$loginOverlay = max(0, min(90, (int)$db->getSetting('login_bg_overlay', '40')));
$visualStyle = '--login-from:' . htmlspecialchars($loginBgFrom, ENT_QUOTES)
. ';--login-to:' . htmlspecialchars($loginBgTo, ENT_QUOTES)
. ';--login-overlay:' . ($loginOverlay / 100);
if ($loginBgImage !== '') {
$visualStyle .= ";--login-image:url('" . htmlspecialchars($loginBgImage, ENT_QUOTES) . "')";
}
} catch (Exception $e) { } catch (Exception $e) {
die('Datenbankfehler: ' . $e->getMessage()); die('Datenbankfehler: ' . $e->getMessage());
} }
@ -134,24 +165,32 @@ try {
<link rel="stylesheet" href="assets/global.css"> <link rel="stylesheet" href="assets/global.css">
<script>(function(){ const t=localStorage.getItem('theme')||'light'; document.documentElement.setAttribute('data-theme',t); })();</script> <script>(function(){ const t=localStorage.getItem('theme')||'light'; document.documentElement.setAttribute('data-theme',t); })();</script>
</head> </head>
<body class="auth-body"> <body class="auth-body<?= $showPanel ? '' : ' auth-body-single' ?>">
<section class="auth-visual"> <?php if ($showPanel): ?>
<section class="auth-visual<?= $loginBgImage !== '' ? ' has-image' : '' ?>" style="<?= $visualStyle ?>">
<div class="auth-brand"> <div class="auth-brand">
<span class="brand-mark"><i class="fas fa-wifi"></i></span> <?php if ($loginLogo): ?>
<span><?= htmlspecialchars($appTitle) ?></span> <img src="<?= htmlspecialchars($loginLogo) ?>" alt="<?= htmlspecialchars($loginBrand) ?>" class="auth-brand-logo">
<?php else: ?>
<span class="brand-mark"><i class="fas fa-wifi"></i></span>
<span><?= htmlspecialchars($loginBrand) ?></span>
<?php endif; ?>
</div> </div>
<div class="auth-claim"> <div class="auth-claim">
<h2><?= __('auth_claim_title') ?></h2> <?php if ($claimTitle !== ''): ?><h2><?= htmlspecialchars($claimTitle) ?></h2><?php endif; ?>
<p><?= __('auth_claim_text') ?></p> <?php if ($claimText !== ''): ?><p><?= htmlspecialchars($claimText) ?></p><?php endif; ?>
<?php if (!empty($loginFeatures)): ?>
<ul class="auth-features"> <ul class="auth-features">
<li><span class="tick"><i class="fas fa-check"></i></span> <?= __('auth_feature_1') ?></li> <?php foreach ($loginFeatures as $feature): ?>
<li><span class="tick"><i class="fas fa-check"></i></span> <?= __('auth_feature_2') ?></li> <li><span class="tick"><i class="fas fa-check"></i></span> <?= htmlspecialchars($feature) ?></li>
<li><span class="tick"><i class="fas fa-check"></i></span> <?= __('auth_feature_3') ?></li> <?php endforeach; ?>
</ul> </ul>
<?php endif; ?>
</div> </div>
<div class="auth-foot">&copy; <?= date('Y') ?> <?= htmlspecialchars($appTitle) ?></div> <div class="auth-foot"><?= htmlspecialchars($loginFooter) ?></div>
</section> </section>
<?php endif; ?>
<section class="auth-panel"> <section class="auth-panel">
<div class="auth-tools"> <div class="auth-tools">
@ -167,8 +206,15 @@ try {
</div> </div>
<div class="login-container"> <div class="login-container">
<?php if ($logoUrl): ?> <?php if (!$showPanel): ?>
<img src="<?= htmlspecialchars($logoUrl) ?>" alt="Logo" class="logo"> <?php if ($loginLogo): ?>
<img src="<?= htmlspecialchars($loginLogo) ?>" alt="<?= htmlspecialchars($loginBrand) ?>" class="logo">
<?php else: ?>
<div class="auth-mark">
<span class="brand-mark"><i class="fas fa-wifi"></i></span>
<span><?= htmlspecialchars($loginBrand) ?></span>
</div>
<?php endif; ?>
<?php endif; ?> <?php endif; ?>
<h1><?= __('login_title') ?></h1> <h1><?= __('login_title') ?></h1>
<p class="subtitle"><?= __('login_subtitle') ?></p> <p class="subtitle"><?= __('login_subtitle') ?></p>