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
|
## ✨ 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
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
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 '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
|
||||||
|
|
|
||||||
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">
|
<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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue