deploy
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
use App\OidcClient;
|
||||
|
||||
$config = app()->config();
|
||||
$session = app()->session();
|
||||
$session->start();
|
||||
|
||||
if (!$config->authEnabled) {
|
||||
echo '<div class="card">Auth ist deaktiviert.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
$code = (string)($_GET['code'] ?? '');
|
||||
$state = (string)($_GET['state'] ?? '');
|
||||
$expectedState = (string)($_SESSION['oidc_state'] ?? '');
|
||||
$nonce = (string)($_SESSION['oidc_nonce'] ?? '');
|
||||
|
||||
if ($code === '' || $state === '' || $expectedState === '' || !hash_equals($expectedState, $state)) {
|
||||
echo '<div class="card">Ungültiger Login-Status.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
unset($_SESSION['oidc_state']);
|
||||
|
||||
$client = new OidcClient($config);
|
||||
$token = $client->exchangeCode($code);
|
||||
|
||||
$idToken = (string)($token['id_token'] ?? '');
|
||||
$accessToken = (string)($token['access_token'] ?? '');
|
||||
if ($idToken === '') {
|
||||
echo '<div class="card">Kein ID Token erhalten.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
$claims = $client->decodeJwt($idToken);
|
||||
$client->validateIdToken($claims, $nonce);
|
||||
unset($_SESSION['oidc_nonce']);
|
||||
|
||||
$groups = $client->groupsFromClaims($claims);
|
||||
$accessClaims = null;
|
||||
if (!$groups && $accessToken !== '') {
|
||||
try {
|
||||
$accessClaims = $client->decodeJwt($accessToken);
|
||||
$groups = $client->groupsFromClaims($accessClaims);
|
||||
} catch (\Throwable $e) {
|
||||
// ignore access token decoding errors
|
||||
}
|
||||
}
|
||||
$user = [
|
||||
'sub' => (string)($claims['sub'] ?? ''),
|
||||
'email' => (string)($claims['email'] ?? ''),
|
||||
'name' => (string)($claims['name'] ?? ($claims['preferred_username'] ?? '')),
|
||||
'username' => (string)($claims['preferred_username'] ?? $claims['email'] ?? $claims['sub'] ?? ''),
|
||||
'groups' => $groups,
|
||||
'id_token' => $idToken,
|
||||
];
|
||||
|
||||
app()->auth()->storeUser($claims, $groups, $idToken);
|
||||
|
||||
if (defined('APP_AUTH_DEBUG') && APP_AUTH_DEBUG) {
|
||||
$log = [
|
||||
'ts' => date('c'),
|
||||
'sub' => $user['sub'],
|
||||
'email' => $user['email'],
|
||||
'name' => $user['name'],
|
||||
'groups' => $groups,
|
||||
'id_token_claims' => $claims,
|
||||
'access_token_claims' => $accessToken ? ($accessClaims ?? null) : null,
|
||||
'token_meta' => [
|
||||
'has_id_token' => $idToken !== '',
|
||||
'has_access_token' => $accessToken !== '',
|
||||
'expires_in' => $token['expires_in'] ?? null,
|
||||
'refresh_expires_in' => $token['refresh_expires_in'] ?? null,
|
||||
'scope' => $token['scope'] ?? null,
|
||||
],
|
||||
'claim_source' => !empty($groups) ? 'id_token_or_access_token' : 'none',
|
||||
];
|
||||
@file_put_contents(__DIR__ . '/../../../debug/oidc_login.log', json_encode($log) . PHP_EOL, FILE_APPEND);
|
||||
}
|
||||
|
||||
$returnTo = (string)($_SESSION['oidc_return_to'] ?? '/');
|
||||
unset($_SESSION['oidc_return_to']);
|
||||
redirect($returnTo !== '' && str_starts_with($returnTo, '/') && !str_starts_with($returnTo, '//') ? $returnTo : '/');
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
use App\OidcClient;
|
||||
|
||||
$config = app()->config();
|
||||
if (!$config->authEnabled) {
|
||||
echo '<div class="card">Auth ist deaktiviert.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
$session = app()->session();
|
||||
$session->start();
|
||||
|
||||
$state = bin2hex(random_bytes(16));
|
||||
$nonce = bin2hex(random_bytes(16));
|
||||
$_SESSION['oidc_state'] = $state;
|
||||
$_SESSION['oidc_nonce'] = $nonce;
|
||||
|
||||
$client = new OidcClient($config);
|
||||
redirect($client->authUrl($state, $nonce));
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
use App\OidcClient;
|
||||
|
||||
$config = app()->config();
|
||||
$session = app()->session();
|
||||
$session->start();
|
||||
|
||||
$idToken = null;
|
||||
if (!empty($_SESSION['auth_user']['id_token'])) {
|
||||
$idToken = (string)$_SESSION['auth_user']['id_token'];
|
||||
}
|
||||
|
||||
unset($_SESSION['auth_user'], $_SESSION['auth_id_token'], $_SESSION['auth_expires_at']);
|
||||
|
||||
if ($config->authEnabled) {
|
||||
$client = new OidcClient($config);
|
||||
$url = $client->logoutUrl($idToken);
|
||||
if ($url) {
|
||||
redirect($url);
|
||||
}
|
||||
}
|
||||
|
||||
redirect('/');
|
||||
@@ -0,0 +1,347 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_auth();
|
||||
|
||||
$service = dashboards();
|
||||
$ownerKey = auth_user_key();
|
||||
$groups = auth_groups();
|
||||
$notice = null;
|
||||
$error = null;
|
||||
|
||||
if (!$service->available() || $ownerKey === '') {
|
||||
echo '<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack"><section class="section-box">Dashboard-System nicht verfügbar.</section></div></div></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = trim((string) ($_POST['action'] ?? ''));
|
||||
try {
|
||||
if ($action === 'add_item') {
|
||||
$dashboardId = (int) ($_POST['dashboard_id'] ?? 0);
|
||||
$itemType = trim((string) ($_POST['item_type'] ?? 'link'));
|
||||
$config = [];
|
||||
if ($itemType === 'page_module') {
|
||||
$config['page_module_id'] = (int) ($_POST['page_module_id'] ?? 0);
|
||||
} elseif ($itemType === 'app') {
|
||||
$config['app_id'] = (int) ($_POST['app_id'] ?? 0);
|
||||
} elseif ($itemType === 'widget_template') {
|
||||
$config['widget_template_id'] = (int) ($_POST['widget_template_id'] ?? 0);
|
||||
} elseif ($itemType === 'bookmark_group') {
|
||||
$config['bookmarks'] = trim((string) ($_POST['bookmarks'] ?? ''));
|
||||
} else {
|
||||
$config['url'] = trim((string) ($_POST['target_url'] ?? ''));
|
||||
}
|
||||
$service->createItem($dashboardId, $ownerKey, [
|
||||
'item_type' => $itemType,
|
||||
'title' => trim((string) ($_POST['title'] ?? '')),
|
||||
'description' => trim((string) ($_POST['description'] ?? '')),
|
||||
'grid_column' => trim((string) ($_POST['grid_column'] ?? '')),
|
||||
'grid_row' => trim((string) ($_POST['grid_row'] ?? '')),
|
||||
'column_span' => (int) ($_POST['column_span'] ?? 1),
|
||||
'row_span' => (int) ($_POST['row_span'] ?? 1),
|
||||
'config' => $config,
|
||||
]);
|
||||
$notice = 'Dashboard-Element hinzugefügt.';
|
||||
} elseif ($action === 'delete_item') {
|
||||
$service->deleteItem((int) ($_POST['item_id'] ?? 0), (int) ($_POST['dashboard_id'] ?? 0), $ownerKey);
|
||||
$notice = 'Dashboard-Element entfernt.';
|
||||
}
|
||||
} catch (\Throwable $exception) {
|
||||
$error = $exception->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$accessibleDashboards = $service->listAccessibleDashboards($ownerKey, $groups);
|
||||
$selectedDashboardId = (int) ($_GET['id'] ?? 0);
|
||||
$currentDashboard = null;
|
||||
foreach ($accessibleDashboards as $dashboard) {
|
||||
if ((int) ($dashboard['id'] ?? 0) === $selectedDashboardId) {
|
||||
$currentDashboard = $dashboard;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($currentDashboard === null) {
|
||||
$currentDashboard = $service->ensureDefaultDashboard($ownerKey, 'Mein Dashboard');
|
||||
}
|
||||
$currentDashboardId = (int) ($currentDashboard['id'] ?? 0);
|
||||
$dashboardItems = $service->listItems($currentDashboardId);
|
||||
$ownPageModules = $service->listPageModulesForOwner($ownerKey);
|
||||
$availableApps = $service->listApps($ownerKey, true);
|
||||
$availableWidgetTemplates = $service->listWidgetTemplates($ownerKey, true);
|
||||
$pageModuleMap = [];
|
||||
foreach ($ownPageModules as $pageModule) {
|
||||
$pageModuleMap[(int) ($pageModule['id'] ?? 0)] = $pageModule;
|
||||
}
|
||||
$appMap = [];
|
||||
foreach ($availableApps as $appEntry) {
|
||||
$appMap[(int) ($appEntry['id'] ?? 0)] = $appEntry;
|
||||
}
|
||||
$widgetTemplateMap = [];
|
||||
foreach ($availableWidgetTemplates as $template) {
|
||||
$widgetTemplateMap[(int) ($template['id'] ?? 0)] = $template;
|
||||
}
|
||||
|
||||
$GLOBALS['layout_header_base_title'] = 'Nexus';
|
||||
$GLOBALS['layout_header_title'] = 'Nexus';
|
||||
$GLOBALS['layout_header_context'] = 'Dashboard';
|
||||
$GLOBALS['layout_header_text'] = 'Persönliche Arbeitsfläche mit frei platzierbaren Dashboard-Elementen.';
|
||||
?>
|
||||
<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack">
|
||||
<header class="module-hero submenu-box">
|
||||
<div class="module-hero-top module-hero-top--compact">
|
||||
<nav class="module-tabs" aria-label="Dashboard Navigation">
|
||||
<a class="module-button module-button--tab-active" href="/dashboard">Dashboard</a>
|
||||
<a class="module-button module-button--tab" href="/dashboards">Dashboards</a>
|
||||
<a class="module-button module-button--tab" href="/integrations">Integrationen</a>
|
||||
<a class="module-button module-button--tab" href="/page-modules">Seitenmodule</a>
|
||||
<?php if (auth_is_admin()): ?>
|
||||
<a class="module-button module-button--tab" href="/modules">Aktive Module</a>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
<div class="module-hero-actions module-submenu-actions">
|
||||
<a class="module-button module-button--secondary module-button--small" href="/">Nexus Übersicht</a>
|
||||
<a class="module-button module-button--secondary module-button--small" href="/settings">Nexus Einstellungen</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<?php if ($error !== null): ?>
|
||||
<section class="section-box"><?= e($error) ?></section>
|
||||
<?php elseif ($notice !== null): ?>
|
||||
<section class="section-box"><?= e($notice) ?></section>
|
||||
<?php endif; ?>
|
||||
|
||||
<section class="section-box">
|
||||
<h2><?= e((string) ($currentDashboard['title'] ?? 'Dashboard')) ?></h2>
|
||||
<p class="muted"><?= e((string) ($currentDashboard['description'] ?? 'Dein aktuelles Standard-Dashboard.')) ?></p>
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Aktives Dashboard</span>
|
||||
<select onchange="if (this.value) window.location.href='/dashboard?id=' + this.value;">
|
||||
<?php foreach ($accessibleDashboards as $dashboard): ?>
|
||||
<option value="<?= (int) ($dashboard['id'] ?? 0) ?>" <?= (int) ($dashboard['id'] ?? 0) === $currentDashboardId ? 'selected' : '' ?>>
|
||||
<?= e((string) ($dashboard['title'] ?? 'Dashboard')) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-box">
|
||||
<h2>Element hinzufügen</h2>
|
||||
<p class="muted">Verfügbar sind direkte Links, iFrames, persönliche Linklisten, globale Apps, Seitenmodule und wiederverwendbare Widgets.</p>
|
||||
<form method="post" class="setup-form">
|
||||
<input type="hidden" name="action" value="add_item">
|
||||
<input type="hidden" name="dashboard_id" value="<?= $currentDashboardId ?>">
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Titel</span>
|
||||
<input type="text" name="title" required>
|
||||
</label>
|
||||
<label class="setup-field muted">
|
||||
<span>Typ</span>
|
||||
<select name="item_type" data-dashboard-item-type>
|
||||
<option value="link">Link</option>
|
||||
<option value="iframe">iFrame</option>
|
||||
<option value="bookmark_group">Linkliste</option>
|
||||
<option value="app">App</option>
|
||||
<option value="page_module">Seitenmodul</option>
|
||||
<option value="widget_template">Widget-Vorlage</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Beschreibung</span>
|
||||
<input type="text" name="description">
|
||||
</label>
|
||||
<label class="setup-field muted" data-dashboard-target-url>
|
||||
<span>Ziel-URL</span>
|
||||
<input type="url" name="target_url" placeholder="https://...">
|
||||
</label>
|
||||
<label class="setup-field muted" data-dashboard-bookmarks hidden>
|
||||
<span>Linkliste</span>
|
||||
<textarea name="bookmarks" rows="5" placeholder="Bezeichnung | https://ziel.example Nexus | /dashboard"></textarea>
|
||||
</label>
|
||||
<label class="setup-field muted" data-dashboard-app hidden>
|
||||
<span>App</span>
|
||||
<select name="app_id">
|
||||
<option value="0">Bitte wählen</option>
|
||||
<?php foreach ($availableApps as $appEntry): ?>
|
||||
<option value="<?= (int) ($appEntry['id'] ?? 0) ?>"><?= e((string) ($appEntry['name'] ?? 'App')) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<label class="setup-field muted" data-dashboard-page-module hidden>
|
||||
<span>Seitenmodul</span>
|
||||
<select name="page_module_id">
|
||||
<option value="0">Bitte wählen</option>
|
||||
<?php foreach ($ownPageModules as $pageModule): ?>
|
||||
<option value="<?= (int) ($pageModule['id'] ?? 0) ?>"><?= e((string) ($pageModule['title'] ?? 'Seitenmodul')) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<label class="setup-field muted" data-dashboard-widget-template hidden>
|
||||
<span>Widget-Vorlage</span>
|
||||
<select name="widget_template_id">
|
||||
<option value="0">Bitte wählen</option>
|
||||
<?php foreach ($availableWidgetTemplates as $template): ?>
|
||||
<option value="<?= (int) ($template['id'] ?? 0) ?>"><?= e((string) ($template['name'] ?? 'Widget')) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Spaltenbreite</span>
|
||||
<select name="column_span">
|
||||
<option value="1">1</option>
|
||||
<option value="2" selected>2</option>
|
||||
<option value="3">3</option>
|
||||
<option value="4">4</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="setup-field muted">
|
||||
<span>Zeilenhöhe</span>
|
||||
<select name="row_span">
|
||||
<option value="1" selected>1</option>
|
||||
<option value="2">2</option>
|
||||
<option value="3">3</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="setup-field muted">
|
||||
<span>Grid-Spalte optional</span>
|
||||
<input type="number" name="grid_column" min="1" max="4">
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-actions setup-actions--footer">
|
||||
<button class="cta-button" type="submit">Element speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<?php if ($dashboardItems === []): ?>
|
||||
<section class="section-box dashboard-empty">Noch keine Elemente vorhanden.</section>
|
||||
<?php else: ?>
|
||||
<div class="dashboard-grid">
|
||||
<?php foreach ($dashboardItems as $item): ?>
|
||||
<?php
|
||||
$itemType = (string) ($item['item_type'] ?? 'link');
|
||||
$config = is_array($item['config'] ?? null) ? $item['config'] : [];
|
||||
if ($itemType === 'widget_template' && !empty($config['widget_template_id']) && isset($widgetTemplateMap[(int) $config['widget_template_id']])) {
|
||||
$template = $widgetTemplateMap[(int) $config['widget_template_id']];
|
||||
$itemType = (string) ($template['widget_type'] ?? $itemType);
|
||||
$config = array_merge(is_array($template['config'] ?? null) ? $template['config'] : [], $config);
|
||||
}
|
||||
$columnSpan = max(1, min(4, (int) ($item['column_span'] ?? 1)));
|
||||
$rowSpan = max(1, min(4, (int) ($item['row_span'] ?? 1)));
|
||||
$gridStyles = 'grid-column: span ' . $columnSpan . '; grid-row: span ' . $rowSpan . ';';
|
||||
if (!empty($item['grid_column'])) {
|
||||
$gridStyles .= 'grid-column-start:' . (int) $item['grid_column'] . ';';
|
||||
}
|
||||
if (!empty($item['grid_row'])) {
|
||||
$gridStyles .= 'grid-row-start:' . (int) $item['grid_row'] . ';';
|
||||
}
|
||||
$pageModule = null;
|
||||
if ($itemType === 'page_module' && !empty($config['page_module_id'])) {
|
||||
$pageModule = $pageModuleMap[(int) $config['page_module_id']] ?? null;
|
||||
}
|
||||
$appEntry = null;
|
||||
if ($itemType === 'app' && !empty($config['app_id'])) {
|
||||
$appEntry = $appMap[(int) $config['app_id']] ?? null;
|
||||
}
|
||||
$targetUrl = trim((string) ($config['url'] ?? ''));
|
||||
if ($pageModule !== null) {
|
||||
$targetUrl = trim((string) ($pageModule['target_url'] ?? $targetUrl));
|
||||
}
|
||||
if ($appEntry !== null) {
|
||||
$targetUrl = trim((string) ($appEntry['app_url'] ?? $targetUrl));
|
||||
}
|
||||
$bookmarks = [];
|
||||
if ($itemType === 'bookmark_group') {
|
||||
foreach (preg_split('/\r\n|\r|\n/', trim((string) ($config['bookmarks'] ?? ''))) ?: [] as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
[$label, $url] = array_pad(array_map('trim', explode('|', $line, 2)), 2, '');
|
||||
if ($label !== '' && $url !== '') {
|
||||
$bookmarks[] = ['label' => $label, 'url' => $url];
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<article class="card-box dashboard-widget" style="<?= e($gridStyles) ?>">
|
||||
<div class="dashboard-widget__head">
|
||||
<div>
|
||||
<span class="module-admin-meta__label"><?= e(strtoupper($itemType)) ?></span>
|
||||
<h2><?= e((string) ($item['title'] ?? 'Element')) ?></h2>
|
||||
<?php if (!empty($item['description'])): ?>
|
||||
<p><?= e((string) $item['description']) ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="delete_item">
|
||||
<input type="hidden" name="dashboard_id" value="<?= $currentDashboardId ?>">
|
||||
<input type="hidden" name="item_id" value="<?= (int) ($item['id'] ?? 0) ?>">
|
||||
<button class="module-button module-button--secondary module-button--small" type="submit">Entfernen</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php if ($itemType === 'bookmark_group' && $bookmarks !== []): ?>
|
||||
<div class="dashboard-links">
|
||||
<?php foreach ($bookmarks as $bookmark): ?>
|
||||
<a class="module-button module-button--secondary module-button--small" href="<?= e($bookmark['url']) ?>" target="_blank" rel="noreferrer"><?= e($bookmark['label']) ?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php elseif (($itemType === 'iframe' || ($pageModule !== null && (string) ($pageModule['module_type'] ?? '') === 'iframe')) && $targetUrl !== ''): ?>
|
||||
<iframe class="dashboard-widget__frame" src="<?= e($targetUrl) ?>" loading="lazy" referrerpolicy="no-referrer"></iframe>
|
||||
<?php elseif ($appEntry !== null): ?>
|
||||
<div class="dashboard-widget__meta">
|
||||
<?php if (!empty($appEntry['icon_url'])): ?>
|
||||
<img class="dashboard-app-icon" src="<?= e((string) $appEntry['icon_url']) ?>" alt="">
|
||||
<?php endif; ?>
|
||||
<p><?= e((string) ($appEntry['description'] ?? $targetUrl)) ?></p>
|
||||
<a class="module-button module-button--secondary module-button--small" href="<?= e($targetUrl) ?>" target="_blank" rel="noreferrer">App öffnen</a>
|
||||
</div>
|
||||
<?php elseif ($pageModule !== null): ?>
|
||||
<div class="dashboard-widget__meta">
|
||||
<p><?= e((string) ($pageModule['description'] ?? 'Seitenmodul aus der globalen Nexus-Verwaltung.')) ?></p>
|
||||
<a class="module-button module-button--secondary module-button--small" href="/page-modules/view/<?= (int) ($pageModule['id'] ?? 0) ?>">Öffnen</a>
|
||||
</div>
|
||||
<?php elseif ($targetUrl !== ''): ?>
|
||||
<div class="dashboard-widget__meta">
|
||||
<p><?= e($targetUrl) ?></p>
|
||||
<a class="module-button module-button--secondary module-button--small" href="<?= e($targetUrl) ?>" target="_blank" rel="noreferrer">Öffnen</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="dashboard-widget__meta"><p>Für dieses Element ist noch kein Inhalt hinterlegt.</p></div>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div></div></div>
|
||||
<script>
|
||||
(() => {
|
||||
const typeSelect = document.querySelector('[data-dashboard-item-type]');
|
||||
const urlField = document.querySelector('[data-dashboard-target-url]');
|
||||
const pageModuleField = document.querySelector('[data-dashboard-page-module]');
|
||||
const bookmarksField = document.querySelector('[data-dashboard-bookmarks]');
|
||||
const appField = document.querySelector('[data-dashboard-app]');
|
||||
const widgetTemplateField = document.querySelector('[data-dashboard-widget-template]');
|
||||
if (!typeSelect || !urlField || !pageModuleField || !bookmarksField || !appField || !widgetTemplateField) return;
|
||||
const sync = () => {
|
||||
const value = typeSelect.value;
|
||||
urlField.hidden = !['link', 'iframe'].includes(value);
|
||||
pageModuleField.hidden = value !== 'page_module';
|
||||
bookmarksField.hidden = value !== 'bookmark_group';
|
||||
appField.hidden = value !== 'app';
|
||||
widgetTemplateField.hidden = value !== 'widget_template';
|
||||
};
|
||||
typeSelect.addEventListener('change', sync);
|
||||
sync();
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_auth();
|
||||
|
||||
$service = dashboards();
|
||||
$ownerKey = auth_user_key();
|
||||
$notice = null;
|
||||
$error = null;
|
||||
|
||||
if (!$service->available() || $ownerKey === '') {
|
||||
echo '<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack"><section class="section-box">Dashboard-System nicht verfügbar.</section></div></div></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = trim((string) ($_POST['action'] ?? ''));
|
||||
try {
|
||||
if ($action === 'create_dashboard') {
|
||||
$service->createDashboard($ownerKey, [
|
||||
'title' => trim((string) ($_POST['title'] ?? '')),
|
||||
'slug' => trim((string) ($_POST['slug'] ?? '')),
|
||||
'description' => trim((string) ($_POST['description'] ?? '')),
|
||||
'visibility' => trim((string) ($_POST['visibility'] ?? 'private')),
|
||||
'is_default' => isset($_POST['is_default']),
|
||||
]);
|
||||
$notice = 'Dashboard angelegt.';
|
||||
} elseif ($action === 'set_default') {
|
||||
$service->setDefaultDashboard($ownerKey, (int) ($_POST['dashboard_id'] ?? 0));
|
||||
$notice = 'Standard-Dashboard gesetzt.';
|
||||
} elseif ($action === 'delete_dashboard') {
|
||||
$service->deleteDashboard((int) ($_POST['dashboard_id'] ?? 0), $ownerKey);
|
||||
$notice = 'Dashboard gelöscht.';
|
||||
}
|
||||
} catch (\Throwable $exception) {
|
||||
$error = $exception->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$dashboardsList = $service->listDashboardsForOwner($ownerKey);
|
||||
|
||||
$GLOBALS['layout_header_base_title'] = 'Nexus';
|
||||
$GLOBALS['layout_header_title'] = 'Nexus';
|
||||
$GLOBALS['layout_header_context'] = 'Dashboards';
|
||||
$GLOBALS['layout_header_text'] = 'Verwaltung persönlicher und später freigebbarer Dashboard-Flächen.';
|
||||
?>
|
||||
<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack">
|
||||
<header class="module-hero submenu-box">
|
||||
<div class="module-hero-top module-hero-top--compact">
|
||||
<nav class="module-tabs" aria-label="Dashboard Verwaltung">
|
||||
<a class="module-button module-button--tab" href="/dashboard">Dashboard</a>
|
||||
<a class="module-button module-button--tab-active" href="/dashboards">Dashboards</a>
|
||||
<a class="module-button module-button--tab" href="/integrations">Integrationen</a>
|
||||
<a class="module-button module-button--tab" href="/page-modules">Seitenmodule</a>
|
||||
</nav>
|
||||
<div class="module-hero-actions module-submenu-actions">
|
||||
<a class="module-button module-button--secondary module-button--small" href="/">Nexus Übersicht</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<?php if ($error !== null): ?>
|
||||
<section class="section-box"><?= e($error) ?></section>
|
||||
<?php elseif ($notice !== null): ?>
|
||||
<section class="section-box"><?= e($notice) ?></section>
|
||||
<?php endif; ?>
|
||||
|
||||
<section class="section-box">
|
||||
<h2>Neues Dashboard</h2>
|
||||
<form method="post" class="setup-form">
|
||||
<input type="hidden" name="action" value="create_dashboard">
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Titel</span>
|
||||
<input type="text" name="title" required>
|
||||
</label>
|
||||
<label class="setup-field muted">
|
||||
<span>Slug optional</span>
|
||||
<input type="text" name="slug">
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Beschreibung</span>
|
||||
<input type="text" name="description">
|
||||
</label>
|
||||
<label class="setup-field muted">
|
||||
<span>Sichtbarkeit</span>
|
||||
<select name="visibility">
|
||||
<option value="private">Privat</option>
|
||||
<option value="public">Öffentlich</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<label class="setup-field muted">
|
||||
<input type="checkbox" name="is_default" value="1">
|
||||
<span>Als Standard-Dashboard setzen</span>
|
||||
</label>
|
||||
<div class="setup-actions setup-actions--footer">
|
||||
<button class="cta-button" type="submit">Dashboard anlegen</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div class="module-admin-grid">
|
||||
<?php foreach ($dashboardsList as $dashboard): ?>
|
||||
<article class="card-box module-admin-card">
|
||||
<div class="module-admin-card__head">
|
||||
<div class="module-admin-card__title">
|
||||
<h2><?= e((string) ($dashboard['title'] ?? 'Dashboard')) ?></h2>
|
||||
<p><?= e((string) ($dashboard['description'] ?? 'Persönliche Dashboard-Fläche.')) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-admin-meta">
|
||||
<div class="module-admin-meta__item">
|
||||
<span class="module-admin-meta__label">Sichtbarkeit</span>
|
||||
<strong class="module-admin-badge"><?= e(ucfirst((string) ($dashboard['visibility'] ?? 'private'))) ?></strong>
|
||||
</div>
|
||||
<div class="module-admin-meta__item">
|
||||
<span class="module-admin-meta__label">Standard</span>
|
||||
<strong class="module-admin-badge<?= !empty($dashboard['is_default']) ? ' module-admin-badge--success' : '' ?>"><?= !empty($dashboard['is_default']) ? 'Ja' : 'Nein' ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-admin-actions">
|
||||
<a class="module-button module-button--secondary module-button--small" href="/dashboard?id=<?= (int) ($dashboard['id'] ?? 0) ?>">Öffnen</a>
|
||||
<?php if (empty($dashboard['is_default'])): ?>
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="set_default">
|
||||
<input type="hidden" name="dashboard_id" value="<?= (int) ($dashboard['id'] ?? 0) ?>">
|
||||
<button class="module-button module-button--secondary module-button--small" type="submit">Als Standard</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="delete_dashboard">
|
||||
<input type="hidden" name="dashboard_id" value="<?= (int) ($dashboard['id'] ?? 0) ?>">
|
||||
<button class="module-button module-button--secondary module-button--small" type="submit">Löschen</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div></div></div>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
http_response_code(404);
|
||||
?>
|
||||
<div class="card">
|
||||
<div class="pill">404</div>
|
||||
<h1 style="margin-top:.75rem;">Seite nicht gefunden</h1>
|
||||
<p class="muted">Die angeforderte Seite existiert nicht oder wurde verschoben.</p>
|
||||
<div style="margin-top:1rem;">
|
||||
<a class="nav-link" href="/">Zur Startseite</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
$auth = app()->auth();
|
||||
$authUser = $auth->user();
|
||||
$service = dashboards();
|
||||
$ownerKey = auth_user_key();
|
||||
$groups = auth_groups();
|
||||
$selectedDashboardId = (int) ($_GET['board'] ?? 0);
|
||||
$currentDashboard = $service->available()
|
||||
? $service->resolveHomeDashboard($ownerKey !== '' ? $ownerKey : null, $groups, $authUser !== null, $selectedDashboardId)
|
||||
: null;
|
||||
$dashboardItems = $currentDashboard !== null ? $service->listItems((int) ($currentDashboard['id'] ?? 0)) : [];
|
||||
$accessibleApps = ($authUser !== null && $service->available()) ? $service->listApps($ownerKey, true) : [];
|
||||
$appsById = [];
|
||||
foreach ($accessibleApps as $appEntry) {
|
||||
$appsById[(int) ($appEntry['id'] ?? 0)] = $appEntry;
|
||||
}
|
||||
$widgetTemplates = ($authUser !== null && $service->available()) ? $service->listWidgetTemplates($ownerKey, true) : [];
|
||||
$widgetTemplatesById = [];
|
||||
foreach ($widgetTemplates as $template) {
|
||||
$widgetTemplatesById[(int) ($template['id'] ?? 0)] = $template;
|
||||
}
|
||||
|
||||
$GLOBALS['layout_header_base_title'] = 'Nexus';
|
||||
$GLOBALS['layout_header_title'] = 'Nexus';
|
||||
$GLOBALS['layout_header_context'] = $currentDashboard !== null ? (string) ($currentDashboard['title'] ?? 'Dashboard') : 'Dashboard';
|
||||
$GLOBALS['layout_header_text'] = $currentDashboard !== null
|
||||
? (string) ($currentDashboard['description'] ?? '')
|
||||
: 'Kein öffentliches oder persönliches Home-Dashboard verfügbar.';
|
||||
|
||||
$renderBookmarks = static function (array $config): array {
|
||||
$items = [];
|
||||
$raw = trim((string) ($config['bookmarks'] ?? ''));
|
||||
if ($raw === '') {
|
||||
return [];
|
||||
}
|
||||
foreach (preg_split('/\r\n|\r|\n/', $raw) ?: [] as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
[$label, $url] = array_pad(array_map('trim', explode('|', $line, 2)), 2, '');
|
||||
if ($label !== '' && $url !== '') {
|
||||
$items[] = ['label' => $label, 'url' => $url];
|
||||
}
|
||||
}
|
||||
return $items;
|
||||
};
|
||||
?>
|
||||
<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack">
|
||||
<?php if ($currentDashboard === null): ?>
|
||||
<section class="section-box">
|
||||
<h2>Kein Home-Dashboard verfügbar</h2>
|
||||
<p class="muted">Lege als Administrator ein öffentliches Dashboard fest oder richte dein persönliches Dashboard ein.</p>
|
||||
<?php if ($authUser !== null): ?>
|
||||
<a class="module-button module-button--secondary module-button--small" href="/dashboard">Zum Dashboard</a>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
<?php elseif ($dashboardItems === []): ?>
|
||||
<section class="section-box">
|
||||
<h2><?= e((string) ($currentDashboard['title'] ?? 'Dashboard')) ?></h2>
|
||||
<p class="muted">Dieses Dashboard enthält noch keine Elemente.</p>
|
||||
<?php if ($authUser !== null): ?>
|
||||
<a class="module-button module-button--secondary module-button--small" href="/dashboard?id=<?= (int) ($currentDashboard['id'] ?? 0) ?>">Dashboard bearbeiten</a>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
<?php else: ?>
|
||||
<div class="dashboard-grid">
|
||||
<?php foreach ($dashboardItems as $item): ?>
|
||||
<?php
|
||||
$itemType = (string) ($item['item_type'] ?? 'link');
|
||||
$config = is_array($item['config'] ?? null) ? $item['config'] : [];
|
||||
if ($itemType === 'widget_template' && !empty($config['widget_template_id']) && isset($widgetTemplatesById[(int) $config['widget_template_id']])) {
|
||||
$template = $widgetTemplatesById[(int) $config['widget_template_id']];
|
||||
$itemType = (string) ($template['widget_type'] ?? $itemType);
|
||||
$config = array_merge(is_array($template['config'] ?? null) ? $template['config'] : [], $config);
|
||||
}
|
||||
$columnSpan = max(1, min(4, (int) ($item['column_span'] ?? 1)));
|
||||
$rowSpan = max(1, min(4, (int) ($item['row_span'] ?? 1)));
|
||||
$gridStyles = 'grid-column: span ' . $columnSpan . '; grid-row: span ' . $rowSpan . ';';
|
||||
if (!empty($item['grid_column'])) {
|
||||
$gridStyles .= 'grid-column-start:' . (int) $item['grid_column'] . ';';
|
||||
}
|
||||
if (!empty($item['grid_row'])) {
|
||||
$gridStyles .= 'grid-row-start:' . (int) $item['grid_row'] . ';';
|
||||
}
|
||||
$targetUrl = trim((string) ($config['url'] ?? ''));
|
||||
$pageModule = null;
|
||||
if ($itemType === 'page_module' && !empty($config['page_module_id'])) {
|
||||
$pageModule = $service->getPageModule((int) $config['page_module_id'], $ownerKey, $groups);
|
||||
$targetUrl = trim((string) ($pageModule['target_url'] ?? $targetUrl));
|
||||
}
|
||||
$appEntry = null;
|
||||
if ($itemType === 'app' && !empty($config['app_id'])) {
|
||||
$appEntry = $appsById[(int) $config['app_id']] ?? null;
|
||||
$targetUrl = trim((string) ($appEntry['app_url'] ?? $targetUrl));
|
||||
}
|
||||
$bookmarks = $itemType === 'bookmark_group' ? $renderBookmarks($config) : [];
|
||||
?>
|
||||
<article class="card-box dashboard-widget" style="<?= e($gridStyles) ?>">
|
||||
<div class="dashboard-widget__head">
|
||||
<div>
|
||||
<span class="module-admin-meta__label"><?= e(strtoupper(str_replace('_', ' ', $itemType))) ?></span>
|
||||
<h2><?= e((string) ($item['title'] ?? 'Element')) ?></h2>
|
||||
<?php if (!empty($item['description'])): ?>
|
||||
<p><?= e((string) $item['description']) ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($itemType === 'bookmark_group' && $bookmarks !== []): ?>
|
||||
<div class="dashboard-links">
|
||||
<?php foreach ($bookmarks as $bookmark): ?>
|
||||
<a class="module-button module-button--secondary module-button--small" href="<?= e($bookmark['url']) ?>" target="_blank" rel="noreferrer"><?= e($bookmark['label']) ?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php elseif (($itemType === 'iframe' || ($pageModule !== null && (string) ($pageModule['module_type'] ?? '') === 'iframe')) && $targetUrl !== ''): ?>
|
||||
<iframe class="dashboard-widget__frame" src="<?= e($targetUrl) ?>" loading="lazy" referrerpolicy="no-referrer"></iframe>
|
||||
<?php elseif ($appEntry !== null): ?>
|
||||
<div class="dashboard-widget__meta">
|
||||
<?php if (!empty($appEntry['icon_url'])): ?>
|
||||
<img class="dashboard-app-icon" src="<?= e((string) $appEntry['icon_url']) ?>" alt="">
|
||||
<?php endif; ?>
|
||||
<p><?= e((string) ($appEntry['description'] ?? $targetUrl)) ?></p>
|
||||
<a class="module-button module-button--secondary module-button--small" href="<?= e($targetUrl) ?>" target="_blank" rel="noreferrer">App öffnen</a>
|
||||
</div>
|
||||
<?php elseif ($pageModule !== null): ?>
|
||||
<div class="dashboard-widget__meta">
|
||||
<p><?= e((string) ($pageModule['description'] ?? 'Seitenmodul aus dem Nexus-System.')) ?></p>
|
||||
<a class="module-button module-button--secondary module-button--small" href="/page-modules/view/<?= (int) ($pageModule['id'] ?? 0) ?>">Öffnen</a>
|
||||
</div>
|
||||
<?php elseif ($targetUrl !== ''): ?>
|
||||
<div class="dashboard-widget__meta">
|
||||
<p><?= e($targetUrl) ?></p>
|
||||
<a class="module-button module-button--secondary module-button--small" href="<?= e($targetUrl) ?>" target="_blank" rel="noreferrer">Öffnen</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="dashboard-empty">Für dieses Element ist noch kein Inhalt hinterlegt.</div>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div></div></div>
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_admin();
|
||||
|
||||
$service = dashboards();
|
||||
$ownerKey = 'system';
|
||||
$notice = null;
|
||||
$error = null;
|
||||
|
||||
if (!$service->available() || $ownerKey === '') {
|
||||
echo '<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack"><section class="section-box">Integrationssystem nicht verfügbar.</section></div></div></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = trim((string) ($_POST['action'] ?? ''));
|
||||
try {
|
||||
if ($action === 'create_integration') {
|
||||
$service->createIntegration($ownerKey, [
|
||||
'type' => trim((string) ($_POST['type'] ?? 'generic')),
|
||||
'name' => trim((string) ($_POST['name'] ?? '')),
|
||||
'base_url' => trim((string) ($_POST['base_url'] ?? '')),
|
||||
'visibility' => trim((string) ($_POST['visibility'] ?? 'private')),
|
||||
'is_active' => isset($_POST['is_active']),
|
||||
'config' => [
|
||||
'notes' => trim((string) ($_POST['notes'] ?? '')),
|
||||
],
|
||||
]);
|
||||
$notice = 'Integration angelegt.';
|
||||
} elseif ($action === 'delete_integration') {
|
||||
$service->deleteIntegration((int) ($_POST['integration_id'] ?? 0), $ownerKey);
|
||||
$notice = 'Integration gelöscht.';
|
||||
}
|
||||
} catch (\Throwable $exception) {
|
||||
$error = $exception->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$integrations = $service->listIntegrationsForOwner($ownerKey);
|
||||
|
||||
$GLOBALS['layout_header_base_title'] = 'Nexus Setup';
|
||||
$GLOBALS['layout_header_title'] = 'Nexus Setup';
|
||||
$GLOBALS['layout_header_context'] = 'Integrationen';
|
||||
$GLOBALS['layout_header_text'] = 'Globale Integrationen für spätere Widgets, Apps und Suchfunktionen.';
|
||||
?>
|
||||
<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack">
|
||||
<header class="module-hero submenu-box">
|
||||
<div class="module-hero-top module-hero-top--compact">
|
||||
<nav class="module-tabs" aria-label="Nexus Setup Navigation">
|
||||
<a class="module-button module-button--tab" href="/settings">Allgemein</a>
|
||||
<a class="module-button module-button--tab" href="/settings/widgets">Widgets</a>
|
||||
<a class="module-button module-button--tab-active" href="/integrations">Integrationen</a>
|
||||
<a class="module-button module-button--tab" href="/settings/search-engines">Suchmaschinen</a>
|
||||
<a class="module-button module-button--tab" href="/settings/apps">Apps</a>
|
||||
<a class="module-button module-button--tab" href="/dashboards">Dashboards</a>
|
||||
</nav>
|
||||
<div class="module-hero-actions module-submenu-actions">
|
||||
<a class="module-button module-button--secondary module-button--small" href="/">Nexus Übersicht</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<?php if ($error !== null): ?>
|
||||
<section class="section-box"><?= e($error) ?></section>
|
||||
<?php elseif ($notice !== null): ?>
|
||||
<section class="section-box"><?= e($notice) ?></section>
|
||||
<?php endif; ?>
|
||||
|
||||
<section class="section-box">
|
||||
<h2>Neue Integration</h2>
|
||||
<form method="post" class="setup-form">
|
||||
<input type="hidden" name="action" value="create_integration">
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Name</span>
|
||||
<input type="text" name="name" required>
|
||||
</label>
|
||||
<label class="setup-field muted">
|
||||
<span>Typ</span>
|
||||
<select name="type">
|
||||
<option value="home_assistant">Home Assistant</option>
|
||||
<option value="pi_hole">Pi-hole</option>
|
||||
<option value="proxmox">Proxmox</option>
|
||||
<option value="docker">Docker</option>
|
||||
<option value="generic">Generic</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Basis-URL</span>
|
||||
<input type="url" name="base_url" placeholder="https://...">
|
||||
</label>
|
||||
<label class="setup-field muted">
|
||||
<span>Sichtbarkeit</span>
|
||||
<select name="visibility">
|
||||
<option value="public">Öffentlich</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Notizen</span>
|
||||
<input type="text" name="notes">
|
||||
</label>
|
||||
<label class="setup-field muted">
|
||||
<input type="checkbox" name="is_active" value="1" checked>
|
||||
<span>Aktiv</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-actions setup-actions--footer">
|
||||
<button class="cta-button" type="submit">Integration speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div class="module-admin-grid">
|
||||
<?php foreach ($integrations as $integration): ?>
|
||||
<article class="card-box module-admin-card">
|
||||
<div class="module-admin-card__head">
|
||||
<div class="module-admin-card__title">
|
||||
<h2><?= e((string) ($integration['name'] ?? 'Integration')) ?></h2>
|
||||
<p><?= e((string) (($integration['config']['notes'] ?? '') ?: (string) ($integration['base_url'] ?? 'Zentrale externe Anbindung.'))) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-admin-meta">
|
||||
<div class="module-admin-meta__item">
|
||||
<span class="module-admin-meta__label">Typ</span>
|
||||
<strong class="module-admin-badge"><?= e((string) ($integration['type'] ?? 'generic')) ?></strong>
|
||||
</div>
|
||||
<div class="module-admin-meta__item">
|
||||
<span class="module-admin-meta__label">Status</span>
|
||||
<strong class="module-admin-badge<?= !empty($integration['is_active']) ? ' module-admin-badge--success' : ' module-admin-badge--warning' ?>"><?= !empty($integration['is_active']) ? 'Aktiv' : 'Inaktiv' ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-admin-actions">
|
||||
<?php if (!empty($integration['base_url'])): ?>
|
||||
<a class="module-button module-button--secondary module-button--small" href="<?= e((string) $integration['base_url']) ?>" target="_blank" rel="noreferrer">Öffnen</a>
|
||||
<?php endif; ?>
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="delete_integration">
|
||||
<input type="hidden" name="integration_id" value="<?= (int) ($integration['id'] ?? 0) ?>">
|
||||
<button class="module-button module-button--secondary module-button--small" type="submit">Löschen</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div></div></div>
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
$moduleName = (string)($_GET['module'] ?? '');
|
||||
$module = modules()->get($moduleName);
|
||||
$notice = null;
|
||||
|
||||
require_admin();
|
||||
|
||||
if (!$module) {
|
||||
http_response_code(404);
|
||||
echo '<div class="card">Modul nicht gefunden.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$selectedUsers = is_array($_POST['auth_user_values'] ?? null) ? $_POST['auth_user_values'] : [];
|
||||
$selectedGroups = is_array($_POST['auth_group_values'] ?? null) ? $_POST['auth_group_values'] : [];
|
||||
$manualUsers = (string)($_POST['auth_users'] ?? '');
|
||||
$manualGroups = (string)($_POST['auth_groups'] ?? '');
|
||||
|
||||
modules()->saveAuth($moduleName, [
|
||||
'required' => isset($_POST['auth_required']),
|
||||
'users' => array_merge($selectedUsers, preg_split('/[,\\n]+/', $manualUsers) ?: []),
|
||||
'groups' => array_merge($selectedGroups, preg_split('/[,\\n]+/', $manualGroups) ?: []),
|
||||
]);
|
||||
$notice = 'Zugriff gespeichert.';
|
||||
$module = modules()->get($moduleName) ?: $module;
|
||||
}
|
||||
|
||||
$authConfig = is_array($module['auth'] ?? null) ? $module['auth'] : ['required' => false, 'users' => [], 'groups' => []];
|
||||
$allowedUsers = is_array($authConfig['users'] ?? null) ? array_values(array_filter(array_map('strval', $authConfig['users']))) : [];
|
||||
$allowedGroups = is_array($authConfig['groups'] ?? null) ? array_values(array_filter(array_map('strval', $authConfig['groups']))) : [];
|
||||
$knownUsers = modules()->knownAuthUsers();
|
||||
$knownGroups = modules()->knownAuthGroups();
|
||||
$currentUser = auth_user();
|
||||
if (is_array($currentUser) && trim((string)($currentUser['sub'] ?? '')) !== '') {
|
||||
$currentSub = (string)$currentUser['sub'];
|
||||
$hasCurrentUser = false;
|
||||
foreach ($knownUsers as $knownUser) {
|
||||
if ((string)($knownUser['sub'] ?? '') === $currentSub) {
|
||||
$hasCurrentUser = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$hasCurrentUser) {
|
||||
$knownUsers[] = [
|
||||
'sub' => $currentSub,
|
||||
'username' => (string)($currentUser['username'] ?? ''),
|
||||
'email' => (string)($currentUser['email'] ?? ''),
|
||||
'name' => (string)($currentUser['name'] ?? ''),
|
||||
'groups' => is_array($currentUser['groups'] ?? null) ? $currentUser['groups'] : [],
|
||||
];
|
||||
}
|
||||
}
|
||||
$knownGroups = array_values(array_unique(array_merge($knownGroups, auth_groups())));
|
||||
sort($knownGroups, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
$knownUserValues = array_column($knownUsers, 'sub');
|
||||
$manualUsers = array_values(array_filter($allowedUsers, fn (string $value): bool => !in_array($value, $knownUserValues, true)));
|
||||
$manualGroups = array_values(array_filter($allowedGroups, fn (string $value): bool => !in_array($value, $knownGroups, true)));
|
||||
?>
|
||||
<div class="card">
|
||||
<div class="pill">Zugriff</div>
|
||||
<h1 style="margin-top:.75rem;"><?= e($module['title']) ?> - Zugriffsrechte</h1>
|
||||
<p class="muted">Diese Seite ist nur fuer eingeloggte Mitglieder der Gruppe <?= e(app()->config()->oidcAdminGroup) ?> verfuegbar.</p>
|
||||
|
||||
<?php if ($notice): ?>
|
||||
<div class="card" style="margin-top:1rem; border-color:var(--accent-2);">
|
||||
<?= e($notice) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" style="margin-top:1rem; display:grid; gap:14px; max-width:520px;">
|
||||
<label class="muted" style="display:flex; align-items:center; gap:10px;">
|
||||
<input type="checkbox" name="auth_required" value="1" <?= !empty($authConfig['required']) ? 'checked' : '' ?>>
|
||||
<span>Login fuer dieses Modul erforderlich</span>
|
||||
</label>
|
||||
|
||||
<div class="muted" style="display:grid; gap:8px;">
|
||||
<span>Erlaubte Benutzer</span>
|
||||
<?php if ($knownUsers === []): ?>
|
||||
<small class="muted">Noch keine Keycloak-User bekannt. User erscheinen hier, nachdem sie sich einmal angemeldet haben.</small>
|
||||
<?php else: ?>
|
||||
<div style="display:grid; gap:6px;">
|
||||
<?php foreach ($knownUsers as $knownUser): ?>
|
||||
<?php
|
||||
$sub = (string)($knownUser['sub'] ?? '');
|
||||
$label = trim((string)($knownUser['name'] ?? ''));
|
||||
if ($label === '') {
|
||||
$label = trim((string)($knownUser['username'] ?? ''));
|
||||
}
|
||||
$email = trim((string)($knownUser['email'] ?? ''));
|
||||
$suffix = $email !== '' && $email !== $label ? ' (' . $email . ')' : '';
|
||||
?>
|
||||
<label style="display:flex; align-items:center; gap:10px;">
|
||||
<input type="checkbox" name="auth_user_values[]" value="<?= e($sub) ?>" <?= in_array($sub, $allowedUsers, true) ? 'checked' : '' ?>>
|
||||
<span><?= e(($label !== '' ? $label : $sub) . $suffix) ?></span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<textarea name="auth_users" rows="3" placeholder="Weitere Keycloak-Sub, Benutzername oder E-Mail, je Zeile oder Komma"><?= e(implode("\n", $manualUsers)) ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="muted" style="display:grid; gap:8px;">
|
||||
<span>Erlaubte Gruppen</span>
|
||||
<?php if ($knownGroups === []): ?>
|
||||
<small class="muted">Noch keine Keycloak-Gruppen bekannt. Gruppen werden aus angemeldeten Usern und gespeicherten Modulrechten gesammelt.</small>
|
||||
<?php else: ?>
|
||||
<div style="display:grid; gap:6px;">
|
||||
<?php foreach ($knownGroups as $knownGroup): ?>
|
||||
<label style="display:flex; align-items:center; gap:10px;">
|
||||
<input type="checkbox" name="auth_group_values[]" value="<?= e($knownGroup) ?>" <?= in_array($knownGroup, $allowedGroups, true) ? 'checked' : '' ?>>
|
||||
<span><?= e($knownGroup) ?></span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<textarea name="auth_groups" rows="3" placeholder="Weitere Gruppen, je Zeile oder Komma"><?= e(implode("\n", $manualGroups)) ?></textarea>
|
||||
</div>
|
||||
|
||||
<small class="muted">Wenn Login aktiv ist und Benutzer/Gruppen leer bleiben, darf jeder eingeloggte Benutzer das Modul oeffnen.</small>
|
||||
|
||||
<div style="display:flex; gap:10px;">
|
||||
<button class="cta-button" type="submit">Zugriff speichern</button>
|
||||
<a class="nav-link" href="/modules/setup/<?= e($moduleName) ?>">Setup</a>
|
||||
<a class="nav-link" href="/modules">Zurück</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
$modules = modules()->all();
|
||||
$error = null;
|
||||
$notice = null;
|
||||
$GLOBALS['layout_header_base_title'] = 'Modulverwaltung';
|
||||
$GLOBALS['layout_header_title'] = 'Modulverwaltung';
|
||||
$GLOBALS['layout_header_context'] = 'Aktive Module';
|
||||
$GLOBALS['layout_header_text'] = '';
|
||||
$GLOBALS['layout_header_actions'] = [];
|
||||
$knownAuthUsers = modules()->knownAuthUsers();
|
||||
$authUserLabels = [];
|
||||
foreach ($knownAuthUsers as $knownAuthUser) {
|
||||
if (!is_array($knownAuthUser)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$label = trim((string) ($knownAuthUser['name'] ?? ''));
|
||||
if ($label === '') {
|
||||
$label = trim((string) ($knownAuthUser['username'] ?? ''));
|
||||
}
|
||||
if ($label === '') {
|
||||
$label = trim((string) ($knownAuthUser['email'] ?? ''));
|
||||
}
|
||||
|
||||
foreach (['sub', 'username', 'email'] as $key) {
|
||||
$value = trim((string) ($knownAuthUser[$key] ?? ''));
|
||||
if ($value !== '' && $label !== '') {
|
||||
$authUserLabels[$value] = $label;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
require_admin();
|
||||
$name = (string)($_POST['module'] ?? '');
|
||||
$action = (string)($_POST['action'] ?? '');
|
||||
|
||||
if ($name !== '' && ($action === 'enable' || $action === 'disable')) {
|
||||
$enabled = $action === 'enable';
|
||||
modules()->setEnabled($name, $enabled);
|
||||
$notice = $enabled ? 'Modul aktiviert.' : 'Modul deaktiviert.';
|
||||
$modules = modules()->all();
|
||||
} elseif ($name !== '' && $action === 'migrate') {
|
||||
$applied = modules()->applyPendingMigrations($name);
|
||||
$notice = count($applied) . ' Migration(en) angewendet.';
|
||||
$modules = modules()->all();
|
||||
} else {
|
||||
$error = 'Ungültige Aktion.';
|
||||
}
|
||||
}
|
||||
?>
|
||||
<?php require_auth(); ?>
|
||||
<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack">
|
||||
<?php if ($error): ?>
|
||||
<div class="section-box bg-red-900 border-l-4 border-red-500 text-red-100" role="alert">
|
||||
<?= e($error) ?>
|
||||
</div>
|
||||
<?php elseif ($notice): ?>
|
||||
<div class="section-box" style="border-color:var(--accent-2);">
|
||||
<?= e($notice) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="submenu-box">
|
||||
<div class="module-hero-top module-hero-top--compact">
|
||||
<nav class="module-tabs" aria-label="Modulverwaltung">
|
||||
<a class="module-button module-button--tab-active" href="/modules">Aktive Module</a>
|
||||
<a class="module-button module-button--tab" href="/modules/install">Module installieren/aktivieren</a>
|
||||
<a class="module-button module-button--tab" href="/modules/sql-import">Zentralen SQL-Import</a>
|
||||
</nav>
|
||||
<div class="module-submenu-actions">
|
||||
<a class="module-button module-button--secondary module-button--small" href="/">Nexus Übersicht</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="module-admin-grid">
|
||||
<?php foreach ($modules as $module): ?>
|
||||
<?php if (empty($module['enabled'])) { continue; } ?>
|
||||
<?php
|
||||
$auth = is_array($module['auth'] ?? null) ? $module['auth'] : [];
|
||||
$authRequired = !empty($auth['required']);
|
||||
$authUsers = is_array($auth['users'] ?? null) ? array_filter($auth['users']) : [];
|
||||
$authGroups = is_array($auth['groups'] ?? null) ? array_filter($auth['groups']) : [];
|
||||
$hasSpecificAccess = $authRequired && ($authUsers !== [] || $authGroups !== []);
|
||||
if ($hasSpecificAccess) {
|
||||
$accessParts = [];
|
||||
if ($authGroups !== []) {
|
||||
$accessParts[] = 'Gruppen: ' . implode(', ', $authGroups);
|
||||
}
|
||||
if ($authUsers !== []) {
|
||||
$userLabels = array_map(
|
||||
static fn (string $user): string => $authUserLabels[$user] ?? $user,
|
||||
$authUsers
|
||||
);
|
||||
$accessParts[] = 'Nutzer: ' . implode(', ', $userLabels);
|
||||
}
|
||||
$accessLabel = implode(' · ', $accessParts);
|
||||
$accessClass = ' module-admin-badge--success';
|
||||
} elseif ($authRequired) {
|
||||
$accessLabel = 'Login erforderlich';
|
||||
$accessClass = ' module-admin-badge--warning';
|
||||
} else {
|
||||
$accessLabel = 'Offen ohne Modulschutz';
|
||||
$accessClass = ' module-admin-badge--danger';
|
||||
}
|
||||
$migrationStatus = modules()->migrationStatus($module['name']);
|
||||
$pendingMigrations = $migrationStatus['pending'] ?? [];
|
||||
$changedMigrations = $migrationStatus['changed'] ?? [];
|
||||
$migrationLabel = $pendingMigrations !== []
|
||||
? count($pendingMigrations) . ' ausstehend'
|
||||
: (($migrationStatus['available'] ?? 0) > 0 ? 'Schema aktuell' : 'Keine Migrationen');
|
||||
?>
|
||||
<article class="card-box module-admin-card">
|
||||
<div class="module-admin-card__head">
|
||||
<div class="module-admin-card__title">
|
||||
<h2><?= e($module['title']) ?></h2>
|
||||
<p><?= e($module['description'] ?? '') ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-admin-meta">
|
||||
<div class="module-admin-meta__item">
|
||||
<span class="module-admin-meta__label">Status</span>
|
||||
<strong class="module-admin-badge module-admin-badge--success">Aktiv</strong>
|
||||
</div>
|
||||
<div class="module-admin-meta__item">
|
||||
<span class="module-admin-meta__label">Zugriff</span>
|
||||
<strong class="module-admin-badge<?= e($accessClass) ?>"><?= e($accessLabel) ?></strong>
|
||||
</div>
|
||||
<div class="module-admin-meta__item">
|
||||
<span class="module-admin-meta__label">Migrationen</span>
|
||||
<strong class="module-admin-badge<?= $pendingMigrations !== [] ? ' module-admin-badge--accent' : (($migrationStatus['available'] ?? 0) > 0 ? ' module-admin-badge--success' : '') ?>"><?= e($migrationLabel) ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($changedMigrations !== []): ?>
|
||||
<div class="module-admin-warning">
|
||||
Achtung: <?= e((string)count($changedMigrations)) ?> bereits angewendete Migration(en) wurden verändert.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="module-admin-actions">
|
||||
<a class="nav-link" href="/module/<?= e($module['name']) ?>">Öffnen</a>
|
||||
<a class="nav-link" href="/modules/setup/<?= e($module['name']) ?>">Setup</a>
|
||||
<a class="nav-link" href="/modules/access/<?= e($module['name']) ?>">Zugriff</a>
|
||||
<?php if ($pendingMigrations !== []): ?>
|
||||
<form method="post" style="margin:0;">
|
||||
<input type="hidden" name="module" value="<?= e($module['name']) ?>">
|
||||
<button class="cta-button" name="action" value="migrate">Migrationen anwenden</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<form method="post" style="margin:0;">
|
||||
<input type="hidden" name="module" value="<?= e($module['name']) ?>">
|
||||
<button class="cta-button" name="action" value="disable" style="background:var(--panel); color:var(--text);">Deaktivieren</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div></div></div>
|
||||
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
$modules = modules()->all();
|
||||
$error = null;
|
||||
$notice = null;
|
||||
$GLOBALS['layout_header_base_title'] = 'Modulverwaltung';
|
||||
$GLOBALS['layout_header_title'] = 'Modulverwaltung';
|
||||
$GLOBALS['layout_header_context'] = 'Module installieren/aktivieren';
|
||||
$GLOBALS['layout_header_text'] = '';
|
||||
$GLOBALS['layout_header_actions'] = [];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
require_admin();
|
||||
$name = (string)($_POST['module'] ?? '');
|
||||
$action = (string)($_POST['action'] ?? '');
|
||||
if ($name !== '' && ($action === 'enable' || $action === 'disable')) {
|
||||
modules()->setEnabled($name, $action === 'enable');
|
||||
$notice = $action === 'enable' ? 'Modul aktiviert.' : 'Modul deaktiviert.';
|
||||
$modules = modules()->all();
|
||||
} elseif ($name !== '' && $action === 'migrate') {
|
||||
$applied = modules()->applyPendingMigrations($name);
|
||||
$notice = count($applied) . ' Migration(en) angewendet.';
|
||||
$modules = modules()->all();
|
||||
} else {
|
||||
$error = 'Ungültige Aktion.';
|
||||
}
|
||||
}
|
||||
|
||||
$active = [];
|
||||
$inactive = [];
|
||||
foreach ($modules as $m) {
|
||||
if (!empty($m['enabled'])) {
|
||||
$active[] = $m;
|
||||
} else {
|
||||
$inactive[] = $m;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<?php require_auth(); ?>
|
||||
<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack">
|
||||
<?php if ($error): ?>
|
||||
<div class="section-box bg-red-900 border-l-4 border-red-500 text-red-100" role="alert">
|
||||
<?= e($error) ?>
|
||||
</div>
|
||||
<?php elseif ($notice): ?>
|
||||
<div class="section-box" style="border-color:var(--accent-2);">
|
||||
<?= e($notice) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="submenu-box">
|
||||
<div class="module-hero-top module-hero-top--compact">
|
||||
<nav class="module-tabs" aria-label="Modulverwaltung">
|
||||
<a class="module-button module-button--tab" href="/modules">Aktive Module</a>
|
||||
<a class="module-button module-button--tab-active" href="/modules/install">Module installieren/aktivieren</a>
|
||||
<a class="module-button module-button--tab" href="/modules/sql-import">Zentralen SQL-Import</a>
|
||||
</nav>
|
||||
<div class="module-submenu-actions">
|
||||
<a class="module-button module-button--secondary module-button--small" href="/">Nexus Übersicht</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="section-box">
|
||||
<div class="module-box-head">
|
||||
<div>
|
||||
<h2 class="module-box-title">Aktive Module</h2>
|
||||
<p>Bereits aktivierte Module mit optionalen Migrationsschritten.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-admin-grid module-admin-grid--compact">
|
||||
<?php foreach ($active as $module): ?>
|
||||
<?php
|
||||
$migrationStatus = modules()->migrationStatus($module['name']);
|
||||
$pendingMigrations = $migrationStatus['pending'] ?? [];
|
||||
$migrationLabel = $pendingMigrations !== []
|
||||
? count($pendingMigrations) . ' ausstehend'
|
||||
: (($migrationStatus['available'] ?? 0) > 0 ? 'Schema aktuell' : 'Keine Migrationen');
|
||||
?>
|
||||
<article class="card-box module-admin-card">
|
||||
<div class="module-admin-card__head">
|
||||
<div class="module-admin-card__title">
|
||||
<h2><?= e($module['title']) ?></h2>
|
||||
<p><?= e($module['description'] ?? '') ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-admin-meta">
|
||||
<div class="module-admin-meta__item">
|
||||
<span class="module-admin-meta__label">Status</span>
|
||||
<strong class="module-admin-badge module-admin-badge--success">Aktiv</strong>
|
||||
</div>
|
||||
<div class="module-admin-meta__item">
|
||||
<span class="module-admin-meta__label">Migrationen</span>
|
||||
<strong class="module-admin-badge<?= $pendingMigrations !== [] ? ' module-admin-badge--accent' : (($migrationStatus['available'] ?? 0) > 0 ? ' module-admin-badge--success' : '') ?>"><?= e($migrationLabel) ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-admin-actions">
|
||||
<a class="nav-link" href="/module/<?= e($module['name']) ?>">Öffnen</a>
|
||||
<?php if ($pendingMigrations !== []): ?>
|
||||
<form method="post" style="margin:0;">
|
||||
<input type="hidden" name="module" value="<?= e($module['name']) ?>">
|
||||
<button class="cta-button" name="action" value="migrate">Migrationen anwenden</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<form method="post" style="margin:0;">
|
||||
<input type="hidden" name="module" value="<?= e($module['name']) ?>">
|
||||
<button class="cta-button" name="action" value="disable" style="background:var(--panel); color:var(--text);">Deaktivieren</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-box">
|
||||
<div class="module-box-head">
|
||||
<div>
|
||||
<h2 class="module-box-title">Deaktivierte Module</h2>
|
||||
<p>Erkannte Module basieren auf Ordnern in <code>modules/</code>.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-admin-grid module-admin-grid--compact">
|
||||
<?php foreach ($inactive as $module): ?>
|
||||
<?php
|
||||
$migrationStatus = modules()->migrationStatus($module['name']);
|
||||
$pendingMigrations = $migrationStatus['pending'] ?? [];
|
||||
$migrationLabel = $pendingMigrations !== []
|
||||
? count($pendingMigrations) . ' ausstehend'
|
||||
: (($migrationStatus['available'] ?? 0) > 0 ? 'Schema aktuell' : 'Keine Migrationen');
|
||||
?>
|
||||
<article class="card-box module-admin-card">
|
||||
<div class="module-admin-card__head">
|
||||
<div class="module-admin-card__title">
|
||||
<h2><?= e($module['title']) ?></h2>
|
||||
<p><?= e($module['description'] ?? '') ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-admin-meta">
|
||||
<div class="module-admin-meta__item">
|
||||
<span class="module-admin-meta__label">Status</span>
|
||||
<strong class="module-admin-badge">Deaktiviert</strong>
|
||||
</div>
|
||||
<div class="module-admin-meta__item">
|
||||
<span class="module-admin-meta__label">Migrationen</span>
|
||||
<strong class="module-admin-badge<?= $pendingMigrations !== [] ? ' module-admin-badge--accent' : (($migrationStatus['available'] ?? 0) > 0 ? ' module-admin-badge--success' : '') ?>"><?= e($migrationLabel) ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-admin-actions">
|
||||
<a class="nav-link" href="/modules/setup/<?= e($module['name']) ?>">Setup</a>
|
||||
<?php if ($pendingMigrations !== []): ?>
|
||||
<form method="post" style="margin:0;">
|
||||
<input type="hidden" name="module" value="<?= e($module['name']) ?>">
|
||||
<button class="cta-button" name="action" value="migrate">Migrationen anwenden</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<form method="post" style="margin:0;">
|
||||
<input type="hidden" name="module" value="<?= e($module['name']) ?>">
|
||||
<button class="cta-button" name="action" value="enable">Aktivieren</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
</div></div></div>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
require_admin();
|
||||
|
||||
$GLOBALS['layout_header_base_title'] = 'Modulverwaltung';
|
||||
$GLOBALS['layout_header_title'] = 'Modulverwaltung';
|
||||
$GLOBALS['layout_header_context'] = 'SQL-Import';
|
||||
$GLOBALS['layout_header_text'] = '';
|
||||
$GLOBALS['layout_header_actions'] = [];
|
||||
|
||||
$service = new \App\ModuleSqlImportService(modules());
|
||||
$availableModules = $service->importableModules();
|
||||
$selectedModule = (string) ($_POST['module'] ?? ($_GET['module'] ?? ''));
|
||||
$error = null;
|
||||
$notice = null;
|
||||
$result = null;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
try {
|
||||
if (!isset($_FILES['sql_file']) || !is_array($_FILES['sql_file'])) {
|
||||
throw new RuntimeException('Bitte eine SQL-Datei auswählen.');
|
||||
}
|
||||
|
||||
$result = $service->importUploadedFile($selectedModule, $_FILES['sql_file']);
|
||||
$notice = sprintf(
|
||||
'%s %d Statements aus %s wurden nach %s importiert.',
|
||||
(string) ($result['message'] ?? 'Import erfolgreich.'),
|
||||
(int) ($result['statement_count'] ?? 0),
|
||||
(string) ($result['file'] ?? 'der Datei'),
|
||||
(string) ($result['target_label'] ?? 'der Ziel-Datenbank')
|
||||
);
|
||||
} catch (Throwable $exception) {
|
||||
$error = $exception->getMessage();
|
||||
}
|
||||
}
|
||||
?>
|
||||
<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack">
|
||||
<?php if ($error): ?>
|
||||
<div class="section-box bg-red-900 border-l-4 border-red-500 text-red-100" role="alert">
|
||||
<?= e($error) ?>
|
||||
</div>
|
||||
<?php elseif ($notice): ?>
|
||||
<div class="section-box" style="border-color:var(--accent-2);">
|
||||
<?= e($notice) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="submenu-box">
|
||||
<div class="module-hero-top module-hero-top--compact">
|
||||
<nav class="module-tabs" aria-label="Modulverwaltung">
|
||||
<a class="module-button module-button--tab" href="/modules">Aktive Module</a>
|
||||
<a class="module-button module-button--tab" href="/modules/install">Module installieren/aktivieren</a>
|
||||
<a class="module-button module-button--tab-active" href="/modules/sql-import">Zentralen SQL-Import</a>
|
||||
</nav>
|
||||
<div class="module-submenu-actions">
|
||||
<a class="module-button module-button--secondary module-button--small" href="/">Nexus Übersicht</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="section-box">
|
||||
<div class="module-box-head">
|
||||
<div>
|
||||
<h2 class="module-box-title">Zentraler SQL-Import</h2>
|
||||
<p>Gemeinsame Standard-Lösung für Module mit SQL-Upload in die konfigurierte Ziel-Datenbank.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($availableModules === []): ?>
|
||||
<div class="card-box">
|
||||
Aktuell ist kein Modul für den generischen SQL-Import vorbereitet.
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<form method="post" enctype="multipart/form-data" class="section-box" style="padding:0; border:0; box-shadow:none; background:transparent;">
|
||||
<div style="display:grid; gap:1rem;">
|
||||
<label style="display:grid; gap:.35rem;">
|
||||
<span>Modul</span>
|
||||
<select name="module" required>
|
||||
<option value="">Bitte wählen</option>
|
||||
<?php foreach ($availableModules as $module): ?>
|
||||
<option value="<?= e($module['name']) ?>" <?= $selectedModule === $module['name'] ? 'selected' : '' ?>>
|
||||
<?= e($module['title']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label style="display:grid; gap:.35rem;">
|
||||
<span>SQL-Datei</span>
|
||||
<input type="file" name="sql_file" accept=".sql,text/sql,application/sql" required>
|
||||
</label>
|
||||
|
||||
<div class="muted" style="font-size:.95rem;">
|
||||
Der generische Import nutzt die Standard-Datenbankverbindung des Moduls oder einen optionalen Modul-Callback für Spezialfälle.
|
||||
</div>
|
||||
|
||||
<div style="display:flex; gap:10px; flex-wrap:wrap;">
|
||||
<button class="cta-button" type="submit">SQL importieren</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<?php if ($availableModules !== []): ?>
|
||||
<section class="section-box">
|
||||
<div class="module-box-head">
|
||||
<div>
|
||||
<h2 class="module-box-title">Importierbare Module</h2>
|
||||
<p>Schnelleinstieg für den Import mit bereits vorausgewähltem Modul.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-admin-grid module-admin-grid--compact">
|
||||
<?php foreach ($availableModules as $module): ?>
|
||||
<article class="card-box module-admin-card">
|
||||
<div class="module-admin-card__head">
|
||||
<div class="module-admin-card__title">
|
||||
<h2><?= e($module['title']) ?></h2>
|
||||
<p><?= e($module['description']) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-admin-actions">
|
||||
<a class="nav-link" href="/modules/sql-import?module=<?= e($module['name']) ?>">Für dieses Modul importieren</a>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
</div></div></div>
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_auth();
|
||||
|
||||
$service = dashboards();
|
||||
$ownerKey = auth_user_key();
|
||||
$notice = null;
|
||||
$error = null;
|
||||
|
||||
if (!$service->available() || $ownerKey === '') {
|
||||
echo '<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack"><section class="section-box">Seitenmodul-System nicht verfügbar.</section></div></div></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = trim((string) ($_POST['action'] ?? ''));
|
||||
try {
|
||||
if ($action === 'create_page_module') {
|
||||
$service->createPageModule($ownerKey, [
|
||||
'title' => trim((string) ($_POST['title'] ?? '')),
|
||||
'slug' => trim((string) ($_POST['slug'] ?? '')),
|
||||
'module_type' => trim((string) ($_POST['module_type'] ?? 'link')),
|
||||
'target_url' => trim((string) ($_POST['target_url'] ?? '')),
|
||||
'description' => trim((string) ($_POST['description'] ?? '')),
|
||||
'visibility' => trim((string) ($_POST['visibility'] ?? 'private')),
|
||||
'open_mode' => trim((string) ($_POST['open_mode'] ?? 'embed')),
|
||||
'is_active' => isset($_POST['is_active']),
|
||||
]);
|
||||
$notice = 'Seitenmodul angelegt.';
|
||||
} elseif ($action === 'delete_page_module') {
|
||||
$service->deletePageModule((int) ($_POST['page_module_id'] ?? 0), $ownerKey);
|
||||
$notice = 'Seitenmodul gelöscht.';
|
||||
}
|
||||
} catch (\Throwable $exception) {
|
||||
$error = $exception->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$pageModules = $service->listPageModulesForOwner($ownerKey);
|
||||
|
||||
$GLOBALS['layout_header_base_title'] = 'Nexus';
|
||||
$GLOBALS['layout_header_title'] = 'Nexus';
|
||||
$GLOBALS['layout_header_context'] = 'Seitenmodule';
|
||||
$GLOBALS['layout_header_text'] = 'On-the-fly angelegte Link- und iFrame-Module ohne eigenen Modulordner.';
|
||||
?>
|
||||
<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack">
|
||||
<header class="module-hero submenu-box">
|
||||
<div class="module-hero-top module-hero-top--compact">
|
||||
<nav class="module-tabs" aria-label="Seitenmodule Navigation">
|
||||
<a class="module-button module-button--tab" href="/dashboard">Dashboard</a>
|
||||
<a class="module-button module-button--tab" href="/dashboards">Dashboards</a>
|
||||
<a class="module-button module-button--tab" href="/integrations">Integrationen</a>
|
||||
<a class="module-button module-button--tab-active" href="/page-modules">Seitenmodule</a>
|
||||
</nav>
|
||||
<div class="module-hero-actions module-submenu-actions">
|
||||
<a class="module-button module-button--secondary module-button--small" href="/">Nexus Übersicht</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<?php if ($error !== null): ?>
|
||||
<section class="section-box"><?= e($error) ?></section>
|
||||
<?php elseif ($notice !== null): ?>
|
||||
<section class="section-box"><?= e($notice) ?></section>
|
||||
<?php endif; ?>
|
||||
|
||||
<section class="section-box">
|
||||
<h2>Neues Seitenmodul</h2>
|
||||
<form method="post" class="setup-form">
|
||||
<input type="hidden" name="action" value="create_page_module">
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Titel</span>
|
||||
<input type="text" name="title" required>
|
||||
</label>
|
||||
<label class="setup-field muted">
|
||||
<span>Slug optional</span>
|
||||
<input type="text" name="slug">
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Typ</span>
|
||||
<select name="module_type">
|
||||
<option value="link">Link</option>
|
||||
<option value="iframe">iFrame</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="setup-field muted">
|
||||
<span>Ziel-URL</span>
|
||||
<input type="url" name="target_url" required placeholder="https://...">
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Beschreibung</span>
|
||||
<input type="text" name="description">
|
||||
</label>
|
||||
<label class="setup-field muted">
|
||||
<span>Öffnen als</span>
|
||||
<select name="open_mode">
|
||||
<option value="embed">Im Nexus einbetten</option>
|
||||
<option value="new_tab">Neuer Tab</option>
|
||||
<option value="same_tab">Direkt öffnen</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Sichtbarkeit</span>
|
||||
<select name="visibility">
|
||||
<option value="private">Privat</option>
|
||||
<option value="public">Öffentlich</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="setup-field muted">
|
||||
<input type="checkbox" name="is_active" value="1" checked>
|
||||
<span>Aktiv</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-actions setup-actions--footer">
|
||||
<button class="cta-button" type="submit">Seitenmodul speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div class="module-admin-grid">
|
||||
<?php foreach ($pageModules as $pageModule): ?>
|
||||
<article class="card-box module-admin-card">
|
||||
<div class="module-admin-card__head">
|
||||
<div class="module-admin-card__title">
|
||||
<h2><?= e((string) ($pageModule['title'] ?? 'Seitenmodul')) ?></h2>
|
||||
<p><?= e((string) ($pageModule['description'] ?? ($pageModule['target_url'] ?? ''))) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-admin-meta">
|
||||
<div class="module-admin-meta__item">
|
||||
<span class="module-admin-meta__label">Typ</span>
|
||||
<strong class="module-admin-badge"><?= e((string) ($pageModule['module_type'] ?? 'link')) ?></strong>
|
||||
</div>
|
||||
<div class="module-admin-meta__item">
|
||||
<span class="module-admin-meta__label">Öffnen</span>
|
||||
<strong class="module-admin-badge"><?= e((string) ($pageModule['open_mode'] ?? 'embed')) ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-admin-actions">
|
||||
<a class="module-button module-button--secondary module-button--small" href="/page-modules/view/<?= (int) ($pageModule['id'] ?? 0) ?>">Öffnen</a>
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="delete_page_module">
|
||||
<input type="hidden" name="page_module_id" value="<?= (int) ($pageModule['id'] ?? 0) ?>">
|
||||
<button class="module-button module-button--secondary module-button--small" type="submit">Löschen</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div></div></div>
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_auth();
|
||||
|
||||
$service = dashboards();
|
||||
$ownerKey = auth_user_key();
|
||||
$groups = auth_groups();
|
||||
$pageModuleId = (int) ($_GET['id'] ?? 0);
|
||||
$pageModule = $service->getPageModule($pageModuleId, $ownerKey, $groups);
|
||||
|
||||
if ($pageModule === null) {
|
||||
http_response_code(404);
|
||||
echo '<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack"><section class="section-box">Seitenmodul nicht gefunden.</section></div></div></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
$targetUrl = trim((string) ($pageModule['target_url'] ?? ''));
|
||||
$openMode = (string) ($pageModule['open_mode'] ?? 'embed');
|
||||
if ($targetUrl !== '' && $openMode === 'same_tab') {
|
||||
redirect($targetUrl);
|
||||
}
|
||||
|
||||
$GLOBALS['layout_header_base_title'] = 'Nexus';
|
||||
$GLOBALS['layout_header_title'] = 'Nexus';
|
||||
$GLOBALS['layout_header_context'] = (string) ($pageModule['title'] ?? 'Seitenmodul');
|
||||
$GLOBALS['layout_header_text'] = (string) ($pageModule['description'] ?? 'On-the-fly angelegtes Seitenmodul.');
|
||||
?>
|
||||
<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack">
|
||||
<header class="module-hero submenu-box">
|
||||
<div class="module-hero-top module-hero-top--compact">
|
||||
<nav class="module-tabs" aria-label="Seitenmodul Navigation">
|
||||
<a class="module-button module-button--tab" href="/dashboard">Dashboard</a>
|
||||
<a class="module-button module-button--tab" href="/dashboards">Dashboards</a>
|
||||
<a class="module-button module-button--tab" href="/integrations">Integrationen</a>
|
||||
<a class="module-button module-button--tab-active" href="/page-modules">Seitenmodule</a>
|
||||
</nav>
|
||||
<div class="module-hero-actions module-submenu-actions">
|
||||
<a class="module-button module-button--secondary module-button--small" href="/">Nexus Übersicht</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="section-box">
|
||||
<h2><?= e((string) ($pageModule['title'] ?? 'Seitenmodul')) ?></h2>
|
||||
<p class="muted"><?= e((string) ($pageModule['description'] ?? '')) ?></p>
|
||||
<?php if ($targetUrl === ''): ?>
|
||||
<div class="dashboard-empty">Dieses Seitenmodul hat noch keine Ziel-URL.</div>
|
||||
<?php elseif ((string) ($pageModule['module_type'] ?? 'link') === 'iframe' || $openMode === 'embed'): ?>
|
||||
<iframe class="dashboard-widget__frame dashboard-widget__frame--page" src="<?= e($targetUrl) ?>" loading="lazy" referrerpolicy="no-referrer"></iframe>
|
||||
<?php else: ?>
|
||||
<div class="dashboard-widget__meta">
|
||||
<p><?= e($targetUrl) ?></p>
|
||||
<a class="module-button module-button--secondary module-button--small" href="<?= e($targetUrl) ?>" target="_blank" rel="noreferrer">Extern öffnen</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
</div></div></div>
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
$isList = (isset($_GET['list']) && $_GET['list'] === '1');
|
||||
$isRaw = (isset($_GET['raw']) && $_GET['raw'] === '1');
|
||||
|
||||
if ($isList || $isRaw) {
|
||||
if (!auth_is_admin()) {
|
||||
http_response_code(403);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo 'forbidden';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
require_admin();
|
||||
|
||||
if (!defined('APP_DEBUG_TOOL') || !APP_DEBUG_TOOL) {
|
||||
echo '<div class="card">Debug-Tool ist deaktiviert.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
$debugDir = __DIR__ . '/../../../debug';
|
||||
if (!is_dir($debugDir)) {
|
||||
if ($isList || $isRaw) {
|
||||
http_response_code(404);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo 'debug_dir_missing';
|
||||
return;
|
||||
}
|
||||
echo '<div class="card">Debug-Verzeichnis fehlt.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
$files = array_values(array_filter(scandir($debugDir) ?: [], function ($f) use ($debugDir) {
|
||||
if ($f === '.' || $f === '..') return false;
|
||||
$path = $debugDir . '/' . $f;
|
||||
return is_file($path);
|
||||
}));
|
||||
|
||||
$selected = (string)($_GET['file'] ?? '');
|
||||
$content = null;
|
||||
|
||||
if ($selected !== '' && preg_match('/^[a-zA-Z0-9._-]+$/', $selected)) {
|
||||
$path = $debugDir . '/' . $selected;
|
||||
if (is_file($path)) {
|
||||
$content = file_get_contents($path);
|
||||
}
|
||||
}
|
||||
|
||||
if ($isList) {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($files);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($isRaw) {
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
$tail = isset($_GET['tail']) ? (int)$_GET['tail'] : 0;
|
||||
if ($tail > 0 && $content !== null) {
|
||||
$lines = preg_split('/\\R/', $content) ?: [];
|
||||
$content = implode(PHP_EOL, array_slice($lines, -$tail));
|
||||
}
|
||||
echo $content ?? '';
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<div class="card">
|
||||
<div class="pill">Debug</div>
|
||||
<h1 style="margin-top:.75rem;">Debug Logs</h1>
|
||||
<p class="muted">Hier kannst du temporäre Log-Files aus dem <code>debug/</code>-Ordner ansehen.</p>
|
||||
|
||||
<div style="margin-top:.5rem;">
|
||||
<a class="nav-link" href="/debug?file=oidc_login.log">OIDC Login</a>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:1rem;" class="grid">
|
||||
<div class="card" style="background:var(--panel-2);">
|
||||
<strong>Logs</strong>
|
||||
<ul style="margin-top:.5rem;">
|
||||
<?php if (!$files): ?>
|
||||
<li class="muted">Keine Logs vorhanden.</li>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($files as $f): ?>
|
||||
<li>
|
||||
<a class="nav-link" href="/debug?file=<?= e($f) ?>"><?= e($f) ?></a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card" style="background:var(--panel-2);">
|
||||
<strong>Inhalt</strong>
|
||||
<?php if ($content === null): ?>
|
||||
<p class="muted" style="margin-top:.5rem;">Wähle eine Datei.</p>
|
||||
<?php else: ?>
|
||||
<pre id="debug-content" style="margin-top:.5rem; white-space:pre-wrap; font-family:monospace;"><?= e($content) ?></pre>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($selected !== ''): ?>
|
||||
<script>
|
||||
(() => {
|
||||
const el = document.getElementById('debug-content');
|
||||
if (!el) return;
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('raw', '1');
|
||||
let last = '';
|
||||
async function tick() {
|
||||
try {
|
||||
const res = await fetch(url.toString(), { cache: 'no-store' });
|
||||
if (!res.ok) return;
|
||||
const text = await res.text();
|
||||
if (text !== last) {
|
||||
el.textContent = text;
|
||||
last = text;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
tick();
|
||||
setInterval(tick, 3000);
|
||||
})();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
$pdo = app()->basePdo();
|
||||
$error = null;
|
||||
$notice = null;
|
||||
|
||||
require_admin();
|
||||
|
||||
if (!$pdo) {
|
||||
echo '<div class="card">Base-DB nicht aktiviert.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = (string)($_POST['action'] ?? '');
|
||||
|
||||
if ($action === 'add_role') {
|
||||
$role = trim((string)($_POST['role'] ?? ''));
|
||||
$desc = trim((string)($_POST['description'] ?? ''));
|
||||
if ($role === '') {
|
||||
$error = 'Rollenname fehlt.';
|
||||
} else {
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO nexus_roles (name, description)
|
||||
VALUES (:name, :description)
|
||||
ON CONFLICT(name) DO UPDATE SET description = excluded.description"
|
||||
);
|
||||
$stmt->execute(['name' => $role, 'description' => $desc]);
|
||||
$notice = 'Rolle gespeichert.';
|
||||
}
|
||||
} elseif ($action === 'add_user') {
|
||||
$email = trim((string)($_POST['email'] ?? ''));
|
||||
$password = (string)($_POST['password'] ?? '');
|
||||
$role = trim((string)($_POST['role'] ?? 'user'));
|
||||
|
||||
if ($email === '' || $password === '') {
|
||||
$error = 'E-Mail und Passwort sind erforderlich.';
|
||||
} else {
|
||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
$pdo->prepare(
|
||||
"INSERT INTO nexus_users (email, password_hash, role, is_active)
|
||||
VALUES (:email, :hash, :role, 1)"
|
||||
)->execute([
|
||||
'email' => $email,
|
||||
'hash' => $hash,
|
||||
'role' => $role !== '' ? $role : 'user',
|
||||
]);
|
||||
|
||||
$pdo->prepare(
|
||||
"INSERT INTO nexus_roles (name) VALUES (:name)
|
||||
ON CONFLICT(name) DO NOTHING"
|
||||
)->execute(['name' => $role !== '' ? $role : 'user']);
|
||||
|
||||
$notice = 'User angelegt.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$roles = $pdo->query("SELECT name, description FROM nexus_roles ORDER BY name")->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
||||
$users = $pdo->query("SELECT id, email, role, is_active, created_at FROM nexus_users ORDER BY id DESC")->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
||||
?>
|
||||
<div class="card">
|
||||
<div class="pill">Userverwaltung</div>
|
||||
<h1 style="margin-top:.75rem;">User & Rollen</h1>
|
||||
<p class="muted">Admin kann Module aktivieren/deaktivieren, Benutzer können Module nutzen.</p>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="bg-red-900 border-l-4 border-red-500 text-red-100 p-4 mb-6" role="alert">
|
||||
<?= e($error) ?>
|
||||
</div>
|
||||
<?php elseif ($notice): ?>
|
||||
<div class="card" style="margin-top:1rem; border-color:var(--accent-2);">
|
||||
<?= e($notice) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="margin-top:1.5rem;" class="grid">
|
||||
<div class="card" style="background:var(--panel-2);">
|
||||
<strong>Rollen</strong>
|
||||
<ul style="margin-top:.5rem;">
|
||||
<?php foreach ($roles as $r): ?>
|
||||
<li><?= e($r['name']) ?> <span class="muted"><?= e($r['description'] ?? '') ?></span></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
|
||||
<form method="post" style="margin-top:1rem; display:grid; gap:10px;">
|
||||
<input type="hidden" name="action" value="add_role">
|
||||
<input type="text" name="role" placeholder="Rollenname (z. B. admin)">
|
||||
<input type="text" name="description" placeholder="Beschreibung">
|
||||
<button class="cta-button" type="submit">Rolle hinzufügen</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card" style="background:var(--panel-2);">
|
||||
<strong>User anlegen</strong>
|
||||
<form method="post" style="margin-top:1rem; display:grid; gap:10px;">
|
||||
<input type="hidden" name="action" value="add_user">
|
||||
<input type="email" name="email" placeholder="E-Mail">
|
||||
<input type="password" name="password" placeholder="Passwort">
|
||||
<input type="text" name="role" placeholder="Rolle (admin|user|...)">
|
||||
<button class="cta-button" type="submit">User anlegen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style="margin-top:1.5rem;">Userliste</h3>
|
||||
<div style="margin-top:.5rem; background:var(--panel-2);" class="card">
|
||||
<table class="min-w-full divide-y divide-gray-700">
|
||||
<thead class="bg-gray-900">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">E-Mail</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Rolle</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Aktiv</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Erstellt</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-gray-800 divide-y divide-gray-700">
|
||||
<?php foreach ($users as $u): ?>
|
||||
<tr>
|
||||
<td class="px-6 py-4 text-sm"><?= e($u['email']) ?></td>
|
||||
<td class="px-6 py-4 text-sm"><?= e($u['role']) ?></td>
|
||||
<td class="px-6 py-4 text-sm"><?= !empty($u['is_active']) ? 'Ja' : 'Nein' ?></td>
|
||||
<td class="px-6 py-4 text-sm"><?= e((string)$u['created_at']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
$themes = [
|
||||
'light' => 'Light',
|
||||
'ocean' => 'Ocean',
|
||||
'graphite' => 'Graphite',
|
||||
];
|
||||
|
||||
require_auth();
|
||||
|
||||
$current = user_theme();
|
||||
$timezoneOptions = modules()->timezones();
|
||||
$nexusSettings = nexus_settings();
|
||||
$isAdmin = auth_is_admin();
|
||||
$notice = null;
|
||||
$publicDashboards = $isAdmin ? dashboards()->listPublicDashboards() : [];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$settingsSection = trim((string) ($_POST['settings_section'] ?? 'theme'));
|
||||
if ($settingsSection === 'nexus' && $isAdmin) {
|
||||
$displayTimezoneCustom = isset($_POST['display_timezone_custom']) ? '1' : '0';
|
||||
$displayTimezone = trim((string) ($_POST['display_timezone'] ?? ''));
|
||||
$cronTimezone = trim((string) ($_POST['cron_timezone'] ?? ''));
|
||||
$globalHomeDashboardId = (int) ($_POST['global_home_dashboard_id'] ?? 0);
|
||||
|
||||
foreach (['displayTimezone' => $displayTimezone, 'cronTimezone' => $cronTimezone] as $key => $value) {
|
||||
if ($value !== '') {
|
||||
try {
|
||||
new DateTimeZone($value);
|
||||
} catch (\Throwable) {
|
||||
$$key = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$nexusSettings['display_timezone_custom'] = $displayTimezoneCustom;
|
||||
$nexusSettings['display_timezone'] = $displayTimezoneCustom === '1' ? $displayTimezone : '';
|
||||
$nexusSettings['cron_timezone'] = $cronTimezone;
|
||||
$nexusSettings['global_home_dashboard_id'] = $globalHomeDashboardId > 0 ? (string) $globalHomeDashboardId : '';
|
||||
nexus_save_settings($nexusSettings);
|
||||
$nexusSettings = nexus_settings();
|
||||
$notice = 'Nexus-Einstellungen gespeichert.';
|
||||
} else {
|
||||
$theme = (string)($_POST['theme'] ?? 'light');
|
||||
if (!isset($themes[$theme])) {
|
||||
$theme = 'light';
|
||||
}
|
||||
set_user_theme($theme);
|
||||
$current = $theme;
|
||||
$notice = 'Theme gespeichert.';
|
||||
}
|
||||
}
|
||||
|
||||
$systemTimezone = nexus_system_timezone_name();
|
||||
$effectiveDisplayTimezone = nexus_display_timezone_name();
|
||||
$effectiveCronTimezone = nexus_cron_timezone_name();
|
||||
$displayTimezoneCustom = !empty($nexusSettings['display_timezone_custom']);
|
||||
$savedDisplayTimezone = trim((string) ($nexusSettings['display_timezone'] ?? ''));
|
||||
$savedCronTimezone = trim((string) ($nexusSettings['cron_timezone'] ?? ''));
|
||||
$globalHomeDashboardId = (int) ($nexusSettings['global_home_dashboard_id'] ?? 0);
|
||||
|
||||
$GLOBALS['layout_header_base_title'] = 'Nexus Setup';
|
||||
$GLOBALS['layout_header_title'] = 'Nexus Setup';
|
||||
$GLOBALS['layout_header_context'] = 'Allgemein';
|
||||
$GLOBALS['layout_header_text'] = 'Globale Grundeinstellungen, Home-Dashboard und systemweite Standardwerte.';
|
||||
?>
|
||||
<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack">
|
||||
<header class="module-hero submenu-box">
|
||||
<div class="module-hero-top module-hero-top--compact">
|
||||
<nav class="module-tabs" aria-label="Nexus Setup Navigation">
|
||||
<a class="module-button module-button--tab-active" href="/settings">Allgemein</a>
|
||||
<?php if ($isAdmin): ?>
|
||||
<a class="module-button module-button--tab" href="/settings/widgets">Widgets</a>
|
||||
<a class="module-button module-button--tab" href="/integrations">Integrationen</a>
|
||||
<a class="module-button module-button--tab" href="/settings/search-engines">Suchmaschinen</a>
|
||||
<a class="module-button module-button--tab" href="/settings/apps">Apps</a>
|
||||
<a class="module-button module-button--tab" href="/dashboards">Dashboards</a>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
<div class="module-hero-actions module-submenu-actions">
|
||||
<a class="module-button module-button--secondary module-button--small" href="/">Nexus Übersicht</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="section-box">
|
||||
<h1>Nexus Einstellungen</h1>
|
||||
<p class="muted">Persönliche Anzeige, Home-Dashboard und systemweite Standardwerte.</p>
|
||||
|
||||
<?php if ($notice): ?>
|
||||
<div class="section-box" style="margin-top:1rem; border-color:var(--accent-2);">
|
||||
<?= e($notice) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="setup-form" style="margin-top:1rem;">
|
||||
<section class="section-box setup-panel">
|
||||
<div class="setup-panel__head">
|
||||
<div>
|
||||
<span class="pill">Benutzer</span>
|
||||
<h2>Persönliches Design</h2>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" class="setup-form">
|
||||
<input type="hidden" name="settings_section" value="theme">
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Farbpalette</span>
|
||||
<select name="theme">
|
||||
<?php foreach ($themes as $key => $label): ?>
|
||||
<option value="<?= e($key) ?>" <?= $current === $key ? 'selected' : '' ?>>
|
||||
<?= e($label) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-actions setup-actions--footer">
|
||||
<button class="cta-button" type="submit">Design speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<?php if ($isAdmin): ?>
|
||||
<section class="section-box setup-panel">
|
||||
<div class="setup-panel__head">
|
||||
<div>
|
||||
<span class="pill">Nexus</span>
|
||||
<h2>Standard-Zeitzonen</h2>
|
||||
<p class="muted">Diese Werte werden von Modulen als Default übernommen, sofern dort kein eigener Override gesetzt ist.</p>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" class="setup-form">
|
||||
<input type="hidden" name="settings_section" value="nexus">
|
||||
<datalist id="nexus-timezone-options">
|
||||
<?php foreach ($timezoneOptions as $timezoneOption): ?>
|
||||
<option value="<?= e((string) $timezoneOption['value']) ?>"><?= e((string) $timezoneOption['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</datalist>
|
||||
<div class="setup-grid">
|
||||
<div class="setup-field muted">
|
||||
<span>System-Zeitzone</span>
|
||||
<div><?= e($systemTimezone) ?></div>
|
||||
<small class="muted">Diese Zeitzone wird genutzt, wenn keine globale Anzeige-Zeitzone gesetzt ist.</small>
|
||||
</div>
|
||||
<label class="setup-field muted">
|
||||
<span>Öffentliches Home-Dashboard</span>
|
||||
<select name="global_home_dashboard_id">
|
||||
<option value="0">Kein öffentliches Home-Dashboard</option>
|
||||
<?php foreach ($publicDashboards as $dashboard): ?>
|
||||
<option value="<?= (int) ($dashboard['id'] ?? 0) ?>" <?= (int) ($dashboard['id'] ?? 0) === $globalHomeDashboardId ? 'selected' : '' ?>>
|
||||
<?= e((string) ($dashboard['title'] ?? 'Dashboard')) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<small class="muted">Dieses öffentliche Dashboard wird am Root-Pfad angezeigt, wenn Nutzer nicht eingeloggt sind.</small>
|
||||
</label>
|
||||
<div class="setup-field muted">
|
||||
<span>Anzeige-Zeitzone</span>
|
||||
<label style="display:flex; align-items:center; gap:10px;">
|
||||
<input type="checkbox" name="display_timezone_custom" value="1" <?= $displayTimezoneCustom ? 'checked' : '' ?> data-global-display-tz-toggle>
|
||||
<span>Custom-Zeitzone verwenden</span>
|
||||
</label>
|
||||
<div class="setup-db-message setup-db-message--hint">Aktiv: <?= e($effectiveDisplayTimezone) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setup-grid" data-global-display-tz-panel <?= $displayTimezoneCustom ? '' : 'hidden' ?>>
|
||||
<label class="setup-field muted">
|
||||
<span>Custom Anzeige-Zeitzone</span>
|
||||
<input type="text" name="display_timezone" value="<?= e($savedDisplayTimezone !== '' ? $savedDisplayTimezone : $effectiveDisplayTimezone) ?>" list="nexus-timezone-options" autocomplete="off">
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Standard-Zeitzone für Crons</span>
|
||||
<input type="text" name="cron_timezone" value="<?= e($savedCronTimezone !== '' ? $savedCronTimezone : $effectiveCronTimezone) ?>" list="nexus-timezone-options" autocomplete="off">
|
||||
<small class="muted">Wird in Modul-Crons als Standard verwendet. Einzelne Module oder Einträge können das übersteuern.</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-actions setup-actions--footer">
|
||||
<button class="cta-button" type="submit">Nexus-Einstellungen speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
</div></div></div>
|
||||
<?php if ($isAdmin): ?>
|
||||
<script>
|
||||
(() => {
|
||||
const toggle = document.querySelector('[data-global-display-tz-toggle]');
|
||||
const panel = document.querySelector('[data-global-display-tz-panel]');
|
||||
if (!toggle || !panel) return;
|
||||
const sync = () => {
|
||||
panel.hidden = !toggle.checked;
|
||||
};
|
||||
toggle.addEventListener('change', sync);
|
||||
sync();
|
||||
})();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_admin();
|
||||
|
||||
$service = dashboards();
|
||||
$ownerKey = 'system';
|
||||
$notice = null;
|
||||
$error = null;
|
||||
$integrations = $service->listIntegrationsForOwner($ownerKey);
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = trim((string) ($_POST['action'] ?? ''));
|
||||
try {
|
||||
if ($action === 'create_app') {
|
||||
$service->createApp($ownerKey, [
|
||||
'name' => trim((string) ($_POST['name'] ?? '')),
|
||||
'description' => trim((string) ($_POST['description'] ?? '')),
|
||||
'app_url' => trim((string) ($_POST['app_url'] ?? '')),
|
||||
'icon_url' => trim((string) ($_POST['icon_url'] ?? '')),
|
||||
'integration_id' => (int) ($_POST['integration_id'] ?? 0),
|
||||
'visibility' => 'public',
|
||||
]);
|
||||
$notice = 'App gespeichert.';
|
||||
} elseif ($action === 'delete_app') {
|
||||
$service->deleteApp((int) ($_POST['app_id'] ?? 0), $ownerKey);
|
||||
$notice = 'App gelöscht.';
|
||||
}
|
||||
} catch (\Throwable $exception) {
|
||||
$error = $exception->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$apps = $service->listApps($ownerKey, true);
|
||||
|
||||
$GLOBALS['layout_header_base_title'] = 'Nexus Setup';
|
||||
$GLOBALS['layout_header_title'] = 'Nexus Setup';
|
||||
$GLOBALS['layout_header_context'] = 'Apps';
|
||||
$GLOBALS['layout_header_text'] = 'Globale Apps, die Nutzer später ihren Dashboards hinzufügen können.';
|
||||
?>
|
||||
<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack">
|
||||
<header class="module-hero submenu-box">
|
||||
<div class="module-hero-top module-hero-top--compact">
|
||||
<nav class="module-tabs" aria-label="Nexus Setup Navigation">
|
||||
<a class="module-button module-button--tab" href="/settings">Allgemein</a>
|
||||
<a class="module-button module-button--tab" href="/settings/widgets">Widgets</a>
|
||||
<a class="module-button module-button--tab" href="/integrations">Integrationen</a>
|
||||
<a class="module-button module-button--tab" href="/settings/search-engines">Suchmaschinen</a>
|
||||
<a class="module-button module-button--tab-active" href="/settings/apps">Apps</a>
|
||||
<a class="module-button module-button--tab" href="/dashboards">Dashboards</a>
|
||||
</nav>
|
||||
<div class="module-hero-actions module-submenu-actions">
|
||||
<a class="module-button module-button--secondary module-button--small" href="/">Nexus Übersicht</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<?php if ($error !== null): ?>
|
||||
<section class="section-box"><?= e($error) ?></section>
|
||||
<?php elseif ($notice !== null): ?>
|
||||
<section class="section-box"><?= e($notice) ?></section>
|
||||
<?php endif; ?>
|
||||
|
||||
<section class="section-box">
|
||||
<h2>Neue App</h2>
|
||||
<form method="post" class="setup-form">
|
||||
<input type="hidden" name="action" value="create_app">
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Name</span>
|
||||
<input type="text" name="name" required>
|
||||
</label>
|
||||
<label class="setup-field muted">
|
||||
<span>App-URL</span>
|
||||
<input type="url" name="app_url" required placeholder="https://...">
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Beschreibung</span>
|
||||
<input type="text" name="description">
|
||||
</label>
|
||||
<label class="setup-field muted">
|
||||
<span>Icon-URL optional</span>
|
||||
<input type="url" name="icon_url" placeholder="https://.../icon.png">
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Integration optional</span>
|
||||
<select name="integration_id">
|
||||
<option value="0">Keine Integration</option>
|
||||
<?php foreach ($integrations as $integration): ?>
|
||||
<option value="<?= (int) ($integration['id'] ?? 0) ?>"><?= e((string) ($integration['name'] ?? 'Integration')) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-actions setup-actions--footer">
|
||||
<button class="cta-button" type="submit">App speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div class="module-admin-grid">
|
||||
<?php foreach ($apps as $appEntry): ?>
|
||||
<article class="card-box module-admin-card">
|
||||
<div class="module-admin-card__head">
|
||||
<div class="module-admin-card__title">
|
||||
<h2><?= e((string) ($appEntry['name'] ?? 'App')) ?></h2>
|
||||
<p><?= e((string) ($appEntry['description'] ?? ($appEntry['app_url'] ?? ''))) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-admin-actions">
|
||||
<a class="module-button module-button--secondary module-button--small" href="<?= e((string) ($appEntry['app_url'] ?? '#')) ?>" target="_blank" rel="noreferrer">Öffnen</a>
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="delete_app">
|
||||
<input type="hidden" name="app_id" value="<?= (int) ($appEntry['id'] ?? 0) ?>">
|
||||
<button class="module-button module-button--secondary module-button--small" type="submit">Löschen</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div></div></div>
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_admin();
|
||||
|
||||
$service = dashboards();
|
||||
$ownerKey = 'system';
|
||||
$notice = null;
|
||||
$error = null;
|
||||
$integrations = $service->listIntegrationsForOwner($ownerKey);
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = trim((string) ($_POST['action'] ?? ''));
|
||||
try {
|
||||
if ($action === 'create_search_engine') {
|
||||
$service->createSearchEngine($ownerKey, [
|
||||
'name' => trim((string) ($_POST['name'] ?? '')),
|
||||
'short_code' => trim((string) ($_POST['short_code'] ?? '')),
|
||||
'engine_type' => trim((string) ($_POST['engine_type'] ?? 'template')),
|
||||
'template_url' => trim((string) ($_POST['template_url'] ?? '')),
|
||||
'integration_id' => (int) ($_POST['integration_id'] ?? 0),
|
||||
'visibility' => 'public',
|
||||
'is_default' => isset($_POST['is_default']),
|
||||
]);
|
||||
$notice = 'Suchmaschine gespeichert.';
|
||||
} elseif ($action === 'delete_search_engine') {
|
||||
$service->deleteSearchEngine((int) ($_POST['engine_id'] ?? 0), $ownerKey);
|
||||
$notice = 'Suchmaschine gelöscht.';
|
||||
}
|
||||
} catch (\Throwable $exception) {
|
||||
$error = $exception->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$engines = $service->listSearchEngines($ownerKey, true);
|
||||
|
||||
$GLOBALS['layout_header_base_title'] = 'Nexus Setup';
|
||||
$GLOBALS['layout_header_title'] = 'Nexus Setup';
|
||||
$GLOBALS['layout_header_context'] = 'Suchmaschinen';
|
||||
$GLOBALS['layout_header_text'] = 'Globale Suchmaschinen für die spätere Suche im Nexus-System.';
|
||||
?>
|
||||
<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack">
|
||||
<header class="module-hero submenu-box">
|
||||
<div class="module-hero-top module-hero-top--compact">
|
||||
<nav class="module-tabs" aria-label="Nexus Setup Navigation">
|
||||
<a class="module-button module-button--tab" href="/settings">Allgemein</a>
|
||||
<a class="module-button module-button--tab" href="/settings/widgets">Widgets</a>
|
||||
<a class="module-button module-button--tab" href="/integrations">Integrationen</a>
|
||||
<a class="module-button module-button--tab-active" href="/settings/search-engines">Suchmaschinen</a>
|
||||
<a class="module-button module-button--tab" href="/settings/apps">Apps</a>
|
||||
<a class="module-button module-button--tab" href="/dashboards">Dashboards</a>
|
||||
</nav>
|
||||
<div class="module-hero-actions module-submenu-actions">
|
||||
<a class="module-button module-button--secondary module-button--small" href="/">Nexus Übersicht</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<?php if ($error !== null): ?>
|
||||
<section class="section-box"><?= e($error) ?></section>
|
||||
<?php elseif ($notice !== null): ?>
|
||||
<section class="section-box"><?= e($notice) ?></section>
|
||||
<?php endif; ?>
|
||||
|
||||
<section class="section-box">
|
||||
<h2>Neue Suchmaschine</h2>
|
||||
<form method="post" class="setup-form">
|
||||
<input type="hidden" name="action" value="create_search_engine">
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Name</span>
|
||||
<input type="text" name="name" required>
|
||||
</label>
|
||||
<label class="setup-field muted">
|
||||
<span>Kurzcode</span>
|
||||
<input type="text" name="short_code" placeholder="g" required>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Typ</span>
|
||||
<select name="engine_type">
|
||||
<option value="template">Such-Template</option>
|
||||
<option value="integration">Integration</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="setup-field muted">
|
||||
<span>Such-URL</span>
|
||||
<input type="url" name="template_url" placeholder="https://www.google.com/search?q=%s">
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Integration optional</span>
|
||||
<select name="integration_id">
|
||||
<option value="0">Keine Integration</option>
|
||||
<?php foreach ($integrations as $integration): ?>
|
||||
<option value="<?= (int) ($integration['id'] ?? 0) ?>"><?= e((string) ($integration['name'] ?? 'Integration')) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<label class="setup-field muted">
|
||||
<input type="checkbox" name="is_default" value="1">
|
||||
<span>Als globale Standard-Suche setzen</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-actions setup-actions--footer">
|
||||
<button class="cta-button" type="submit">Suchmaschine speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div class="module-admin-grid">
|
||||
<?php foreach ($engines as $engine): ?>
|
||||
<article class="card-box module-admin-card">
|
||||
<div class="module-admin-card__head">
|
||||
<div class="module-admin-card__title">
|
||||
<h2><?= e((string) ($engine['name'] ?? 'Suche')) ?></h2>
|
||||
<p><?= e((string) (($engine['template_url'] ?? '') ?: 'Integration-basierte Suche.')) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-admin-meta">
|
||||
<div class="module-admin-meta__item">
|
||||
<span class="module-admin-meta__label">Kurzcode</span>
|
||||
<strong class="module-admin-badge"><?= e((string) ($engine['short_code'] ?? '')) ?></strong>
|
||||
</div>
|
||||
<div class="module-admin-meta__item">
|
||||
<span class="module-admin-meta__label">Standard</span>
|
||||
<strong class="module-admin-badge<?= !empty($engine['is_default']) ? ' module-admin-badge--success' : '' ?>"><?= !empty($engine['is_default']) ? 'Ja' : 'Nein' ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-admin-actions">
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="delete_search_engine">
|
||||
<input type="hidden" name="engine_id" value="<?= (int) ($engine['id'] ?? 0) ?>">
|
||||
<button class="module-button module-button--secondary module-button--small" type="submit">Löschen</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div></div></div>
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_admin();
|
||||
|
||||
$service = dashboards();
|
||||
$ownerKey = 'system';
|
||||
$notice = null;
|
||||
$error = null;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = trim((string) ($_POST['action'] ?? ''));
|
||||
try {
|
||||
if ($action === 'create_widget') {
|
||||
$service->createWidgetTemplate($ownerKey, [
|
||||
'name' => trim((string) ($_POST['name'] ?? '')),
|
||||
'widget_type' => trim((string) ($_POST['widget_type'] ?? 'link')),
|
||||
'description' => trim((string) ($_POST['description'] ?? '')),
|
||||
'visibility' => 'public',
|
||||
'config' => [
|
||||
'url' => trim((string) ($_POST['target_url'] ?? '')),
|
||||
'bookmarks' => trim((string) ($_POST['bookmarks'] ?? '')),
|
||||
],
|
||||
]);
|
||||
$notice = 'Widget-Vorlage gespeichert.';
|
||||
} elseif ($action === 'delete_widget') {
|
||||
$service->deleteWidgetTemplate((int) ($_POST['widget_id'] ?? 0), $ownerKey);
|
||||
$notice = 'Widget-Vorlage gelöscht.';
|
||||
}
|
||||
} catch (\Throwable $exception) {
|
||||
$error = $exception->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$widgets = $service->listWidgetTemplates($ownerKey, true);
|
||||
|
||||
$GLOBALS['layout_header_base_title'] = 'Nexus Setup';
|
||||
$GLOBALS['layout_header_title'] = 'Nexus Setup';
|
||||
$GLOBALS['layout_header_context'] = 'Widgets';
|
||||
$GLOBALS['layout_header_text'] = 'Globale Widget-Vorlagen, die andere Nutzer in ihre Dashboards übernehmen können.';
|
||||
?>
|
||||
<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack">
|
||||
<header class="module-hero submenu-box">
|
||||
<div class="module-hero-top module-hero-top--compact">
|
||||
<nav class="module-tabs" aria-label="Nexus Setup Navigation">
|
||||
<a class="module-button module-button--tab" href="/settings">Allgemein</a>
|
||||
<a class="module-button module-button--tab-active" href="/settings/widgets">Widgets</a>
|
||||
<a class="module-button module-button--tab" href="/integrations">Integrationen</a>
|
||||
<a class="module-button module-button--tab" href="/settings/search-engines">Suchmaschinen</a>
|
||||
<a class="module-button module-button--tab" href="/settings/apps">Apps</a>
|
||||
<a class="module-button module-button--tab" href="/dashboards">Dashboards</a>
|
||||
</nav>
|
||||
<div class="module-hero-actions module-submenu-actions">
|
||||
<a class="module-button module-button--secondary module-button--small" href="/">Nexus Übersicht</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<?php if ($error !== null): ?>
|
||||
<section class="section-box"><?= e($error) ?></section>
|
||||
<?php elseif ($notice !== null): ?>
|
||||
<section class="section-box"><?= e($notice) ?></section>
|
||||
<?php endif; ?>
|
||||
|
||||
<section class="section-box">
|
||||
<h2>Neue Widget-Vorlage</h2>
|
||||
<form method="post" class="setup-form">
|
||||
<input type="hidden" name="action" value="create_widget">
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Name</span>
|
||||
<input type="text" name="name" required>
|
||||
</label>
|
||||
<label class="setup-field muted">
|
||||
<span>Typ</span>
|
||||
<select name="widget_type">
|
||||
<option value="link">Link</option>
|
||||
<option value="iframe">iFrame</option>
|
||||
<option value="bookmark_group">Linkliste</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Beschreibung</span>
|
||||
<input type="text" name="description">
|
||||
</label>
|
||||
<label class="setup-field muted">
|
||||
<span>Ziel-URL</span>
|
||||
<input type="url" name="target_url" placeholder="https://...">
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Linkliste optional</span>
|
||||
<textarea name="bookmarks" rows="5" placeholder="Name | https://ziel.example Monitoring | https://grafana.example"></textarea>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-actions setup-actions--footer">
|
||||
<button class="cta-button" type="submit">Widget speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div class="module-admin-grid">
|
||||
<?php foreach ($widgets as $widget): ?>
|
||||
<article class="card-box module-admin-card">
|
||||
<div class="module-admin-card__head">
|
||||
<div class="module-admin-card__title">
|
||||
<h2><?= e((string) ($widget['name'] ?? 'Widget')) ?></h2>
|
||||
<p><?= e((string) ($widget['description'] ?? 'Globale Widget-Vorlage.')) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-admin-meta">
|
||||
<div class="module-admin-meta__item">
|
||||
<span class="module-admin-meta__label">Typ</span>
|
||||
<strong class="module-admin-badge"><?= e((string) ($widget['widget_type'] ?? 'link')) ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-admin-actions">
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="delete_widget">
|
||||
<input type="hidden" name="widget_id" value="<?= (int) ($widget['id'] ?? 0) ?>">
|
||||
<button class="module-button module-button--secondary module-button--small" type="submit">Löschen</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div></div></div>
|
||||
26
docs/Umsetzungsanweisung/Old-Nexus/partials/structure/layout_end.php
Executable file
26
docs/Umsetzungsanweisung/Old-Nexus/partials/structure/layout_end.php
Executable file
@@ -0,0 +1,26 @@
|
||||
</main>
|
||||
<?php
|
||||
$requestPath = app()->request()->path();
|
||||
$currentModuleName = current_module_name();
|
||||
if ($currentModuleName === null && preg_match('~^/modules/(?:setup|access)/([a-zA-Z0-9_-]+)~', $requestPath, $moduleMatch)) {
|
||||
$currentModuleName = $moduleMatch[1];
|
||||
}
|
||||
$showModuleDebug = auth_is_admin() && $currentModuleName !== null && module_debug_enabled($currentModuleName);
|
||||
?>
|
||||
<?php if ($showModuleDebug): ?>
|
||||
<?php
|
||||
$nexusDebugPayload = json_encode([
|
||||
'enabled' => true,
|
||||
'entries' => module_debug_entries($currentModuleName),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if (!is_string($nexusDebugPayload)) {
|
||||
$nexusDebugPayload = '{"enabled":false,"entries":[]}';
|
||||
}
|
||||
?>
|
||||
<div id="nexus-debug-root"></div>
|
||||
<script id="nexus-debug-data" type="application/json"><?= $nexusDebugPayload ?></script>
|
||||
<?php endif; ?>
|
||||
<script src="<?= e(app()->assets()->versioned('/assets/js/app.js')) ?>" defer></script>
|
||||
<?php asset_scripts('footer'); ?>
|
||||
</body>
|
||||
</html>
|
||||
127
docs/Umsetzungsanweisung/Old-Nexus/partials/structure/layout_start.php
Executable file
127
docs/Umsetzungsanweisung/Old-Nexus/partials/structure/layout_start.php
Executable file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
$requestPath = app()->request()->path();
|
||||
$currentModuleName = current_module_name();
|
||||
if ($currentModuleName === null && preg_match('~^/modules/(?:setup|access)/([a-zA-Z0-9_-]+)~', $requestPath, $moduleMatch)) {
|
||||
$currentModuleName = $moduleMatch[1];
|
||||
}
|
||||
$currentModule = $currentModuleName !== null ? modules()->get($currentModuleName) : null;
|
||||
$isStagingHost = defined('APP_DOMAIN_PRIMARY') && str_starts_with((string) APP_DOMAIN_PRIMARY, 'staging.');
|
||||
$headerEyebrow = '';
|
||||
$defaultHeaderTitle = $currentModule
|
||||
? (string)($currentModule['title'] ?? $currentModuleName)
|
||||
: ('Nexus' . ($isStagingHost ? ' (staging)' : ''));
|
||||
$headerText = $currentModule ? (string)($currentModule['description'] ?? '') : '';
|
||||
$headerBaseTitle = isset($GLOBALS['layout_header_base_title']) && is_string($GLOBALS['layout_header_base_title']) && trim($GLOBALS['layout_header_base_title']) !== ''
|
||||
? trim($GLOBALS['layout_header_base_title'])
|
||||
: $defaultHeaderTitle;
|
||||
$headerTitle = isset($GLOBALS['layout_header_title']) && is_string($GLOBALS['layout_header_title']) && trim($GLOBALS['layout_header_title']) !== ''
|
||||
? trim($GLOBALS['layout_header_title'])
|
||||
: $headerBaseTitle;
|
||||
$headerContext = isset($GLOBALS['layout_header_context']) && is_string($GLOBALS['layout_header_context'])
|
||||
? trim($GLOBALS['layout_header_context'])
|
||||
: '';
|
||||
$headerText = isset($GLOBALS['layout_header_text']) && is_string($GLOBALS['layout_header_text'])
|
||||
? trim($GLOBALS['layout_header_text'])
|
||||
: $headerText;
|
||||
$headerActions = isset($GLOBALS['layout_header_actions']) && is_array($GLOBALS['layout_header_actions'])
|
||||
? $GLOBALS['layout_header_actions']
|
||||
: [];
|
||||
$auth = app()->auth();
|
||||
$authUser = $auth->user();
|
||||
$isDebugAdmin = auth_is_admin();
|
||||
$isCurrentModuleDebugEnabled = $isDebugAdmin && $currentModuleName !== null && module_debug_enabled($currentModuleName);
|
||||
$isNexusDebugEnabled = $isCurrentModuleDebugEnabled;
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Nexus</title>
|
||||
<script>
|
||||
(() => {
|
||||
const moduleName = <?= json_encode($currentModuleName) ?>;
|
||||
const read = (key, fallback) => {
|
||||
try { return localStorage.getItem(key) || fallback; } catch (error) { return fallback; }
|
||||
};
|
||||
const mainTheme = read('nexus.theme', 'day');
|
||||
const mainAccent = read('nexus.accent', 'logo');
|
||||
const moduleTheme = moduleName ? read(`nexus.module.${moduleName}.theme`, 'inherit') : mainTheme;
|
||||
const moduleAccent = moduleName ? read(`nexus.module.${moduleName}.accent`, 'inherit') : mainAccent;
|
||||
document.documentElement.dataset.module = moduleName || '';
|
||||
document.documentElement.dataset.theme = moduleTheme === 'inherit' ? mainTheme : moduleTheme;
|
||||
document.documentElement.dataset.accent = moduleAccent === 'inherit' ? mainAccent : moduleAccent;
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="<?= e(app()->assets()->versioned('/assets/css/app.css')) ?>">
|
||||
<?php asset_styles(); ?>
|
||||
<?php asset_scripts('header'); ?>
|
||||
</head>
|
||||
<body data-nexus-debug-admin="<?= $isDebugAdmin ? '1' : '0' ?>" data-nexus-debug-enabled="<?= $isNexusDebugEnabled ? '1' : '0' ?>" data-module-debug-enabled="<?= $isCurrentModuleDebugEnabled ? '1' : '0' ?>">
|
||||
<main class="main-shell">
|
||||
<section class="home-hero app-header main-header-box" data-module-name="<?= e((string)($currentModuleName ?? '')) ?>">
|
||||
<a class="brand-mark" href="/" aria-label="Nexus">
|
||||
<img src="/assets/images/logo.png" alt="Nexus Logo">
|
||||
</a>
|
||||
<div class="brand-copy">
|
||||
<?php if ($headerEyebrow !== ''): ?>
|
||||
<span class="eyebrow"><?= e($headerEyebrow) ?></span>
|
||||
<?php endif; ?>
|
||||
<h1>
|
||||
<?= e($headerTitle) ?>
|
||||
<?php if ($headerContext !== ''): ?>
|
||||
<span class="module-page-context"> / <?= e($headerContext) ?></span>
|
||||
<?php endif; ?>
|
||||
</h1>
|
||||
<?php if ($headerText !== ''): ?>
|
||||
<p><?= e($headerText) ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="theme-switcher" aria-label="Darstellung">
|
||||
<?php if ($headerActions !== []): ?>
|
||||
<div class="header-actions">
|
||||
<?php foreach ($headerActions as $headerAction): ?>
|
||||
<?php
|
||||
if (!is_array($headerAction)) {
|
||||
continue;
|
||||
}
|
||||
$actionHref = trim((string) ($headerAction['href'] ?? ''));
|
||||
$actionLabel = trim((string) ($headerAction['label'] ?? ''));
|
||||
if ($actionHref === '' || $actionLabel === '') {
|
||||
continue;
|
||||
}
|
||||
?>
|
||||
<a class="nav-link" href="<?= e($actionHref) ?>"><?= e($actionLabel) ?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($auth->isEnabled()): ?>
|
||||
<a class="auth-pill" href="<?= $authUser === null ? '/auth/login' : '/auth/logout' ?>">
|
||||
<?= $authUser === null ? 'Login' : 'Logout ' . e((string)($authUser['username'] ?? $authUser['name'] ?? '')) ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<label>
|
||||
<span>Modus</span>
|
||||
<select data-theme-mode data-theme-scope="<?= $currentModuleName !== null ? 'module' : 'main' ?>">
|
||||
<?php if ($currentModuleName !== null): ?>
|
||||
<option value="inherit">Wie Hauptsystem</option>
|
||||
<?php endif; ?>
|
||||
<option value="day">Day</option>
|
||||
<option value="night">Night</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Farbe</span>
|
||||
<select data-theme-accent data-theme-scope="<?= $currentModuleName !== null ? 'module' : 'main' ?>">
|
||||
<?php if ($currentModuleName !== null): ?>
|
||||
<option value="inherit">Wie Hauptsystem</option>
|
||||
<?php endif; ?>
|
||||
<option value="logo">Logo</option>
|
||||
<option value="pink">Pink</option>
|
||||
<option value="cyan">Cyan</option>
|
||||
<option value="orange">Orange</option>
|
||||
<option value="green">Gruen</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
Reference in New Issue
Block a user