feat(setup): Erstinstallation ueber den Update-Agent, Installationskonto, Downloads
Bisher gab es nur den Update-Weg: eine Anwendung musste bereits installiert und eingerichtet sein, damit sich etwas aktualisieren liess. Die Erstinstallation auf einem neuen System war Handarbeit - Paket kopieren, Konfiguration abtippen, Token besorgen. Setup-API (neu) - POST /api/setup/v1/login tauscht Benutzername und Passwort gegen ein Token mit 30 Minuten Gueltigkeit und ausschliesslich setup:install. Es wird nicht mitgeschrieben und lebt im Installer nur im Speicher. - GET /api/setup/v1/catalog zeigt nur, was zur Laufzeitkennung des anfragenden Systems passt. Ein Projekt mit ausschliesslich Windows-Paket taucht auf einem Linux-Rechner gar nicht erst auf. - POST /api/setup/v1/token stellt das Dauertoken der Anwendung aus. Welche Rechte vergeben werden, entscheidet der Server; die Anfrage kann nur einschraenken. Sonst waere der Umweg ueber ein kurzlebiges Token wirkungslos. Rollentrennung (Migration 012) - dc_users bekommt role, disabled und last_login_at. Die Rolle "installer" darf sich ueber den Setup-Weg anmelden und nicht am WebUI. Die Zugangsdaten werden auf jedem Zielsystem eingetippt; mit einem Administratorkonto verteilte man damit den Zugang zu Tokens, Lizenzen und Monitoren auf jeden Rechner, auf dem je etwas installiert wurde. - Auth::verifyCredentials() prueft sessionfrei, damit Setup- und WebUI-Login nicht zwei verschiedene Haertungsgrade haben (Drosselung, Timing-Angleichung, Rehash gelten fuer beide). - Konten mit hinterlegtem TOTP-Geheimnis werden am Setup-Weg mit 501 abgewiesen. Eine TOTP-Pruefung gibt es im Deploymentcenter noch nicht; sie stillschweigend zu uebergehen waere ein Rueckschritt. - Benutzerverwaltung im WebUI - es gab bisher gar keine, nur den einen von install_db.php angelegten Admin. Das letzte aktive Administratorkonto laesst sich weder deaktivieren noch loeschen. Installer - update-agent --action install fuehrt durch Anmeldung, Auswahl, Zielverzeichnis, Installation und Einrichtung. Die Dateien kommen ueber denselben Pfad wie ein Update - mit Pruefsumme, Signatur, Staging und Rollback. Ein zweiter Download-Weg waere ein zweiter Ort fuer dieselben Fehler. - --action configure holt die Einrichtung nachtraeglich. - setup.json im Paket beschreibt die benoetigten Werte. Bewusst im Paket und nicht zentral: so ist sie mit der Anwendung versioniert. - Gefragt wird nur, was uebrig bleibt: bereits gesetzt -> detect:... -> provision -> fragen. Platzhalter wie changeme oder <dein-wert> gelten dabei nicht als eingerichtet, sonst liefe die Anwendung mit der Vorlage los. - SetupWriter erhaelt vorhandene Inhalte. Eine appsettings.json fuehrt neben den abgefragten Werten meist Logging und anderes; sie neu zu erzeugen waere bequemer und verloere das - bei einer Neuinstallation ohne Backup. int und bool landen als JSON-Typ, nicht als Zeichenkette. Downloads - scripts/build_installer.ps1 baut selbstenthaltende Einzeldateien fuer win-x64, linux-x64 und linux-arm64 (rund 34 MB, .NET-Laufzeit inbegriffen). Ohne NativeAOT und ohne Trimming: Spectre.Console loest ueber Reflexion auf und braeche sonst erst beim Anwender. - scripts/upload_installer.py laedt sie nach /installer/. Getrennt von deploy.py, das client-dotnet bewusst ausklammert. - Bereich "Installer" auf der UpdateService-Seite mit Groessen, Pruefsummen und den wget-Befehlen; die Angaben stammen aus installer.json statt aus fest eingetragenem Text. - install.sh und install.ps1 laden, pruefen die Pruefsumme und legen ab - sie richten bewusst nichts selbst ein. Das Manifest wird BOM-frei geschrieben, sonst scheitert json_decode() daran. Enthaelt ausserdem die bislang nicht committete Arbeit an den RocketChat-Benachrichtigungen (Migrationen 010 und 011) sowie die Loesch- und Editierfunktion des UpdateService; die betroffenen Dateien liessen sich nicht getrennt stagen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2388b5abe1
commit
c8f3e78635
@@ -224,6 +224,31 @@ function dc_flash(string $message, string $type = 'success'): void
|
||||
$_SESSION['dc_flash'] = ['message' => $message, 'type' => $type];
|
||||
}
|
||||
|
||||
/**
|
||||
* Wie viele handlungsfaehige Administratorkonten gibt es noch?
|
||||
*
|
||||
* Grundlage fuer die Sperren beim Deaktivieren und Loeschen: Faellt das
|
||||
* letzte Konto weg, das sich am WebUI anmelden kann, kommt niemand mehr an
|
||||
* die Oberflaeche - auch nicht, um den Schritt rueckgaengig zu machen.
|
||||
* Installationskonten zaehlen bewusst nicht mit, sie duerfen sich hier gar
|
||||
* nicht anmelden.
|
||||
*/
|
||||
function dc_active_admin_count(PDO $pdo): int
|
||||
{
|
||||
try {
|
||||
$stmt = $pdo->query("SELECT COUNT(*) FROM dc_users WHERE role = 'admin' AND disabled = 0");
|
||||
return (int)$stmt->fetchColumn();
|
||||
} catch (Throwable $e) {
|
||||
// Vor Migration 012 gibt es weder role noch disabled; dann sind alle
|
||||
// Konten Administratoren.
|
||||
try {
|
||||
return (int)$pdo->query('SELECT COUNT(*) FROM dc_users')->fetchColumn();
|
||||
} catch (Throwable $inner) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Post/Redirect/Get: leitet nach einer Aktion auf dieselbe Seite um, damit
|
||||
* ein Neuladen das Formular nicht erneut abschickt.
|
||||
@@ -767,6 +792,122 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
dc_redirect('#tab-updateservice');
|
||||
}
|
||||
|
||||
case 'edit_release': {
|
||||
$releaseId = (int)($_POST['release_id'] ?? 0);
|
||||
if ($releaseId <= 0) {
|
||||
dc_flash('Ungültige Release-ID.', 'danger');
|
||||
dc_redirect('#tab-updateservice');
|
||||
}
|
||||
|
||||
$updateManager = new UpdateManager($pdo);
|
||||
$existing = $updateManager->getReleaseById($releaseId);
|
||||
if ($existing === null) {
|
||||
dc_flash('Das zu bearbeitende Release wurde nicht gefunden.', 'danger');
|
||||
dc_redirect('#tab-updateservice');
|
||||
}
|
||||
|
||||
$productSlug = trim((string)($_POST['product_slug'] ?? ''));
|
||||
$version = trim((string)($_POST['version'] ?? ''));
|
||||
$url = trim((string)($_POST['download_url'] ?? ''));
|
||||
|
||||
if ($productSlug === '' || $version === '' || $url === '') {
|
||||
dc_flash('Projekt, Version und Download-URL sind Pflichtfelder.', 'danger');
|
||||
dc_redirect('#tab-updateservice');
|
||||
}
|
||||
|
||||
$hash = trim((string)($_POST['sha256_hash'] ?? ''));
|
||||
if ($hash !== '' && preg_match('/^[0-9a-f]{64}$/i', $hash) !== 1) {
|
||||
dc_flash('Der SHA256-Hash muss aus genau 64 Hexadezimalzeichen bestehen.', 'danger');
|
||||
dc_redirect('#tab-updateservice');
|
||||
}
|
||||
|
||||
$channel = trim((string)($_POST['channel'] ?? 'prod'));
|
||||
$platform = UpdateManager::normalizePlatform((string)($_POST['platform'] ?? ''));
|
||||
$size = (int)($_POST['size_bytes'] ?? 0);
|
||||
|
||||
$signature = ReleaseSigner::sign(ReleaseSigner::canonical(
|
||||
$productSlug,
|
||||
$version,
|
||||
$channel,
|
||||
$platform,
|
||||
$hash !== '' ? $hash : null,
|
||||
$url,
|
||||
$size
|
||||
));
|
||||
|
||||
try {
|
||||
$ok = $updateManager->updateRelease(
|
||||
$releaseId,
|
||||
$productSlug,
|
||||
$version,
|
||||
$channel,
|
||||
trim((string)($_POST['release_notes'] ?? '')),
|
||||
$url,
|
||||
$hash !== '' ? $hash : null,
|
||||
trim((string)($_POST['git_commit'] ?? '')),
|
||||
$size,
|
||||
$existing['manifest_json'] ?? null,
|
||||
isset($_POST['is_critical']),
|
||||
$actor,
|
||||
$platform,
|
||||
$signature
|
||||
);
|
||||
|
||||
if ($ok) {
|
||||
dc_flash(sprintf(
|
||||
'Release %s (%s, %s) für "%s" erfolgreich aktualisiert.%s',
|
||||
e($version),
|
||||
e($channel),
|
||||
e($platform),
|
||||
e($productSlug),
|
||||
$signature === null
|
||||
? ' Hinweis: unsigniert, kein Signierschlüssel hinterlegt.'
|
||||
: ''
|
||||
));
|
||||
} else {
|
||||
dc_flash('Das Release konnte nicht aktualisiert werden.', 'danger');
|
||||
}
|
||||
} catch (\PDOException $e) {
|
||||
if (str_contains($e->getMessage(), 'uq_prod_ver_chan_plat') || $e->getCode() === '23000') {
|
||||
dc_flash('Ein Release mit dieser Kombination aus Projekt, Version, Kanal und Plattform existiert bereits.', 'danger');
|
||||
} else {
|
||||
dc_flash('Fehler beim Aktualisieren des Releases: ' . $e->getMessage(), 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
dc_redirect('#tab-updateservice');
|
||||
}
|
||||
|
||||
case 'delete_release': {
|
||||
$releaseId = (int)($_POST['release_id'] ?? 0);
|
||||
if ($releaseId <= 0) {
|
||||
dc_flash('Ungültige Release-ID.', 'danger');
|
||||
dc_redirect('#tab-updateservice');
|
||||
}
|
||||
|
||||
$updateManager = new UpdateManager($pdo);
|
||||
$existing = $updateManager->getReleaseById($releaseId);
|
||||
if ($existing === null) {
|
||||
dc_flash('Das zu löschende Release wurde nicht gefunden.', 'danger');
|
||||
dc_redirect('#tab-updateservice');
|
||||
}
|
||||
|
||||
$deleted = $updateManager->deleteRelease($releaseId);
|
||||
if ($deleted) {
|
||||
dc_flash(sprintf(
|
||||
'Release v%s (%s, %s) für "%s" erfolgreich gelöscht.',
|
||||
e($existing['version']),
|
||||
e($existing['channel']),
|
||||
e($existing['platform'] ?? 'any'),
|
||||
e($existing['product_slug'])
|
||||
));
|
||||
} else {
|
||||
dc_flash('Das Release konnte nicht gelöscht werden.', 'danger');
|
||||
}
|
||||
|
||||
dc_redirect('#tab-updateservice');
|
||||
}
|
||||
|
||||
// ------------------------------------------------------ Tokens
|
||||
case 'create_master_token': {
|
||||
$name = trim((string)($_POST['name'] ?? ''));
|
||||
@@ -1025,6 +1166,199 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
dc_redirect('#tab-bugtracker');
|
||||
}
|
||||
|
||||
case 'save_rocketchat_settings': {
|
||||
$enabled = isset($_POST['rocketchat_enabled']) ? '1' : '0';
|
||||
$url = trim((string)($_POST['rocketchat_url'] ?? ''));
|
||||
$username = trim((string)($_POST['rocketchat_username'] ?? ''));
|
||||
$password = (string)($_POST['rocketchat_password'] ?? '');
|
||||
$statusChannel = trim((string)($_POST['rocketchat_status_channel'] ?? ''));
|
||||
$alertChannel = trim((string)($_POST['rocketchat_alert_channel'] ?? ''));
|
||||
$verifySsl = isset($_POST['rocketchat_verify_ssl']) ? '1' : '0';
|
||||
$reportInterval = max(300, (int)($_POST['rocketchat_report_interval'] ?? 43200));
|
||||
|
||||
$settings = [
|
||||
'rocketchat_enabled' => $enabled,
|
||||
'rocketchat_url' => $url,
|
||||
'rocketchat_username' => $username,
|
||||
'rocketchat_password' => $password,
|
||||
'rocketchat_status_channel' => $statusChannel,
|
||||
'rocketchat_alert_channel' => $alertChannel,
|
||||
'rocketchat_verify_ssl' => $verifySsl,
|
||||
'rocketchat_report_interval' => (string)$reportInterval,
|
||||
];
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO dc_settings (skey, svalue) VALUES (:k, :v)
|
||||
ON DUPLICATE KEY UPDATE svalue = VALUES(svalue)
|
||||
");
|
||||
foreach ($settings as $k => $v) {
|
||||
$stmt->execute([':k' => $k, ':v' => $v]);
|
||||
}
|
||||
|
||||
$stmtCron = $pdo->prepare("
|
||||
INSERT INTO watchdog_cron_jobs (name, interval_sec, enabled)
|
||||
VALUES ('rocketchat_status_report', :interval, :enabled)
|
||||
ON DUPLICATE KEY UPDATE interval_sec = VALUES(interval_sec), enabled = VALUES(enabled)
|
||||
");
|
||||
$stmtCron->execute([':interval' => $reportInterval, ':enabled' => (int)$enabled]);
|
||||
|
||||
dc_flash('Rocket.Chat Einstellungen erfolgreich gespeichert.', 'success');
|
||||
dc_redirect('#sub-system-rocketchat');
|
||||
}
|
||||
|
||||
case 'test_rocketchat_connection': {
|
||||
$config = \Deploymentcenter\Modules\Notify\RocketChatNotifier::getConfig($pdo);
|
||||
$res = \Deploymentcenter\Modules\Notify\RocketChatNotifier::testConnection($config);
|
||||
dc_flash($res['message'], $res['success'] ? 'success' : 'danger');
|
||||
dc_redirect('#sub-system-rocketchat');
|
||||
}
|
||||
|
||||
case 'send_rocketchat_report_now': {
|
||||
$res = \Deploymentcenter\Modules\Notify\RocketChatNotifier::sendStatusReport($pdo, true);
|
||||
dc_flash($res['reason'], $res['sent'] ? 'success' : 'warning');
|
||||
dc_redirect('#sub-system-rocketchat');
|
||||
}
|
||||
|
||||
// ------------------------------------------------- Benutzerkonten
|
||||
case 'add_user': {
|
||||
$newUser = trim((string)($_POST['username'] ?? ''));
|
||||
$newPass = (string)($_POST['password'] ?? '');
|
||||
$newRole = ($_POST['role'] ?? 'installer') === 'admin' ? 'admin' : 'installer';
|
||||
|
||||
if (preg_match('/^[A-Za-z0-9._-]{3,64}$/', $newUser) !== 1) {
|
||||
dc_flash('Der Benutzername muss 3 bis 64 Zeichen lang sein und darf Buchstaben, Ziffern, Punkt, Bindestrich und Unterstrich enthalten.', 'danger');
|
||||
dc_redirect('#sub-system-users');
|
||||
}
|
||||
|
||||
// Ein Installationskonto wird auf fremden Rechnern eingegeben.
|
||||
// Ein kurzes Passwort waere dort schnell mitgelesen und liesse
|
||||
// sich ebenso schnell durchprobieren.
|
||||
if (strlen($newPass) < 12) {
|
||||
dc_flash('Das Passwort muss mindestens 12 Zeichen lang sein.', 'danger');
|
||||
dc_redirect('#sub-system-users');
|
||||
}
|
||||
|
||||
$exists = $pdo->prepare('SELECT id FROM dc_users WHERE username = :u LIMIT 1');
|
||||
$exists->execute([':u' => $newUser]);
|
||||
if ($exists->fetch() !== false) {
|
||||
dc_flash(sprintf('Der Benutzername "%s" ist bereits vergeben.', e($newUser)), 'danger');
|
||||
dc_redirect('#sub-system-users');
|
||||
}
|
||||
|
||||
$ins = $pdo->prepare('
|
||||
INSERT INTO dc_users (username, password_hash, role, created_at)
|
||||
VALUES (:u, :h, :r, UTC_TIMESTAMP())
|
||||
');
|
||||
$ins->execute([
|
||||
':u' => $newUser,
|
||||
':h' => password_hash($newPass, PASSWORD_DEFAULT),
|
||||
':r' => $newRole,
|
||||
]);
|
||||
|
||||
Logger::info('Benutzerkonto angelegt', [
|
||||
'username' => $newUser,
|
||||
'role' => $newRole,
|
||||
'actor' => $actor,
|
||||
]);
|
||||
|
||||
dc_flash(sprintf(
|
||||
'Konto "%s" mit der Rolle %s angelegt.%s',
|
||||
e($newUser),
|
||||
e($newRole),
|
||||
$newRole === 'installer'
|
||||
? ' Es kann sich nicht am WebUI anmelden, sondern nur Anwendungen einrichten.'
|
||||
: ''
|
||||
));
|
||||
dc_redirect('#sub-system-users');
|
||||
}
|
||||
|
||||
case 'set_user_password': {
|
||||
$userId = (int)($_POST['user_id'] ?? 0);
|
||||
$newPass = (string)($_POST['password'] ?? '');
|
||||
|
||||
if (strlen($newPass) < 12) {
|
||||
dc_flash('Das Passwort muss mindestens 12 Zeichen lang sein.', 'danger');
|
||||
dc_redirect('#sub-system-users');
|
||||
}
|
||||
|
||||
$upd = $pdo->prepare('UPDATE dc_users SET password_hash = :h WHERE id = :id');
|
||||
$upd->execute([':h' => password_hash($newPass, PASSWORD_DEFAULT), ':id' => $userId]);
|
||||
|
||||
Logger::info('Passwort geaendert', ['user_id' => $userId, 'actor' => $actor]);
|
||||
dc_flash($upd->rowCount() > 0 ? 'Passwort geaendert.' : 'Konto nicht gefunden.',
|
||||
$upd->rowCount() > 0 ? 'success' : 'danger');
|
||||
dc_redirect('#sub-system-users');
|
||||
}
|
||||
|
||||
case 'toggle_user_disabled': {
|
||||
$userId = (int)($_POST['user_id'] ?? 0);
|
||||
|
||||
$row = $pdo->prepare('SELECT id, username, role, disabled FROM dc_users WHERE id = :id LIMIT 1');
|
||||
$row->execute([':id' => $userId]);
|
||||
$target = $row->fetch();
|
||||
|
||||
if (!is_array($target)) {
|
||||
dc_flash('Konto nicht gefunden.', 'danger');
|
||||
dc_redirect('#sub-system-users');
|
||||
}
|
||||
|
||||
$disable = (int)$target['disabled'] === 0;
|
||||
|
||||
// Ohne diese Sperre liesse sich das letzte handlungsfaehige
|
||||
// Administratorkonto abschalten - danach kaeme niemand mehr an
|
||||
// die Oberflaeche, auch nicht, um es rueckgaengig zu machen.
|
||||
if ($disable && $target['role'] === 'admin' && dc_active_admin_count($pdo) <= 1) {
|
||||
dc_flash('Das letzte aktive Administratorkonto kann nicht deaktiviert werden.', 'danger');
|
||||
dc_redirect('#sub-system-users');
|
||||
}
|
||||
|
||||
$upd = $pdo->prepare('UPDATE dc_users SET disabled = :d WHERE id = :id');
|
||||
$upd->execute([':d' => $disable ? 1 : 0, ':id' => $userId]);
|
||||
|
||||
Logger::info('Konto ' . ($disable ? 'deaktiviert' : 'aktiviert'), [
|
||||
'username' => $target['username'],
|
||||
'actor' => $actor,
|
||||
]);
|
||||
|
||||
dc_flash(sprintf('Konto "%s" wurde %s.', e((string)$target['username']),
|
||||
$disable ? 'deaktiviert' : 'wieder aktiviert'));
|
||||
dc_redirect('#sub-system-users');
|
||||
}
|
||||
|
||||
case 'delete_user': {
|
||||
$userId = (int)($_POST['user_id'] ?? 0);
|
||||
|
||||
$row = $pdo->prepare('SELECT id, username, role FROM dc_users WHERE id = :id LIMIT 1');
|
||||
$row->execute([':id' => $userId]);
|
||||
$target = $row->fetch();
|
||||
|
||||
if (!is_array($target)) {
|
||||
dc_flash('Konto nicht gefunden.', 'danger');
|
||||
dc_redirect('#sub-system-users');
|
||||
}
|
||||
|
||||
if ($userId === Auth::userId()) {
|
||||
dc_flash('Das eigene Konto kann nicht geloescht werden.', 'danger');
|
||||
dc_redirect('#sub-system-users');
|
||||
}
|
||||
|
||||
if ($target['role'] === 'admin' && dc_active_admin_count($pdo) <= 1) {
|
||||
dc_flash('Das letzte Administratorkonto kann nicht geloescht werden.', 'danger');
|
||||
dc_redirect('#sub-system-users');
|
||||
}
|
||||
|
||||
$del = $pdo->prepare('DELETE FROM dc_users WHERE id = :id');
|
||||
$del->execute([':id' => $userId]);
|
||||
|
||||
Logger::info('Benutzerkonto geloescht', [
|
||||
'username' => $target['username'],
|
||||
'actor' => $actor,
|
||||
]);
|
||||
|
||||
dc_flash(sprintf('Konto "%s" wurde geloescht.', e((string)$target['username'])));
|
||||
dc_redirect('#sub-system-users');
|
||||
}
|
||||
|
||||
default:
|
||||
dc_flash('Unbekannte Aktion.', 'danger');
|
||||
dc_redirect();
|
||||
@@ -1271,6 +1605,39 @@ $recentEvents = $eventLog->getRecentEvents(100);
|
||||
$updateManager = new UpdateManager($pdo);
|
||||
$releases = $updateManager->getReleases();
|
||||
|
||||
// Benutzerkonten fuer die Verwaltung. Der Passwort-Hash wird bewusst nicht
|
||||
// mitgelesen - er hat in der Ansicht nichts verloren.
|
||||
try {
|
||||
$users = $pdo->query('
|
||||
SELECT id, username, role, disabled, last_login_at, created_at
|
||||
FROM dc_users ORDER BY role ASC, username ASC
|
||||
')->fetchAll() ?: [];
|
||||
} catch (Throwable $e) {
|
||||
// Vor Migration 012 fehlen role, disabled und last_login_at.
|
||||
$users = $pdo->query('SELECT id, username, created_at FROM dc_users ORDER BY username ASC')->fetchAll() ?: [];
|
||||
}
|
||||
$usersSchemaReady = $users === [] || array_key_exists('role', $users[0]);
|
||||
|
||||
// Installer-Downloads. Die Binaries liegen unter /installer/ im Webroot und
|
||||
// werden nicht von deploy.py uebertragen, sondern von
|
||||
// scripts/upload_installer.py - der Quelltext des Agenten gehoert nicht auf
|
||||
// den Webserver, die uebersetzten Dateien schon.
|
||||
$installerManifest = null;
|
||||
$installerPath = dirname(__DIR__) . '/installer/installer.json';
|
||||
|
||||
if (is_file($installerPath)) {
|
||||
$raw = (string)file_get_contents($installerPath);
|
||||
|
||||
// Ein vorangestelltes BOM laesst json_decode() scheitern. Windows-Werkzeuge
|
||||
// schreiben es gern mit; die Datei ist deswegen nicht kaputt.
|
||||
$raw = preg_replace('/^\xEF\xBB\xBF/', '', $raw) ?? $raw;
|
||||
|
||||
$decoded = json_decode($raw, true);
|
||||
if (is_array($decoded) && isset($decoded['binaries']) && is_array($decoded['binaries'])) {
|
||||
$installerManifest = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
$coreTokenManager = new CoreTokenManager($pdo);
|
||||
$coreMasterTokens = $coreTokenManager->getAllMasterTokens();
|
||||
$coreAllTokens = $coreTokenManager->getAllTokens();
|
||||
@@ -1391,6 +1758,10 @@ $evaluatorLastRun = is_array($evaluatorJob) ? ($evaluatorJob['last_run_utc'] ??
|
||||
$evaluatorStale = $evaluatorLastRun === null
|
||||
|| (time() - (int)strtotime((string)$evaluatorLastRun . ' UTC')) > 900;
|
||||
|
||||
// Rocket.Chat Konfiguration und Cron-Status
|
||||
$rcConfig = \Deploymentcenter\Modules\Notify\RocketChatNotifier::getConfig($pdo);
|
||||
$rcCronJob = $pdo->query("SELECT * FROM watchdog_cron_jobs WHERE name = 'rocketchat_status_report'")->fetch() ?: [];
|
||||
|
||||
$baseUrl = Http::baseUrl();
|
||||
$csrfField = Csrf::field();
|
||||
?>
|
||||
@@ -2625,6 +2996,7 @@ Antwort:
|
||||
<th>Release Notes</th>
|
||||
<th>Download & SHA256</th>
|
||||
<th>Datum</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -2664,6 +3036,10 @@ Antwort:
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($r['created_at']) ?></td>
|
||||
<td style="white-space:nowrap;">
|
||||
<button type="button" class="btn btn-sm btn-secondary" onclick="openEditReleaseModal(<?= htmlspecialchars(json_encode($r, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP)) ?>)" title="Release bearbeiten">✏️ Bearbeiten</button>
|
||||
<button type="button" class="btn btn-sm btn-danger" onclick="openDeleteReleaseModal(<?= (int)$r['id'] ?>, '<?= htmlspecialchars(addslashes($r['version'])) ?>', '<?= htmlspecialchars(addslashes($r['product_slug'])) ?>')" title="Release löschen">🗑️ Löschen</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
@@ -2671,6 +3047,110 @@ Antwort:
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sub-update-installer" class="subtab-content">
|
||||
<div class="card">
|
||||
<div class="card-header"><h2 class="card-title">⬇️ Installer herunterladen</h2></div>
|
||||
|
||||
<p style="color:var(--text-muted); font-size:0.875rem; margin-bottom:1.25rem;">
|
||||
Ein Werkzeug für alles: Dasselbe Programm richtet eine Anwendung erstmalig
|
||||
ein und aktualisiert sie später. Es bringt die .NET-Laufzeit mit — auf dem
|
||||
Zielsystem muss nichts vorinstalliert sein.
|
||||
</p>
|
||||
|
||||
<?php if ($installerManifest === null): ?>
|
||||
<div class="alert" style="background:rgba(255,180,0,0.12); border:1px solid rgba(255,180,0,0.3); color:#fde68a; font-weight:500;">
|
||||
<strong>Noch nichts hinterlegt.</strong> Die Binaries werden mit
|
||||
<code>scripts/build_installer.ps1</code> gebaut und mit
|
||||
<code>scripts/upload_installer.py</code> nach <code>/installer/</code>
|
||||
hochgeladen.
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div style="font-size:0.8rem; color:var(--text-muted); margin-bottom:1rem;">
|
||||
Version <strong><?= e((string)($installerManifest['version'] ?? '?')) ?></strong>
|
||||
· Build <code><?= e((string)($installerManifest['gitCommit'] ?? '?')) ?></code>
|
||||
· <?= e(substr((string)($installerManifest['buildDateUtc'] ?? ''), 0, 10)) ?>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Plattform</th>
|
||||
<th>Größe</th>
|
||||
<th>SHA256</th>
|
||||
<th>Download</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($installerManifest['binaries'] as $bin): ?>
|
||||
<?php
|
||||
$binFile = (string)($bin['file'] ?? '');
|
||||
$binHash = (string)($bin['sha256'] ?? '');
|
||||
$binUrl = $baseUrl . '/installer/' . rawurlencode($binFile);
|
||||
?>
|
||||
<tr>
|
||||
<td><code><?= e((string)($bin['platform'] ?? '?')) ?></code></td>
|
||||
<td style="font-family:'Roboto Mono',monospace; font-size:0.8rem;">
|
||||
<?= number_format(((int)($bin['sizeBytes'] ?? 0)) / (1024 * 1024), 1, ',', '.') ?> MB
|
||||
</td>
|
||||
<td>
|
||||
<code style="font-size:0.65rem; color:var(--text-muted); word-break:break-all;" title="<?= e($binHash) ?>">
|
||||
<?= e(substr($binHash, 0, 24)) ?>…
|
||||
</code>
|
||||
</td>
|
||||
<td>
|
||||
<a href="<?= e($binUrl) ?>" class="btn btn-sm">⬇️ Herunterladen</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="alert" style="background:rgba(91,157,255,0.12); border:1px solid rgba(91,157,255,0.3); color:#bfdbfe; margin-top:1rem;">
|
||||
<strong>Prüfsumme vergleichen.</strong> „Programm herunterladen und
|
||||
Zugangsdaten eingeben“ ist genau das Muster, das Phishing nachahmt.
|
||||
Die vollständigen Hashes stehen im Tooltip und unter
|
||||
<a href="<?= e($baseUrl) ?>/installer/installer.json" target="_blank" rel="noopener" style="color:#bfdbfe;">installer.json</a>.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2 class="card-title">Direkt auf dem Zielsystem</h2></div>
|
||||
|
||||
<p style="color:var(--text-muted); font-size:0.875rem; margin-bottom:0.75rem;">
|
||||
<strong>Linux</strong> — lädt das passende Binary, prüft die Prüfsumme und legt es ab:
|
||||
</p>
|
||||
<div class="prompt-box">wget -qO- <?= e($baseUrl) ?>/installer/install.sh | sh</div>
|
||||
|
||||
<p style="color:var(--text-muted); font-size:0.875rem; margin:1rem 0 0.75rem;">
|
||||
Oder ohne Skript, direkt das Binary:
|
||||
</p>
|
||||
<div class="prompt-box">wget <?= e($baseUrl) ?>/installer/update-agent-linux-x64 -O update-agent
|
||||
chmod +x update-agent
|
||||
./update-agent --action install</div>
|
||||
|
||||
<p style="color:var(--text-muted); font-size:0.875rem; margin:1rem 0 0.75rem;">
|
||||
<strong>Windows</strong> (PowerShell):
|
||||
</p>
|
||||
<div class="prompt-box">irm <?= e($baseUrl) ?>/installer/install.ps1 | iex</div>
|
||||
|
||||
<p style="color:var(--text-muted); font-size:0.8rem; margin-top:1rem;">
|
||||
Die Skripte richten <strong>nichts</strong> von selbst ein — sie laden, prüfen
|
||||
und sagen, wie es weitergeht. Ein Skript aus dem Netz, das ungefragt eine
|
||||
Anwendung aufsetzt und dabei nach Zugangsdaten fragt, wäre genau das, wovor
|
||||
man Nutzer sonst warnt.
|
||||
</p>
|
||||
|
||||
<p style="color:var(--text-muted); font-size:0.8rem; margin-top:0.75rem;">
|
||||
Für die Installation wird ein Konto der Rolle <strong>installer</strong>
|
||||
gebraucht — anzulegen unter <em>System → 👤 Benutzer</em>. Ein
|
||||
Administratorkonto funktioniert auch, gehört aber nicht auf ein Zielsystem:
|
||||
seine Zugangsdaten würden damit auf jedem Rechner landen, auf dem je etwas
|
||||
installiert wurde.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sub-update-publish" class="subtab-content">
|
||||
<div class="card">
|
||||
<div class="card-header"><h2 class="card-title">Neues Software-Release veröffentlichen</h2></div>
|
||||
@@ -2740,6 +3220,106 @@ Antwort:
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Release Modal -->
|
||||
<div class="modal-backdrop" id="editReleaseModal">
|
||||
<div class="modal-card">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1.5rem;">
|
||||
<h3 style="margin:0;">✏️ Release bearbeiten</h3>
|
||||
<button type="button" onclick="closeEditReleaseModal()" style="background:none; border:none; color:var(--text-muted); font-size:1.25rem; cursor:pointer;">×</button>
|
||||
</div>
|
||||
<form method="POST" action="index.php#sub-update-releases">
|
||||
<?= $csrfField ?>
|
||||
<input type="hidden" name="action" value="edit_release">
|
||||
<input type="hidden" name="release_id" id="edit_release_id" value="">
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Projekt-Slug</label>
|
||||
<select name="product_slug" id="edit_release_product_slug" class="form-input" required>
|
||||
<?php foreach ($projects as $p): ?>
|
||||
<option value="<?= htmlspecialchars($p['slug']) ?>"><?= htmlspecialchars($p['name']) ?> (<?= htmlspecialchars($p['slug']) ?>)</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Release Kanal</label>
|
||||
<select name="channel" id="edit_release_channel" class="form-input" required>
|
||||
<option value="prod">prod (Produktiv)</option>
|
||||
<option value="beta">beta (Vorab-Test)</option>
|
||||
<option value="dev">dev (Entwicklung)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Plattform</label>
|
||||
<select name="platform" id="edit_release_platform" class="form-input">
|
||||
<option value="any">any (plattformunabhängig)</option>
|
||||
<option value="win-x64">win-x64</option>
|
||||
<option value="win-arm64">win-arm64</option>
|
||||
<option value="linux-x64">linux-x64</option>
|
||||
<option value="linux-arm64">linux-arm64</option>
|
||||
<option value="linux-musl-x64">linux-musl-x64</option>
|
||||
<option value="osx-x64">osx-x64</option>
|
||||
<option value="osx-arm64">osx-arm64</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Version (z. B. 1.2.0)</label>
|
||||
<input type="text" name="version" id="edit_release_version" class="form-input" required placeholder="1.2.0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Git Commit (Kurz-Hash)</label>
|
||||
<input type="text" name="git_commit" id="edit_release_git_commit" class="form-input" placeholder="a1b2c3d">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Download URL</label>
|
||||
<input type="url" name="download_url" id="edit_release_download_url" class="form-input" required placeholder="https://...">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">SHA256 Hash (Optional)</label>
|
||||
<input type="text" name="sha256_hash" id="edit_release_sha256_hash" class="form-input" placeholder="e3b0c44298fc1c149afbf4c8996fb924...">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Größe (Bytes)</label>
|
||||
<input type="number" name="size_bytes" id="edit_release_size_bytes" class="form-input" min="0" placeholder="0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:1rem;">
|
||||
<label class="form-label">Release Notes / Changelog</label>
|
||||
<textarea name="release_notes" id="edit_release_release_notes" class="form-input" rows="3" placeholder="Changelog..."></textarea>
|
||||
</div>
|
||||
<div style="margin-bottom:1.25rem;">
|
||||
<label style="display:inline-flex; align-items:center; gap:0.5rem; font-size:0.875rem; cursor:pointer;">
|
||||
<input type="checkbox" name="is_critical" id="edit_release_is_critical" value="1"> Kritisches Sicherheits-Update (Rollout priorisieren)
|
||||
</label>
|
||||
</div>
|
||||
<div style="display:flex; justify-content:flex-end; gap:0.75rem;">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeEditReleaseModal()">Abbrechen</button>
|
||||
<button type="submit" class="btn">Änderungen Speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Release Modal -->
|
||||
<div class="modal-backdrop" id="deleteReleaseModal">
|
||||
<div class="modal-card" style="max-width:480px;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1.5rem;">
|
||||
<h3 style="margin:0; color:#ef4444;">🗑️ Release löschen</h3>
|
||||
<button type="button" onclick="closeDeleteReleaseModal()" style="background:none; border:none; color:var(--text-muted); font-size:1.25rem; cursor:pointer;">×</button>
|
||||
</div>
|
||||
<p>Möchten Sie das Release <strong id="delete_release_version_display"></strong> für <strong id="delete_release_product_display"></strong> wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.</p>
|
||||
<form method="POST" action="index.php#sub-update-releases" style="margin-top:1.5rem;">
|
||||
<?= $csrfField ?>
|
||||
<input type="hidden" name="action" value="delete_release">
|
||||
<input type="hidden" name="release_id" id="delete_release_id" value="">
|
||||
<div style="display:flex; justify-content:flex-end; gap:0.75rem;">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeDeleteReleaseModal()">Abbrechen</button>
|
||||
<button type="submit" class="btn btn-danger">Release unwiderruflich löschen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================= MODULE: BUGTRACKER & FEATURE-TRACKER ================= -->
|
||||
@@ -3853,6 +4433,229 @@ Antwort:
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sub-system-rocketchat" class="subtab-content">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">💬 Rocket.Chat Benachrichtigungen & Statusberichte</h2>
|
||||
</div>
|
||||
<p style="color:var(--text-muted); font-size:0.875rem; margin-bottom:1.5rem;">
|
||||
Hier konfigurieren Sie die automatischen Rocket.Chat-Benachrichtigungen. Alle 12 Stunden wird ein detaillierter Systemstatusbericht an den Status-Kanal gesendet. Kritische Systemausfälle und Bugs landen sofort im Alarm-Kanal.
|
||||
</p>
|
||||
|
||||
<form method="POST">
|
||||
<?= $csrfField ?>
|
||||
<input type="hidden" name="action" value="save_rocketchat_settings">
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-group" style="grid-column: span 2;">
|
||||
<label class="form-label" style="display:flex; align-items:center; gap:0.5rem; cursor:pointer;">
|
||||
<input type="checkbox" name="rocketchat_enabled" value="1" <?= !empty($rcConfig['enabled']) ? 'checked' : '' ?>>
|
||||
<strong>Rocket.Chat Benachrichtigungen aktivieren</strong>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Rocket.Chat Server-URL</label>
|
||||
<input type="url" name="rocketchat_url" class="form-input" required value="<?= e($rcConfig['url']) ?>" placeholder="https://chat.wh1.mhdf.de">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Benutzername</label>
|
||||
<input type="text" name="rocketchat_username" class="form-input" required value="<?= e($rcConfig['username']) ?>" placeholder="deploymentcenter">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Passwort</label>
|
||||
<input type="password" name="rocketchat_password" class="form-input" required value="<?= e($rcConfig['password']) ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">SSL-Zertifikat verifizieren</label>
|
||||
<label class="form-label" style="display:flex; align-items:center; gap:0.5rem; cursor:pointer; font-weight:normal; margin-top:0.3rem;">
|
||||
<input type="checkbox" name="rocketchat_verify_ssl" value="1" <?= !empty($rcConfig['verify_ssl']) ? 'checked' : '' ?>>
|
||||
SSL-Zertifikat prüfen (bei selbstsignierten/unsicheren Zertifikaten deaktivieren)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Status-Kanal (12h Bericht)</label>
|
||||
<input type="text" name="rocketchat_status_channel" class="form-input" required value="<?= e($rcConfig['status_channel']) ?>" placeholder="#DC-Systemstatus">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Alarm-Kanal (Kritische Ausfälle & Bugs)</label>
|
||||
<input type="text" name="rocketchat_alert_channel" class="form-input" required value="<?= e($rcConfig['alert_channel']) ?>" placeholder="#DC-Alerts">
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="grid-column: span 2;">
|
||||
<label class="form-label">Statusbericht Intervall (Sekunden)</label>
|
||||
<input type="number" name="rocketchat_report_interval" class="form-input" min="300" step="60" value="<?= e((string)$rcConfig['report_interval']) ?>">
|
||||
<span style="font-size:0.75rem; color:var(--text-muted);">Standard: 43200 Sekunden (12 Stunden)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:1.5rem; display:flex; gap:1rem;">
|
||||
<button type="submit" class="btn btn-primary">💾 Einstellungen Speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:1.5rem;">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">🧪 Aktionen & Testlauf</h2>
|
||||
</div>
|
||||
<p style="color:var(--text-muted); font-size:0.875rem; margin-bottom:1.5rem;">
|
||||
Hier können Sie die Erreichbarkeit Ihres Rocket.Chat-Servers testen oder sofort einen 12-Stunden-Statusbericht auslösen.
|
||||
</p>
|
||||
|
||||
<div style="display:flex; flex-wrap:wrap; gap:1rem;">
|
||||
<form method="POST">
|
||||
<?= $csrfField ?>
|
||||
<input type="hidden" name="action" value="test_rocketchat_connection">
|
||||
<button type="submit" class="btn btn-secondary">🧪 Verbindung & Kanäle Testen</button>
|
||||
</form>
|
||||
|
||||
<form method="POST">
|
||||
<?= $csrfField ?>
|
||||
<input type="hidden" name="action" value="send_rocketchat_report_now">
|
||||
<button type="submit" class="btn btn-secondary">📊 12h Statusbericht Jetzt Senden</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:1.5rem; background:rgba(0,0,0,0.2); padding:1rem; border-radius:8px; font-size:0.85rem; color:var(--text-muted);">
|
||||
<strong>📌 Status des automatischen 12h Cron-Jobs:</strong><br>
|
||||
• Letzter Bericht: <?= !empty($rcCronJob['last_run_utc']) ? e(dc_dt((string)$rcCronJob['last_run_utc'])) . ' (' . e(dc_ago((string)$rcCronJob['last_run_utc'])) . ')' : 'Noch nie gelaufen' ?><br>
|
||||
• Letzter Status: <span class="badge badge-<?= ($rcCronJob['last_status'] ?? '') === 'ok' ? 'up' : 'warning' ?>"><?= e((string)($rcCronJob['last_status'] ?? 'bereit')) ?></span><br>
|
||||
• Nächster geplanter Lauf: In ca. <?= !empty($rcCronJob['last_run_utc']) ? max(0, (int)round(($rcConfig['report_interval'] - (time() - strtotime((string)$rcCronJob['last_run_utc'] . ' UTC'))) / 3600)) . ' Std.' : 'sofort beim nächsten Tick' ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sub-system-users" class="subtab-content">
|
||||
<div class="card">
|
||||
<div class="card-header"><h2 class="card-title">👤 Benutzerkonten</h2></div>
|
||||
|
||||
<?php if (!$usersSchemaReady): ?>
|
||||
<div class="alert" style="background:rgba(255,180,0,0.12); border:1px solid rgba(255,180,0,0.3); color:#fde68a; font-weight:500;">
|
||||
<strong>Migration 012 fehlt.</strong> Rollen, Deaktivierung und der Zeitpunkt
|
||||
der letzten Anmeldung stehen erst danach zur Verfügung. Bis dahin gelten alle
|
||||
Konten als Administratoren.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<p style="color:var(--text-muted); font-size:0.875rem; margin-bottom:1.25rem;">
|
||||
Für die Erstinstallation von Anwendungen gibt es die Rolle
|
||||
<strong>installer</strong>. Solche Konten melden sich ausschließlich über
|
||||
<code>/api/setup/v1/login</code> an und bekommen dafür ein Token, das nach
|
||||
30 Minuten verfällt. <strong>Am WebUI können sie sich nicht anmelden.</strong>
|
||||
Der Grund: Die Zugangsdaten werden auf jedem Zielsystem eingegeben — mit einem
|
||||
Administratorkonto verteilte man damit den Zugang zu Tokens, Lizenzen und
|
||||
Monitoren auf jeden Rechner, auf dem je etwas installiert wurde.
|
||||
</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Benutzer</th>
|
||||
<th>Rolle</th>
|
||||
<th>Status</th>
|
||||
<th>Letzte Anmeldung</th>
|
||||
<th>Angelegt</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($users as $u): ?>
|
||||
<?php
|
||||
$uRole = (string)($u['role'] ?? 'admin');
|
||||
$uDisabled = (int)($u['disabled'] ?? 0) === 1;
|
||||
$isSelf = (int)$u['id'] === Auth::userId();
|
||||
?>
|
||||
<tr<?= $uDisabled ? ' style="opacity:0.55;"' : '' ?>>
|
||||
<td>
|
||||
<strong><?= e((string)$u['username']) ?></strong>
|
||||
<?php if ($isSelf): ?><span class="badge badge-up">Sie</span><?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge <?= $uRole === 'admin' ? 'badge-up' : 'badge-warning' ?>">
|
||||
<?= e(strtoupper($uRole)) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td><?= $uDisabled ? '<span class="badge badge-down">DEAKTIVIERT</span>' : 'aktiv' ?></td>
|
||||
<td style="font-size:0.8rem; color:var(--text-muted);">
|
||||
<?= !empty($u['last_login_at']) ? e((string)$u['last_login_at']) : 'nie' ?>
|
||||
</td>
|
||||
<td style="font-size:0.8rem; color:var(--text-muted);"><?= e((string)($u['created_at'] ?? '-')) ?></td>
|
||||
<td>
|
||||
<div style="display:flex; flex-wrap:wrap; gap:0.35rem;">
|
||||
<form method="POST" action="index.php" style="display:inline-flex; gap:0.25rem;"
|
||||
onsubmit="return confirm('Passwort für <?= e((string)$u['username']) ?> wirklich ändern?');">
|
||||
<?= $csrfField ?>
|
||||
<input type="hidden" name="action" value="set_user_password">
|
||||
<input type="hidden" name="_return" value="sub-system-users">
|
||||
<input type="hidden" name="user_id" value="<?= (int)$u['id'] ?>">
|
||||
<input type="password" name="password" class="form-input" required minlength="12"
|
||||
placeholder="Neues Passwort" style="width:170px; padding:0.3rem 0.5rem; font-size:0.8rem;">
|
||||
<button type="submit" class="btn btn-sm btn-secondary">Setzen</button>
|
||||
</form>
|
||||
|
||||
<?php if (!$isSelf): ?>
|
||||
<form method="POST" action="index.php" style="display:inline;">
|
||||
<?= $csrfField ?>
|
||||
<input type="hidden" name="action" value="toggle_user_disabled">
|
||||
<input type="hidden" name="_return" value="sub-system-users">
|
||||
<input type="hidden" name="user_id" value="<?= (int)$u['id'] ?>">
|
||||
<button type="submit" class="btn btn-sm btn-secondary">
|
||||
<?= $uDisabled ? 'Aktivieren' : 'Deaktivieren' ?>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="index.php" style="display:inline;"
|
||||
onsubmit="return confirm('Konto <?= e((string)$u['username']) ?> endgültig löschen?');">
|
||||
<?= $csrfField ?>
|
||||
<input type="hidden" name="action" value="delete_user">
|
||||
<input type="hidden" name="_return" value="sub-system-users">
|
||||
<input type="hidden" name="user_id" value="<?= (int)$u['id'] ?>">
|
||||
<button type="submit" class="btn btn-sm btn-danger">Löschen</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2 class="card-title">Neues Konto anlegen</h2></div>
|
||||
<form method="POST" action="index.php">
|
||||
<?= $csrfField ?>
|
||||
<input type="hidden" name="action" value="add_user">
|
||||
<input type="hidden" name="_return" value="sub-system-users">
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Benutzername</label>
|
||||
<input type="text" name="username" class="form-input" required
|
||||
pattern="[A-Za-z0-9._\-]{3,64}" placeholder="installer">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Rolle</label>
|
||||
<select name="role" class="form-input">
|
||||
<option value="installer" selected>installer (nur Erstinstallation)</option>
|
||||
<option value="admin">admin (volle Verwaltung)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Passwort (mindestens 12 Zeichen)</label>
|
||||
<input type="password" name="password" class="form-input" required minlength="12">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn">Konto anlegen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sub-system-swagger" class="subtab-content">
|
||||
<div class="card">
|
||||
<div class="card-header"><h2 class="card-title">📖 API-Dokumentation</h2></div>
|
||||
@@ -3944,6 +4747,7 @@ SYSTEM
|
||||
],
|
||||
'updateservice': [
|
||||
{ id: 'sub-update-releases', label: '📊 Releases Overview', active: true },
|
||||
{ id: 'sub-update-installer', label: '⬇️ Installer' },
|
||||
{ id: 'sub-update-publish', label: '➕ Release Veröffentlichen' }
|
||||
],
|
||||
'bugtracker': [
|
||||
@@ -3959,6 +4763,8 @@ SYSTEM
|
||||
],
|
||||
'system': [
|
||||
{ id: 'sub-system-status', label: '⚙️ System-Status', active: true },
|
||||
{ id: 'sub-system-users', label: '👤 Benutzer' },
|
||||
{ id: 'sub-system-rocketchat', label: '💬 Rocket.Chat' },
|
||||
{ id: 'sub-system-swagger', label: '📖 API Swagger Docs' },
|
||||
{ id: 'sub-system-migration', label: '🗄️ DB Migration' }
|
||||
]
|
||||
@@ -4373,6 +5179,55 @@ SYSTEM
|
||||
document.getElementById('projSubmitBtn').innerText = 'Projekt Anlegen';
|
||||
}
|
||||
|
||||
// UpdateService Release Modal Functions
|
||||
function openEditReleaseModal(r) {
|
||||
const setVal = (id, val) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.value = val;
|
||||
};
|
||||
|
||||
setVal('edit_release_id', r.id || 0);
|
||||
setVal('edit_release_product_slug', r.product_slug || '');
|
||||
setVal('edit_release_channel', r.channel || 'prod');
|
||||
setVal('edit_release_platform', r.platform || 'any');
|
||||
setVal('edit_release_version', r.version || '');
|
||||
setVal('edit_release_git_commit', r.git_commit || '');
|
||||
setVal('edit_release_download_url', r.download_url || '');
|
||||
setVal('edit_release_sha256_hash', r.sha256_hash || '');
|
||||
setVal('edit_release_size_bytes', r.size_bytes || 0);
|
||||
setVal('edit_release_release_notes', r.release_notes || '');
|
||||
|
||||
const critEl = document.getElementById('edit_release_is_critical');
|
||||
if (critEl) critEl.checked = Boolean(Number(r.is_critical));
|
||||
|
||||
const modal = document.getElementById('editReleaseModal');
|
||||
if (modal) modal.classList.add('active');
|
||||
}
|
||||
|
||||
function closeEditReleaseModal() {
|
||||
const modal = document.getElementById('editReleaseModal');
|
||||
if (modal) modal.classList.remove('active');
|
||||
}
|
||||
|
||||
function openDeleteReleaseModal(id, version, product) {
|
||||
const idEl = document.getElementById('delete_release_id');
|
||||
if (idEl) idEl.value = id;
|
||||
|
||||
const verEl = document.getElementById('delete_release_version_display');
|
||||
if (verEl) verEl.textContent = 'v' + version;
|
||||
|
||||
const prodEl = document.getElementById('delete_release_product_display');
|
||||
if (prodEl) prodEl.textContent = product;
|
||||
|
||||
const modal = document.getElementById('deleteReleaseModal');
|
||||
if (modal) modal.classList.add('active');
|
||||
}
|
||||
|
||||
function closeDeleteReleaseModal() {
|
||||
const modal = document.getElementById('deleteReleaseModal');
|
||||
if (modal) modal.classList.remove('active');
|
||||
}
|
||||
|
||||
// Project Safety Deletion Modal
|
||||
function openDeleteProjectModal(id, slug, name) {
|
||||
document.getElementById('deleteProjId').value = id;
|
||||
|
||||
Reference in New Issue
Block a user