@@ -259,6 +265,27 @@ eigenen CSS-Datei oder direkt in `assets/global.css`:
Logo und Favicon werden nicht über CSS, sondern unter
**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
diff --git a/admin/settings.php b/admin/settings.php
index ceed353..e6429f6 100644
--- a/admin/settings.php
+++ b/admin/settings.php
@@ -58,6 +58,21 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_settings'])) {
$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') {
$expMin = (int)($_POST['default_expire_minutes'] ?? 480);
$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}"),
'tinymce_api_key' => $db->getSetting('tinymce_api_key', ''),
'print_template' => $db->getSetting('print_template', '
'),
+ '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', ''),
'last_cron_sync' => $db->getSetting('last_cron_sync', ''),
];
@@ -232,6 +258,7 @@ $adminBase = '';
= __('settings_tab_cron') ?>
@@ -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
window.addEventListener('DOMContentLoaded', function() {
const hash = location.hash.substring(1);
diff --git a/assets/global.css b/assets/global.css
index f44140b..d54c3e2 100644
--- a/assets/global.css
+++ b/assets/global.css
@@ -999,6 +999,11 @@ input[type="file"] { font-size: 13px; color: var(--text-secondary); }
background: var(--bg-card);
}
.auth-visual {
+ /* Über die Einstellungen überschreibbar (login_bg_from/-to/-image/-overlay). */
+ --login-from: #3b2f8f;
+ --login-to: #6d5ce7;
+ --login-overlay: 0.4;
+
position: relative;
overflow: hidden;
padding: 56px;
@@ -1007,9 +1012,9 @@ input[type="file"] { font-size: 13px; color: var(--text-secondary); }
justify-content: space-between;
color: #fff;
background:
- radial-gradient(900px 480px at 12% 8%, rgba(167,139,250,.55), transparent 60%),
- radial-gradient(700px 520px at 88% 92%, rgba(56,189,248,.35), transparent 62%),
- linear-gradient(150deg, #3b2f8f 0%, #4b3ec4 45%, #6d5ce7 100%);
+ radial-gradient(900px 480px at 12% 8%, rgba(255,255,255,.18), transparent 60%),
+ radial-gradient(700px 520px at 88% 92%, rgba(0,0,0,.18), transparent 62%),
+ linear-gradient(150deg, var(--login-from) 0%, var(--login-to) 100%);
}
.auth-visual::after {
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%);
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-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; }
diff --git a/docs/screenshots/login-branding.png b/docs/screenshots/login-branding.png
new file mode 100644
index 0000000..7a941b8
Binary files /dev/null and b/docs/screenshots/login-branding.png differ
diff --git a/docs/screenshots/login.png b/docs/screenshots/login.png
index 94f056d..100d8f4 100644
Binary files a/docs/screenshots/login.png and b/docs/screenshots/login.png differ
diff --git a/docs/screenshots/settings-login.png b/docs/screenshots/settings-login.png
new file mode 100644
index 0000000..f4256cb
Binary files /dev/null and b/docs/screenshots/settings-login.png differ
diff --git a/docs/screenshots/settings.png b/docs/screenshots/settings.png
index 1bfb8fb..fc43c4c 100644
Binary files a/docs/screenshots/settings.png and b/docs/screenshots/settings.png differ
diff --git a/lang/de.php b/lang/de.php
index 0c548a1..af979e8 100644
--- a/lang/de.php
+++ b/lang/de.php
@@ -220,6 +220,25 @@ return [
// Settings
'settings_title' => 'Einstellungen',
'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' => '0–90 %. Höhere Werte machen den Text auf hellen Bildern besser lesbar.',
+ 'settings_login_preview' => 'Vorschau öffnen',
'settings_tab_general' => 'Allgemein',
'settings_tab_defaults' => 'Voucher-Standards',
'settings_tab_cron' => 'Cron-Sync',
diff --git a/lang/en.php b/lang/en.php
index 04f4114..33c423c 100644
--- a/lang/en.php
+++ b/lang/en.php
@@ -220,6 +220,25 @@ return [
// Settings
'settings_title' => 'Settings',
'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' => '0–90%. Higher values keep text readable on bright images.',
+ 'settings_login_preview' => 'Open preview',
'settings_tab_general' => 'General',
'settings_tab_defaults' => 'Voucher Defaults',
'settings_tab_cron' => 'Cron Sync',
diff --git a/login.php b/login.php
index 7942230..1e977f7 100644
--- a/login.php
+++ b/login.php
@@ -10,7 +10,10 @@ require_once __DIR__ . '/includes/I18n.php';
try {
$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) {
die('Fehler beim Initialisieren: ' . $e->getMessage());
}
@@ -117,6 +120,34 @@ try {
$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) {
die('Datenbankfehler: ' . $e->getMessage());
}
@@ -134,24 +165,32 @@ try {
-
+
-
+
+
-
-
= htmlspecialchars($appTitle) ?>
+
+
 ?>)
+
+
+
= htmlspecialchars($loginBrand) ?>
+
-
= __('auth_claim_title') ?>
-
= __('auth_claim_text') ?>
+
= htmlspecialchars($claimTitle) ?>
+
= htmlspecialchars($claimText) ?>
+
- - = __('auth_feature_1') ?>
- - = __('auth_feature_2') ?>
- - = __('auth_feature_3') ?>
+
+ - = htmlspecialchars($feature) ?>
+
+
-
+
+
@@ -167,8 +206,15 @@ try {
-
-
 ?>)
+
+
+
 ?>)
+
+
+
+ = htmlspecialchars($loginBrand) ?>
+
+
= __('login_title') ?>
= __('login_subtitle') ?>