diff --git a/Readme.md b/Readme.md
index 7102102..dad67de 100644
--- a/Readme.md
+++ b/Readme.md
@@ -24,13 +24,20 @@
## ✨ Features
- 🎟️ **Voucher-Erstellung** mit sofortiger QR-Code-Anzeige, Druckvorlage und E-Mail-Versand
+- 📦 **Bulk-Erstellung** – bis zu 20 Vouchers auf einmal, inkl. Sammeldruck-Layout
+- 🧩 **Voucher-Profile/Templates** – vordefinierte Laufzeiten & Gerätelimits per Schnellauswahl
- 🏢 **Multi-Site-Support** – beliebig viele UniFi-Standorte zentral verwalten
- 👥 **Benutzerverwaltung** mit granularer Site-Zugriffskontrolle
- 🔐 **Authentifizierung** via lokale Accounts **oder** Microsoft 365 OAuth
+- 🔑 **Passwort-Reset** per E-Mail (token-basiert, zeitlich begrenzt)
- 🌍 **Öffentlicher Modus** – optional ohne Login nutzbar (mit CSRF-Schutz & Throttle)
+- 🌗 **Dark Mode** – umschaltbar, Einstellung wird im Browser gespeichert
+- 🌐 **Mehrsprachig** – Deutsch / Englisch per Umschalter (`lang/`)
+- 📱 **Responsive Admin-Layout** mit Hamburger-Menü & Sidebar-Overlay
- 📊 **Admin-Dashboard** mit Live-Statistiken und Sync-Funktion
+- 📝 **Audit-Log** – nachvollziehbare Protokollierung von Login & Änderungen (mit Filter)
- 📥 **CSV-Export** aller Vouchers pro Site
-- 🔄 **Integrierter Auto-Updater** – Updates per Klick aus dem Admin-Bereich
+- 🔄 **Integrierter Auto-Updater** – Updates & DB-Migrationen per Klick aus dem Admin-Bereich
- 🛡️ **Security-by-default**: CSRF-Schutz, bcrypt-Passwörter, Prepared Statements,
Login-Rate-Limiting, OAuth-State-Validierung, Verschlüsselung sensibler Daten
@@ -46,6 +53,13 @@
+### Bulk-Erstellung & Dark Mode
+
+
+

+

+
+
### Administration & Updater
@@ -129,7 +143,9 @@ SSH oder manuelles `git pull`.
geschützten Pfaden (`config.php`, Uploads, …), automatischen DB-Migrationen
und OPcache-Reset
- 🔀 **Channel-Auswahl** zwischen `stable` und `development`
-- 📊 **Migrations-Status** in einem eigenen Tab
+- 📊 **Migrations-Status** in einem eigenen Tab – inkl. Button **„Ausstehende
+ Migrationen ausführen"** (legt z. B. neue Tabellen für bestehende
+ Installationen an, ohne dass ein Code-Update nötig ist)
Während eines Updates wird die Anwendung kurz in den **Wartungsmodus** versetzt:
@@ -272,12 +288,15 @@ Body: {"cmd": "delete-voucher", "_id": "
"}
## 🗺️ Roadmap
-- [ ] Voucher-Templates (vordefinierte Laufzeiten)
-- [ ] Bulk-Voucher-Erstellung
+- [x] Voucher-Templates (vordefinierte Laufzeiten)
+- [x] Bulk-Voucher-Erstellung
+- [x] Mehrsprachigkeit (DE/EN)
+- [x] Dark Mode
+- [x] Passwort-Reset
+- [x] Audit-Log
+- [x] Auto-Updater mit DB-Migrationen
- [ ] Erweiterte Reporting-Funktionen
- [ ] Docker-Container
-- [ ] Mehrsprachigkeit
-- [x] Auto-Updater mit DB-Migrationen
---
diff --git a/docs/screenshots/admin-dashboard-dark.png b/docs/screenshots/admin-dashboard-dark.png
new file mode 100644
index 0000000..0e345f0
Binary files /dev/null and b/docs/screenshots/admin-dashboard-dark.png differ
diff --git a/docs/screenshots/bulk-vouchers.png b/docs/screenshots/bulk-vouchers.png
new file mode 100644
index 0000000..5a7d429
Binary files /dev/null and b/docs/screenshots/bulk-vouchers.png differ
diff --git a/updater/UpdateController.php b/updater/UpdateController.php
index 99d9e28..c7543df 100644
--- a/updater/UpdateController.php
+++ b/updater/UpdateController.php
@@ -34,6 +34,7 @@ class UpdateController
case 'progress': $this->actionProgress(); break;
case 'set_channel': $this->actionSetChannel(); break;
case 'migrations': $this->actionMigrations(); break;
+ case 'run_migrations': $this->actionRunMigrations(); break;
default: $this->renderPage();
}
}
@@ -99,6 +100,28 @@ class UpdateController
}
}
+ private function actionRunMigrations(): void
+ {
+ if (!$this->auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
+ $this->json(['error' => 'Ungültiges Sicherheits-Token'], 403);
+ return;
+ }
+ try {
+ $runner = new MigrationRunner(
+ $this->db->getConnection(),
+ __DIR__ . '/migrations',
+ __DIR__ . '/storage'
+ );
+ $applied = $runner->runPending(true);
+ if ($this->audit) {
+ $this->audit->log('migrations_run', ['applied' => $applied], $_SESSION['user_id'] ?? null);
+ }
+ $this->json(['success' => true, 'applied' => $applied, 'migrations' => $runner->status()]);
+ } catch (\Throwable $e) {
+ $this->json(['error' => $e->getMessage()], 500);
+ }
+ }
+
// ------------------------------------------------------------------- Render
private function renderPage(): void
diff --git a/updater/migrations/0001_feature_ui_tables.sql b/updater/migrations/0001_feature_ui_tables.sql
new file mode 100644
index 0000000..47adb7b
--- /dev/null
+++ b/updater/migrations/0001_feature_ui_tables.sql
@@ -0,0 +1,29 @@
+-- Updater-Migration: Tabellen der UI/Feature-Erweiterung fuer bestehende
+-- Installationen nachziehen (Voucher-Templates + Password-Reset-Tokens).
+-- Idempotent (CREATE TABLE IF NOT EXISTS) – auf frischen Installationen, die
+-- database.sql bereits enthalten, ein No-Op.
+
+CREATE TABLE IF NOT EXISTS `voucher_templates` (
+ `id` INT PRIMARY KEY AUTO_INCREMENT,
+ `name` VARCHAR(255) NOT NULL,
+ `max_uses` INT NOT NULL DEFAULT 1,
+ `expire_minutes` INT NOT NULL DEFAULT 480,
+ `description` VARCHAR(500),
+ `is_active` TINYINT(1) DEFAULT 1,
+ `created_by` INT,
+ `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ FOREIGN KEY (`created_by`) REFERENCES `users`(`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+CREATE TABLE IF NOT EXISTS `password_reset_tokens` (
+ `id` INT PRIMARY KEY AUTO_INCREMENT,
+ `user_id` INT NOT NULL,
+ `token` VARCHAR(128) NOT NULL,
+ `expires_at` TIMESTAMP NOT NULL,
+ `used` TINYINT(1) DEFAULT 0,
+ `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
+ UNIQUE KEY `unique_token` (`token`),
+ INDEX `idx_expires` (`expires_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
diff --git a/updater/templates/update.php b/updater/templates/update.php
index 725cb30..c6fdd9b 100644
--- a/updater/templates/update.php
+++ b/updater/templates/update.php
@@ -133,6 +133,8 @@ $channels = \Updater\UpdateManager::CHANNELS;
Migrations-Status
+
+
@@ -250,10 +252,36 @@ async function loadMigrations() {
'' + m.filename + '' +
'angewandt' : 'badge-off">offen') + '
'
).join('');
+ const pending = d.migrations.some(m => !m.applied);
+ $('btnRunMig').style.display = pending ? 'inline-block' : 'none';
} catch (e) {
el.innerHTML = 'Fehler: ' + e.message + '
';
}
}
+
+$('btnRunMig').addEventListener('click', async () => {
+ $('btnRunMig').disabled = true; $('btnRunMig').textContent = 'Führe aus …';
+ $('migAlert').classList.remove('show');
+ try {
+ const body = new URLSearchParams({ action: 'run_migrations', csrf_token: CSRF });
+ const r = await fetch('update.php', { method: 'POST', body });
+ const d = await r.json();
+ if (d.error) {
+ $('migAlert').textContent = 'Fehler: ' + d.error;
+ $('migAlert').className = 'alert alert-error show';
+ } else {
+ const n = (d.applied || []).length;
+ $('migAlert').textContent = n > 0 ? (n + ' Migration(en) ausgeführt.') : 'Keine ausstehenden Migrationen.';
+ $('migAlert').className = 'alert alert-ok show';
+ loadMigrations();
+ }
+ } catch (e) {
+ $('migAlert').textContent = 'Fehler: ' + e.message;
+ $('migAlert').className = 'alert alert-error show';
+ } finally {
+ $('btnRunMig').disabled = false; $('btnRunMig').textContent = 'Ausstehende Migrationen ausführen';
+ }
+});