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

+

+

+
+
+ Administrators only + Desktop Auth + Section: +
+
+ + +
+
+ Ergebnis +
    + +
  • + +
+
+
+ + + +
+
+ Bitte pruefen +
    + +
  • + +
+
+
+ + + +
+
+
+
+

Queue

+

Wartende Registrierungen

+
+ offen +
+ +
+ +
Aktuell gibt es keine offenen Registrierungen.
+ + + + + + + + + +
+
+ +
+
+
+

Approval

+

Freischaltung

+
+
+ + +
+
Benutzer
+
E-Mail
+
Status
+
+ +
+ + + + + +
+ Gruppen fuer Freischaltung +
+ + + +
+
+ + + +
+ +
+
+ +
Keine Anfrage ausgewaehlt.
+ +
+
+ +
+
+
+

Existing User

+

Bestehenden Benutzer verwalten

+
+
+ +
+ + + + + +
+ Gruppen fuer Aktivierung +
+ + + +
+
+ +
+ + +
+
+
+ +
+
+
+

Reset

+

Passwort zuruecksetzen

+
+
+ +
+ + + + + + + +
+ + +
+
+
+ +
+
+
+
+ User Management - + + -
-
- - - - - - - - - - -
- - -
- -
-

Freischaltung

-

Benutzer:

-

E-Mail:

-

Status:

-
- - - - -
-

Bestehenden Benutzer verwalten

-
- - - -
-

Passwort zurücksetzen

-
- - -
-
-
-
+ + + 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); + }; +})();