diff --git a/Readme.md b/Readme.md index ba33425..af6cb97 100644 --- a/Readme.md +++ b/Readme.md @@ -46,6 +46,7 @@ - 🐳 **Docker** – Dockerfile + docker-compose (MariaDB) - 🌍 **Ö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) +- 🏷️ **Login-Seite individualisierbar** – Firmenname, Logo, Texte, Hintergrundbild bzw. Farbverlauf - 🌗 **Dark Mode** – umschaltbar, Einstellung wird im Browser gespeichert - 🌐 **Mehrsprachig** – Deutsch / Englisch per Umschalter (`lang/`) - 📱 **Responsive Admin-Layout** mit Hamburger-Menü & Sidebar-Overlay @@ -66,7 +67,12 @@
Anmeldung + Anmeldung mit eigenem Branding +
+ +
Voucher erstellen + Einstellungen der Login-Seite
@@ -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', '

{APP_TITLE}

WLAN Code

{VOUCHER_CODE}

Gültig bis: {EXPIRY_DATE} {EXPIRY_TIME}

Site: {SITE_NAME}

Geräte: {MAX_USES}


{INSTRUCTIONS}
'), + '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 = '';
+ @@ -295,6 +322,92 @@ $adminBase = '';
+ +
+

+

+
+ + + +
+ > + +
+ +
+
+ + +
+
+
+ + +
+
+
+ +
+ +
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+ +
+ +
+ + +
+
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ + +
+
+
+ +
+ + + + +
+
+
+

@@ -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 { - + -
+ +
- - + + + + + +
-

-

+

+

+
    -
  • -
  • -
  • + +
  • +
+
-
©
+
+
@@ -167,8 +206,15 @@ try {