Merge pull request #9 from friloo/claude/ecstatic-cannon-H5hGy

Updater-Migration für Feature-Tabellen + README um neue Features erwe…
This commit is contained in:
friloo 2026-06-05 21:27:10 +02:00 committed by GitHub
commit f747a3d429
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 105 additions and 6 deletions

View file

@ -24,13 +24,20 @@
## ✨ Features ## ✨ Features
- 🎟️ **Voucher-Erstellung** mit sofortiger QR-Code-Anzeige, Druckvorlage und E-Mail-Versand - 🎟️ **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 - 🏢 **Multi-Site-Support** beliebig viele UniFi-Standorte zentral verwalten
- 👥 **Benutzerverwaltung** mit granularer Site-Zugriffskontrolle - 👥 **Benutzerverwaltung** mit granularer Site-Zugriffskontrolle
- 🔐 **Authentifizierung** via lokale Accounts **oder** Microsoft 365 OAuth - 🔐 **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) - 🌍 **Ö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 - 📊 **Admin-Dashboard** mit Live-Statistiken und Sync-Funktion
- 📝 **Audit-Log** nachvollziehbare Protokollierung von Login & Änderungen (mit Filter)
- 📥 **CSV-Export** aller Vouchers pro Site - 📥 **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, - 🛡️ **Security-by-default**: CSRF-Schutz, bcrypt-Passwörter, Prepared Statements,
Login-Rate-Limiting, OAuth-State-Validierung, Verschlüsselung sensibler Daten Login-Rate-Limiting, OAuth-State-Validierung, Verschlüsselung sensibler Daten
@ -46,6 +53,13 @@
<img src="docs/screenshots/voucher-result.png" alt="Voucher-Ergebnis" width="32%"> <img src="docs/screenshots/voucher-result.png" alt="Voucher-Ergebnis" width="32%">
</div> </div>
### Bulk-Erstellung & Dark Mode
<div align="center">
<img src="docs/screenshots/bulk-vouchers.png" alt="Bulk-Voucher-Erstellung" width="48%">
<img src="docs/screenshots/admin-dashboard-dark.png" alt="Dashboard im Dark Mode" width="48%">
</div>
### Administration & Updater ### Administration & Updater
<div align="center"> <div align="center">
@ -129,7 +143,9 @@ SSH oder manuelles `git pull`.
geschützten Pfaden (`config.php`, Uploads, …), automatischen DB-Migrationen geschützten Pfaden (`config.php`, Uploads, …), automatischen DB-Migrationen
und OPcache-Reset und OPcache-Reset
- 🔀 **Channel-Auswahl** zwischen `stable` und `development` - 🔀 **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: Während eines Updates wird die Anwendung kurz in den **Wartungsmodus** versetzt:
@ -272,12 +288,15 @@ Body: {"cmd": "delete-voucher", "_id": "<voucher_id>"}
## 🗺️ Roadmap ## 🗺️ Roadmap
- [ ] Voucher-Templates (vordefinierte Laufzeiten) - [x] Voucher-Templates (vordefinierte Laufzeiten)
- [ ] Bulk-Voucher-Erstellung - [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 - [ ] Erweiterte Reporting-Funktionen
- [ ] Docker-Container - [ ] Docker-Container
- [ ] Mehrsprachigkeit
- [x] Auto-Updater mit DB-Migrationen
--- ---

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 KiB

View file

@ -34,6 +34,7 @@ class UpdateController
case 'progress': $this->actionProgress(); break; case 'progress': $this->actionProgress(); break;
case 'set_channel': $this->actionSetChannel(); break; case 'set_channel': $this->actionSetChannel(); break;
case 'migrations': $this->actionMigrations(); break; case 'migrations': $this->actionMigrations(); break;
case 'run_migrations': $this->actionRunMigrations(); break;
default: $this->renderPage(); 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 // ------------------------------------------------------------------- Render
private function renderPage(): void private function renderPage(): void

View file

@ -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;

View file

@ -133,6 +133,8 @@ $channels = \Updater\UpdateManager::CHANNELS;
<div class="card"> <div class="card">
<h2>Migrations-Status</h2> <h2>Migrations-Status</h2>
<div id="migList"><p class="muted">Wird geladen </p></div> <div id="migList"><p class="muted">Wird geladen </p></div>
<button class="btn" id="btnRunMig" style="margin-top:16px; display:none;">Ausstehende Migrationen ausführen</button>
<div class="alert alert-ok" id="migAlert"></div>
</div> </div>
</div> </div>
</div> </div>
@ -250,10 +252,36 @@ async function loadMigrations() {
'<div class="mig-item"><span>' + m.filename + '</span>' + '<div class="mig-item"><span>' + m.filename + '</span>' +
'<span class="badge ' + (m.applied ? 'badge-on">angewandt' : 'badge-off">offen') + '</span></div>' '<span class="badge ' + (m.applied ? 'badge-on">angewandt' : 'badge-off">offen') + '</span></div>'
).join(''); ).join('');
const pending = d.migrations.some(m => !m.applied);
$('btnRunMig').style.display = pending ? 'inline-block' : 'none';
} catch (e) { } catch (e) {
el.innerHTML = '<p class="muted">Fehler: ' + e.message + '</p>'; el.innerHTML = '<p class="muted">Fehler: ' + e.message + '</p>';
} }
} }
$('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';
}
});
</script> </script>
</body> </body>
</html> </html>