This commit is contained in:
2026-03-04 01:58:26 +01:00
parent a7844c145a
commit c360663603
23 changed files with 1115 additions and 81 deletions

11
public/page/404.php Normal file
View File

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

View File

@@ -0,0 +1,49 @@
<?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'] ?? '');
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);
$user = [
'sub' => (string)($claims['sub'] ?? ''),
'email' => (string)($claims['email'] ?? ''),
'name' => (string)($claims['name'] ?? ($claims['preferred_username'] ?? '')),
'groups' => $groups,
'id_token' => $idToken,
];
$_SESSION['auth_user'] = $user;
redirect('/');

View File

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

View File

@@ -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']);
if ($config->authEnabled) {
$client = new OidcClient($config);
$url = $client->logoutUrl($idToken);
if ($url) {
redirect($url);
}
}
redirect('/');

90
public/page/debug.php Normal file
View File

@@ -0,0 +1,90 @@
<?php
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)) {
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 (isset($_GET['raw']) && $_GET['raw'] === '1') {
header('Content-Type: text/plain; charset=utf-8');
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: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; ?>

View File

@@ -4,6 +4,7 @@ $error = null;
$notice = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
require_admin();
$name = (string)($_POST['module'] ?? '');
$action = (string)($_POST['action'] ?? '');
@@ -18,8 +19,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
?>
<div class="card">
<?php require_auth(); ?>
<div class="pill">Module</div>
<h1 style="margin-top:.75rem;">Module verwalten</h1>
<p class="muted">Hier siehst du nur aktive Module. Installierte Module kannst du unten verwalten.</p>
<?php if ($error): ?>
<div class="bg-red-900 border-l-4 border-red-500 text-red-100 p-4 mb-6" role="alert">
@@ -33,31 +36,28 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<div style="margin-top:1rem;" class="grid">
<?php foreach ($modules as $module): ?>
<?php if (empty($module['enabled'])) { continue; } ?>
<div class="card" style="background:var(--panel-2);">
<div style="display:flex; align-items:center; justify-content:space-between; gap:12px;">
<div>
<strong><?= e($module['title']) ?></strong>
<div class="muted" style="font-size:.85rem;"><?= e($module['description'] ?? '') ?></div>
</div>
<?php if (!empty($module['enabled'])): ?>
<span class="pill" style="border-color:var(--accent-2); color:var(--accent-2);">aktiv</span>
<?php else: ?>
<span class="pill">inaktiv</span>
<?php endif; ?>
<span class="pill" style="border-color:var(--accent-2); color:var(--accent-2);">aktiv</span>
</div>
<div style="margin-top:.75rem; display:flex; gap:10px; flex-wrap:wrap;">
<a class="nav-link" href="/module/<?= e($module['name']) ?>">Öffnen</a>
<a class="nav-link" href="/modules/setup/<?= e($module['name']) ?>">Setup</a>
<form method="post" style="margin:0;">
<input type="hidden" name="module" value="<?= e($module['name']) ?>">
<?php if (!empty($module['enabled'])): ?>
<button class="cta-button" name="action" value="disable" style="background:var(--panel); color:var(--text);">Deaktivieren</button>
<?php else: ?>
<button class="cta-button" name="action" value="enable">Aktivieren</button>
<?php endif; ?>
<button class="cta-button" name="action" value="disable" style="background:var(--panel); color:var(--text);">Deaktivieren</button>
</form>
</div>
</div>
<?php endforeach; ?>
</div>
<div style="margin-top:1.5rem;">
<a class="nav-link" href="/modules/install">Modul installieren/aktivieren</a>
</div>
</div>

View File

@@ -0,0 +1,78 @@
<?php
$modules = modules()->all();
$error = null;
$notice = null;
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();
} 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="card">
<div class="pill">Module</div>
<h1 style="margin-top:.75rem;">Module installieren/aktivieren</h1>
<p class="muted">Erkannte Module basieren auf Ordnern in <code>modules/</code>.</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; ?>
<h3 style="margin-top:1.25rem;">Aktive Module</h3>
<div style="margin-top:.5rem;" class="grid">
<?php foreach ($active as $module): ?>
<div class="card" style="background:var(--panel-2);">
<strong><?= e($module['title']) ?></strong>
<div class="muted" style="font-size:.85rem;"><?= e($module['description'] ?? '') ?></div>
<div style="margin-top:.75rem; display:flex; gap:10px;">
<a class="nav-link" href="/module/<?= e($module['name']) ?>">Öffnen</a>
<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>
</div>
<?php endforeach; ?>
</div>
<h3 style="margin-top:1.5rem;">Deaktivierte Module</h3>
<div style="margin-top:.5rem;" class="grid">
<?php foreach ($inactive as $module): ?>
<div class="card" style="background:var(--panel-2);">
<strong><?= e($module['title']) ?></strong>
<div class="muted" style="font-size:.85rem;"><?= e($module['description'] ?? '') ?></div>
<div style="margin-top:.75rem; display:flex; gap:10px;">
<a class="nav-link" href="/modules/setup/<?= e($module['name']) ?>">Setup</a>
<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>
</div>
<?php endforeach; ?>
</div>
</div>

View File

@@ -4,6 +4,8 @@ $module = modules()->get($moduleName);
$error = null;
$notice = null;
require_admin();
if (!$module) {
http_response_code(404);
echo '<div class="card">Modul nicht gefunden.</div>';

47
public/page/settings.php Normal file
View File

@@ -0,0 +1,47 @@
<?php
$themes = [
'light' => 'Light',
'ocean' => 'Ocean',
'graphite' => 'Graphite',
];
require_auth();
$current = user_theme();
$notice = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$theme = (string)($_POST['theme'] ?? 'light');
if (!isset($themes[$theme])) {
$theme = 'light';
}
set_user_theme($theme);
$current = $theme;
$notice = 'Theme gespeichert.';
}
?>
<div class="card">
<div class="pill">Einstellungen</div>
<h1 style="margin-top:.75rem;">User-Design</h1>
<p class="muted">Wähle deine persönliche Farbpalette.</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:12px; max-width:360px;">
<label class="muted" style="display:grid; gap:6px;">
<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>
<button class="cta-button" type="submit">Speichern</button>
</form>
</div>

128
public/page/users.php Normal file
View File

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