diff --git a/config/apps.php b/config/apps.php
index 4521d710..a7415c1d 100644
--- a/config/apps.php
+++ b/config/apps.php
@@ -88,7 +88,7 @@ return [
'icon' => 'UM',
'entry_route' => '/admin/users/',
'window_mode' => 'window',
- 'content_mode' => 'iframe',
+ 'content_mode' => 'native-module',
'default_width' => 1180,
'default_height' => 760,
'supports_widget' => false,
@@ -96,6 +96,20 @@ return [
'module_name' => 'admin',
'required_groups' => ['administrators'],
'summary' => 'Benutzerverwaltung fuer Freischaltungen, Aktivierung, Deaktivierung und Passwort-Reset.',
+ 'native_module' => [
+ 'initializer' => 'initUserManagementApp',
+ 'options' => [
+ 'entryRoute' => '/admin/users/',
+ ],
+ 'assets' => [
+ 'styles' => [
+ ['href' => '/assets/apps/user-management/app.css'],
+ ],
+ 'scripts' => [
+ ['src' => '/assets/apps/user-management/app.js'],
+ ],
+ ],
+ ],
],
[
'app_id' => 'desktop-login',
diff --git a/public/admin/users/index.php b/public/admin/users/index.php
index 027146e8..841b1f1d 100644
--- a/public/admin/users/index.php
+++ b/public/admin/users/index.php
@@ -4,46 +4,62 @@ declare(strict_types=1);
require_once dirname(__DIR__, 3) . '/src/App/bootstrap.php';
-use App\ConfigLoader;
use App\AccountGate;
+use App\ConfigLoader;
use App\RegistrationService;
+use ModulesCore\ModuleHttp;
-session_start();
+if (session_status() !== PHP_SESSION_ACTIVE) {
+ session_start();
+}
$projectRoot = dirname(__DIR__, 3);
+ModuleHttp::requireDesktopAccess($projectRoot);
+
$accountGate = new AccountGate(ConfigLoader::load($projectRoot, 'registration'));
$registration = new RegistrationService($projectRoot, ConfigLoader::load($projectRoot, 'registration'));
-if (isset($_SESSION['desktop_auth']) && is_array($_SESSION['desktop_auth'])) {
- $currentAuthUser = is_array($_SESSION['desktop_auth']['user'] ?? null) ? $_SESSION['desktop_auth']['user'] : [];
- $accountCheck = $accountGate->checkUsername((string) ($currentAuthUser['username'] ?? ''));
-
- if (!($accountCheck['allowed'] ?? false)) {
- unset($_SESSION['desktop_auth']);
- $target = (string) ($accountCheck['state'] ?? '') === 'pending'
- ? '/auth/pending/?username=' . urlencode((string) ($currentAuthUser['username'] ?? ''))
- : '/auth/inactive/?message=' . urlencode((string) ($accountCheck['message'] ?? 'Dieses Konto ist noch nicht freigeschaltet.'));
- header('Location: ' . $target, true, 302);
- exit;
- }
-}
-
$currentUser = is_array($_SESSION['desktop_auth']['user'] ?? null) ? $_SESSION['desktop_auth']['user'] : [];
-$currentGroups = array_map(static fn (string $group): string => strtolower(trim($group, '/')), array_values(array_map('strval', (array) ($currentUser['groups'] ?? []))));
+$currentGroups = array_map(
+ static fn (string $group): string => strtolower(trim($group, '/')),
+ array_values(array_map('strval', (array) ($currentUser['groups'] ?? [])))
+);
+$isPartial = !empty($_GET['partial']);
if (!in_array('administrators', $currentGroups, true)) {
http_response_code(403);
- echo 'Kein Zugriff auf User Management.';
- exit;
+ $forbidden = '
Kein Zugriff auf User Management.
';
+ if ($isPartial) {
+ echo $forbidden;
+ return;
+ }
+
+ ?>
+
+
+
+
+ User Management
+
+
+
+ = $forbidden ?>
+ deactivateUser($manualUsername);
- ($result['success'] ?? false) ? $messages[] = (string) ($result['message'] ?? '') : $errors[] = (string) ($result['message'] ?? '');
+ if (($result['success'] ?? false) === true) {
+ $messages[] = (string) ($result['message'] ?? '');
+ } else {
+ $errors[] = (string) ($result['message'] ?? '');
+ }
+ $section = 'users';
} elseif ($action === 'activate-user') {
$groupDns = array_values(array_filter(array_map('strval', (array) ($_POST['manual_group_dns'] ?? []))));
$result = $registration->activateExistingUser($manualUsername, $groupDns);
- ($result['success'] ?? false) ? $messages[] = (string) ($result['message'] ?? '') : $errors[] = (string) ($result['message'] ?? '');
+ if (($result['success'] ?? false) === true) {
+ $messages[] = (string) ($result['message'] ?? '');
+ } else {
+ $errors[] = (string) ($result['message'] ?? '');
+ }
+ $section = 'users';
} elseif ($action === 'send-reset-mail') {
$origin = ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http') . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost');
$result = $registration->sendPasswordReset($manualResetUsername, $origin);
- ($result['success'] ?? false) ? $messages[] = (string) ($result['message'] ?? '') : $errors[] = (string) ($result['message'] ?? '');
+ if (($result['success'] ?? false) === true) {
+ $messages[] = (string) ($result['message'] ?? '');
+ } else {
+ $errors[] = (string) ($result['message'] ?? '');
+ }
+ $section = 'reset';
} elseif ($action === 'set-password') {
if (strlen($manualResetPassword) < 12) {
$errors[] = 'Das neue Passwort muss mindestens 12 Zeichen lang sein.';
} else {
$result = $registration->setUserPassword($manualResetUsername, $manualResetPassword);
- ($result['success'] ?? false) ? $messages[] = (string) ($result['message'] ?? '') : $errors[] = (string) ($result['message'] ?? '');
+ if (($result['success'] ?? false) === true) {
+ $messages[] = (string) ($result['message'] ?? '');
+ } else {
+ $errors[] = (string) ($result['message'] ?? '');
+ }
}
+ $section = 'reset';
}
}
}
@@ -99,153 +136,286 @@ $availableGroups = $registration->availableGroups();
$selected = $selectedId !== ''
? $registration->find($selectedId)
: ($pendingRequests[0] ?? null);
+
+if ($selectedId === '' && is_array($selected) && isset($selected['id'])) {
+ $selectedId = (string) $selected['id'];
+}
+
+$h = static fn (?string $value): string => htmlspecialchars($value ?? '', ENT_QUOTES);
+$sectionUrl = static function (string $targetSection, string $requestId = ''): string {
+ $query = ['section' => $targetSection];
+ if ($requestId !== '') {
+ $query['id'] = $requestId;
+ }
+
+ return '/admin/users/?' . http_build_query($query, '', '&', PHP_QUERY_RFC3986);
+};
+
+$sectionMeta = [
+ 'pending' => [
+ 'label' => 'Freischaltungen',
+ 'title' => 'Offene Registrierungen',
+ 'description' => 'Neue Registrierungen pruefen, Gruppen zuweisen und Freischaltungen sauber dokumentieren.',
+ 'nav' => 'Anfragen sichten und direkt freigeben.',
+ ],
+ 'users' => [
+ 'label' => 'Benutzerstatus',
+ 'title' => 'Bestehende Benutzer',
+ 'description' => 'LDAP-Benutzer aktivieren, deaktivieren und Gruppen fuer die Desktop-Nutzung zuweisen.',
+ 'nav' => 'Aktivierung und Sperrung vorhandener Konten.',
+ ],
+ 'reset' => [
+ 'label' => 'Passwort-Reset',
+ 'title' => 'Passwortverwaltung',
+ 'description' => 'Reset-Mails ausloesen oder ein Passwort direkt als Administrator setzen.',
+ 'nav' => 'Ruecksetzen oder direkt neu vergeben.',
+ ],
+];
+
+ob_start();
+?>
+
+
+
+
+
+
+
+
+
User Management
+
= $h($sectionMeta[$section]['title']) ?>
+
= $h($sectionMeta[$section]['description']) ?>
+
+
+ Administrators only
+ Desktop Auth
+ Section: = $h($sectionMeta[$section]['label']) ?>
+
+
+
+
+
+
+
Ergebnis
+
+
+ = $h($message) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Queue
+
Wartende Registrierungen
+
+
= count($pendingRequests) ?> offen
+
+
+
+
+
+
+
+
+
Approval
+
Freischaltung
+
+
+
+
+
+
Benutzer = $h((string) ($selected['username'] ?? '')) ?>
+
E-Mail = $h((string) ($selected['email'] ?? '')) ?>
+
Status = $h((string) ($selected['status'] ?? '')) ?>
+
+
+
+
+ Keine Anfrage ausgewaehlt.
+
+
+
+
+
+
+
+
Existing User
+
Bestehenden Benutzer verwalten
+
+
+
+
+
+
+
+
+ Benutzername
+
+
+
+
+ Gruppen fuer Aktivierung
+
+
+
+
+
+ = $h((string) ($group['label'] ?? '')) ?>
+ = $h((string) ($group['dn'] ?? '')) ?>
+
+
+
+
+
+
+
+ Benutzer aktivieren
+ Benutzer deaktivieren
+
+
+
+
+
+
+
+
Reset
+
Passwort zuruecksetzen
+
+
+
+
+
+
+
+
+ Benutzername
+
+
+
+
+ Neues Passwort fuer direkten Reset
+
+
+
+
+ Reset-Mail senden
+ Passwort direkt setzen
+
+
+
+
+
+
+
+
+
User Management
-
+
+
-
-
-
-
Kusche.Berlin
-
User Management
-
Nur für Benutzer in der Gruppe administrators . Hier werden Neuregistrierungen freigeschaltet sowie bestehende LDAP-Benutzer aktiviert, deaktiviert oder zurückgesetzt.
-
-
-
-
-
Ergebnis
-
-
- = htmlspecialchars($message, ENT_QUOTES) ?>
-
-
-
-
-
-
-
-
Bitte prüfen
-
-
- = htmlspecialchars($error, ENT_QUOTES) ?>
-
-
-
-
-
-
-
-
-
-
-
-
Freischaltung
-
Benutzer: = htmlspecialchars((string) ($selected['username'] ?? ''), ENT_QUOTES) ?>
-
E-Mail: = htmlspecialchars((string) ($selected['email'] ?? ''), ENT_QUOTES) ?>
-
Status: = htmlspecialchars((string) ($selected['status'] ?? ''), ENT_QUOTES) ?>
-
-
-
-
-
-
-
-
- Zielgruppen für Freischaltung
-
-
-
-
-
- = htmlspecialchars($group['label'], ENT_QUOTES) ?>
-
-
-
-
-
-
-
- Admin-Notiz
-
-
-
-
- Freischaltung bestätigen
-
-
-
-
-
-
Bestehenden Benutzer verwalten
-
-
-
-
-
- Benutzername
-
-
-
-
- Gruppen für Aktivierung
-
-
-
-
-
- = htmlspecialchars($group['label'], ENT_QUOTES) ?>
-
-
-
-
-
-
-
- Benutzer aktivieren
- Benutzer deaktivieren
-
-
-
-
-
Passwort zurücksetzen
-
-
-
-
-
- Benutzername
-
-
-
-
- Neues Passwort für direkten Reset
-
-
-
-
- Reset-Mail senden
- Passwort direkt setzen
-
-
-
-
-
-
+ = $pageContent ?>
+
+
diff --git a/public/assets/apps/user-management/app.css b/public/assets/apps/user-management/app.css
new file mode 100644
index 00000000..5b9bab50
--- /dev/null
+++ b/public/assets/apps/user-management/app.css
@@ -0,0 +1,262 @@
+#user-management-app {
+ min-height: 100%;
+ color: #0f172a;
+ font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
+}
+
+#user-management-app,
+#user-management-app * {
+ box-sizing: border-box;
+}
+
+#user-management-app.is-loading {
+ opacity: 0.7;
+ pointer-events: none;
+}
+
+#user-management-app .um-shell,
+#user-management-app .um-frame,
+#user-management-app .um-main,
+#user-management-app .um-panel {
+ min-height: 100%;
+}
+
+#user-management-app .um-brand,
+#user-management-app .um-copy,
+#user-management-app .um-meta {
+ margin: 0;
+}
+
+#user-management-app .um-kicker {
+ margin: 0 0 10px;
+ font-size: 12px;
+ font-weight: 700;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ color: #2563eb;
+}
+
+#user-management-app .um-nav-button.is-active {
+ color: #0f172a;
+}
+
+#user-management-app .um-side-card,
+#user-management-app .um-card {
+ display: grid;
+ gap: 14px;
+}
+
+#user-management-app .um-side-title,
+#user-management-app .um-section-title {
+ margin: 0;
+ font-size: 18px;
+ line-height: 1.2;
+}
+
+#user-management-app .um-side-metric,
+#user-management-app .um-summary-item {
+ display: grid;
+ gap: 4px;
+}
+
+#user-management-app .um-side-metric span,
+#user-management-app .um-summary-item span,
+#user-management-app .um-request-card span,
+#user-management-app .um-field span,
+#user-management-app .um-meta,
+#user-management-app .um-empty,
+#user-management-app .um-message,
+#user-management-app .um-checkbox-item p {
+ font-size: 14px;
+ line-height: 1.5;
+ color: #475569;
+}
+
+#user-management-app .um-grid {
+ display: grid;
+ gap: 18px;
+}
+
+#user-management-app .um-grid--split {
+ grid-template-columns: minmax(280px, 0.95fr) minmax(0, 1.35fr);
+}
+
+#user-management-app .um-section-head {
+ display: flex;
+ justify-content: space-between;
+ gap: 16px;
+ align-items: flex-start;
+}
+
+#user-management-app .um-request-list,
+#user-management-app .um-checkbox-grid {
+ display: grid;
+ gap: 12px;
+}
+
+#user-management-app .um-request-card {
+ display: grid;
+ gap: 4px;
+ padding: 14px 16px;
+ border: 1px solid rgba(148, 163, 184, 0.24);
+ border-radius: 16px;
+ background: rgba(255, 255, 255, 0.9);
+ color: inherit;
+ text-decoration: none;
+ transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
+}
+
+#user-management-app .um-request-card:hover,
+#user-management-app .um-request-card.is-active {
+ border-color: rgba(37, 99, 235, 0.4);
+ box-shadow: inset 0 0 0 1px rgba(37, 99, 235, 0.35);
+ transform: translateY(-1px);
+}
+
+#user-management-app .um-request-card strong,
+#user-management-app .um-checkbox-item strong {
+ font-size: 15px;
+}
+
+#user-management-app .um-request-status {
+ font-size: 12px;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: #1d4ed8;
+}
+
+#user-management-app .um-summary-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
+ gap: 12px;
+}
+
+#user-management-app .um-summary-item {
+ padding: 14px 16px;
+ border-radius: 16px;
+ background: rgba(248, 250, 252, 0.9);
+ border: 1px solid rgba(148, 163, 184, 0.18);
+}
+
+#user-management-app .um-summary-item strong {
+ font-size: 15px;
+ color: #0f172a;
+}
+
+#user-management-app .um-form {
+ display: grid;
+ gap: 16px;
+}
+
+#user-management-app .um-field {
+ display: grid;
+ gap: 8px;
+}
+
+#user-management-app .um-field input,
+#user-management-app .um-field textarea {
+ width: 100%;
+ min-height: 46px;
+ padding: 12px 14px;
+ border-radius: 14px;
+ border: 1px solid rgba(148, 163, 184, 0.24);
+ background: rgba(255, 255, 255, 0.95);
+ color: #0f172a;
+ font: inherit;
+}
+
+#user-management-app .um-field textarea {
+ min-height: 120px;
+ resize: vertical;
+}
+
+#user-management-app .um-fieldset {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ display: grid;
+ gap: 10px;
+}
+
+#user-management-app .um-fieldset legend {
+ margin-bottom: 8px;
+ font-size: 14px;
+ font-weight: 700;
+ color: #0f172a;
+}
+
+#user-management-app .um-checkbox-item {
+ align-items: flex-start;
+}
+
+#user-management-app .um-checkbox-item input {
+ margin-top: 2px;
+}
+
+#user-management-app .um-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ align-items: center;
+}
+
+#user-management-app .um-primary {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 44px;
+ padding: 10px 16px;
+ border: 0;
+ border-radius: 14px;
+ background: linear-gradient(135deg, #2563eb, #1d4ed8);
+ color: #fff;
+ font: inherit;
+ font-weight: 700;
+ cursor: pointer;
+}
+
+#user-management-app .um-secondary,
+#user-management-app .um-danger {
+ min-height: 42px;
+}
+
+#user-management-app .um-danger {
+ border-color: rgba(239, 68, 68, 0.28);
+ color: #b91c1c;
+}
+
+#user-management-app .um-message {
+ display: grid;
+ gap: 8px;
+}
+
+#user-management-app .um-message strong {
+ font-size: 15px;
+ color: #0f172a;
+}
+
+#user-management-app .um-message.is-error {
+ color: #b91c1c;
+}
+
+#user-management-app .um-message.is-success {
+ color: #047857;
+}
+
+#user-management-app .um-list {
+ margin: 0;
+ padding-left: 18px;
+}
+
+@media (max-width: 980px) {
+ #user-management-app .um-grid--split {
+ grid-template-columns: 1fr;
+ }
+
+ #user-management-app .um-section-head,
+ #user-management-app .um-actions {
+ flex-direction: column;
+ align-items: stretch;
+ }
+}
diff --git a/public/assets/apps/user-management/app.js b/public/assets/apps/user-management/app.js
new file mode 100644
index 00000000..f4207511
--- /dev/null
+++ b/public/assets/apps/user-management/app.js
@@ -0,0 +1,120 @@
+(() => {
+ const normalizeUrl = (url) => {
+ const next = new URL(url, window.location.origin);
+ next.searchParams.set('partial', '1');
+ return next.toString();
+ };
+
+ const renderError = (host, message) => {
+ host.innerHTML = `
+
+
+
+
+
+ ${String(message || 'User Management konnte nicht geladen werden.')}
+
+
+
+
+
+ `;
+ };
+
+ const bindInteractions = (host, loadRoute) => {
+ host.querySelectorAll('a[href]').forEach((link) => {
+ if (link.dataset.umBound === '1') {
+ return;
+ }
+
+ const href = String(link.getAttribute('href') || '');
+ if (!href.startsWith('/admin/users/')) {
+ return;
+ }
+
+ link.dataset.umBound = '1';
+ link.addEventListener('click', (event) => {
+ event.preventDefault();
+ loadRoute(href);
+ });
+ });
+
+ host.querySelectorAll('form[action]').forEach((form) => {
+ if (form.dataset.umBound === '1') {
+ return;
+ }
+
+ const action = String(form.getAttribute('action') || '');
+ if (!action.startsWith('/admin/users/')) {
+ return;
+ }
+
+ form.dataset.umBound = '1';
+ form.addEventListener('submit', async (event) => {
+ event.preventDefault();
+
+ try {
+ host.classList.add('is-loading');
+ const response = await fetch(normalizeUrl(action), {
+ method: 'POST',
+ credentials: 'same-origin',
+ body: new FormData(form),
+ });
+
+ const html = await response.text();
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`);
+ }
+
+ host.innerHTML = html;
+ bindInteractions(host, loadRoute);
+ } catch (error) {
+ renderError(host, error?.message || 'Formular konnte nicht verarbeitet werden.');
+ } finally {
+ host.classList.remove('is-loading');
+ }
+ });
+ });
+ };
+
+ window.initUserManagementApp = function initUserManagementApp(host, options = {}) {
+ if (!(host instanceof Element)) {
+ return;
+ }
+
+ if (host.dataset.umAppBound === '1') {
+ return;
+ }
+
+ host.dataset.umAppBound = '1';
+
+ const entryRoute = String(options.entryRoute || '/admin/users/');
+ const loadRoute = async (route) => {
+ try {
+ host.classList.add('is-loading');
+ const response = await fetch(normalizeUrl(route), {
+ credentials: 'same-origin',
+ headers: { Accept: 'text/html' },
+ });
+ const html = await response.text();
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`);
+ }
+
+ host.innerHTML = html;
+ bindInteractions(host, loadRoute);
+ } catch (error) {
+ renderError(host, error?.message || 'User Management konnte nicht geladen werden.');
+ } finally {
+ host.classList.remove('is-loading');
+ }
+ };
+
+ if (options.standalone) {
+ bindInteractions(host, loadRoute);
+ return;
+ }
+
+ loadRoute(entryRoute);
+ };
+})();