Updater-Migration für Feature-Tabellen + README um neue Features erweitern
- updater/migrations/0001_feature_ui_tables.sql: legt voucher_templates und password_reset_tokens idempotent an (CREATE TABLE IF NOT EXISTS), damit bestehende Installationen die neuen Tabellen per System-Update erhalten - Updater-UI: Button "Ausstehende Migrationen ausführen" + run_migrations-Action (CSRF-geschützt) im Migrations-Tab - README: Features (Bulk, Templates, Dark Mode, i18n, Audit-Log, Passwort-Reset), zwei neue Screenshots (Bulk-Erstellung, Dashboard Dark Mode), Roadmap & Updater- Abschnitt aktualisiert
This commit is contained in:
parent
526c43e8ee
commit
51485810b4
6 changed files with 105 additions and 6 deletions
31
Readme.md
31
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 @@
|
|||
<img src="docs/screenshots/voucher-result.png" alt="Voucher-Ergebnis" width="32%">
|
||||
</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
|
||||
|
||||
<div align="center">
|
||||
|
|
@ -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": "<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
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
BIN
docs/screenshots/admin-dashboard-dark.png
Normal file
BIN
docs/screenshots/admin-dashboard-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 201 KiB |
BIN
docs/screenshots/bulk-vouchers.png
Normal file
BIN
docs/screenshots/bulk-vouchers.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 603 KiB |
|
|
@ -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
|
||||
|
|
|
|||
29
updater/migrations/0001_feature_ui_tables.sql
Normal file
29
updater/migrations/0001_feature_ui_tables.sql
Normal 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;
|
||||
|
|
@ -133,6 +133,8 @@ $channels = \Updater\UpdateManager::CHANNELS;
|
|||
<div class="card">
|
||||
<h2>Migrations-Status</h2>
|
||||
<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>
|
||||
|
|
@ -250,10 +252,36 @@ async function loadMigrations() {
|
|||
'<div class="mig-item"><span>' + m.filename + '</span>' +
|
||||
'<span class="badge ' + (m.applied ? 'badge-on">angewandt' : 'badge-off">offen') + '</span></div>'
|
||||
).join('');
|
||||
const pending = d.migrations.some(m => !m.applied);
|
||||
$('btnRunMig').style.display = pending ? 'inline-block' : 'none';
|
||||
} catch (e) {
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue