asdsad
All checks were successful
Deploy / deploy-staging (push) Successful in 52s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-06-10 22:44:28 +02:00
parent a8f4a4b36a
commit cd5525ff2e
20 changed files with 1951 additions and 351 deletions

View File

@@ -0,0 +1,222 @@
<?php
declare(strict_types=1);
require_once dirname(__DIR__, 3) . '/src/App/bootstrap.php';
use App\ConfigLoader;
use App\RegistrationAccess;
use App\RegistrationService;
session_start();
$projectRoot = dirname(__DIR__, 3);
$config = ConfigLoader::load($projectRoot, 'registration');
$registration = new RegistrationService($projectRoot, $config);
$access = new RegistrationAccess($config);
if (!$access->canManage()) {
http_response_code(403);
echo 'Kein Zugriff auf Registrierungsfreigaben.';
exit;
}
$csrfToken = (string) ($_SESSION['registration_admin_token'] ?? '');
if ($csrfToken === '') {
$csrfToken = bin2hex(random_bytes(16));
$_SESSION['registration_admin_token'] = $csrfToken;
}
$currentUser = is_array($_SESSION['desktop_auth']['user'] ?? null) ? $_SESSION['desktop_auth']['user'] : [];
$reviewer = (string) ($currentUser['username'] ?? 'unbekannt');
$selectedId = trim((string) ($_GET['id'] ?? $_POST['id'] ?? ''));
$errors = [];
$notice = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$postedToken = (string) ($_POST['csrf_token'] ?? '');
if ($postedToken === '' || !hash_equals($csrfToken, $postedToken)) {
$errors[] = 'Die Formularprüfung ist fehlgeschlagen.';
} else {
$action = (string) ($_POST['action'] ?? '');
if ($action === 'approve') {
$result = $registration->approve($selectedId, array_map('strval', $_POST), $reviewer);
if (($result['success'] ?? false) !== true) {
$errors = array_values(array_map('strval', (array) ($result['errors'] ?? [])));
} else {
$notice = 'Registrierung freigegeben. Ein LDIF-Entwurf für den inaktiven Benutzer wurde erzeugt.';
}
} elseif ($action === 'reject') {
$result = $registration->reject($selectedId, (string) ($_POST['reject_reason'] ?? ''), $reviewer);
if (($result['success'] ?? false) !== true) {
$errors = array_values(array_map('strval', (array) ($result['errors'] ?? [])));
} else {
$notice = 'Registrierung abgelehnt.';
}
}
}
}
$requests = $registration->all();
$selected = $selectedId !== '' ? $registration->find($selectedId) : ($requests[0] ?? null);
$provisioning = is_array($selected['provisioning'] ?? null) ? $selected['provisioning'] : [];
?><!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Registrierungsfreigaben</title>
<link rel="stylesheet" href="/assets/auth/login.css">
</head>
<body>
<main class="login-shell">
<section class="login-panel login-panel-admin">
<div class="login-panel-top">
<p class="login-kicker">Kusche.Berlin</p>
<h1>Registrierungsfreigaben</h1>
<p class="login-copy">Anfragen werden erst nach Prüfung für LDAP und NAS vorbereitet. Die Freigabe erzeugt aktuell einen LDIF-Entwurf für einen inaktiven Benutzer.</p>
</div>
<?php if ($notice !== null): ?>
<div class="login-notice login-notice-success">
<strong>Aktion abgeschlossen</strong>
<p><?= htmlspecialchars($notice, ENT_QUOTES) ?></p>
</div>
<?php endif; ?>
<?php if ($errors !== []): ?>
<div class="login-notice login-notice-error">
<strong>Bitte prüfen</strong>
<ul class="login-list">
<?php foreach ($errors as $error): ?>
<li><?= htmlspecialchars($error, ENT_QUOTES) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<div class="admin-layout">
<aside class="admin-sidebar">
<h2>Offene Vorgänge</h2>
<div class="admin-request-list">
<?php foreach ($requests as $request): ?>
<a class="admin-request-card<?= (($selected['id'] ?? '') === ($request['id'] ?? '')) ? ' is-active' : '' ?>" href="/admin/registrations/?id=<?= urlencode((string) ($request['id'] ?? '')) ?>">
<strong><?= htmlspecialchars((string) ($request['username'] ?? ''), ENT_QUOTES) ?></strong>
<span><?= htmlspecialchars((string) ($request['email'] ?? ''), ENT_QUOTES) ?></span>
<span class="admin-status admin-status-<?= htmlspecialchars((string) ($request['status'] ?? 'pending'), ENT_QUOTES) ?>">
<?= htmlspecialchars((string) ($request['status'] ?? 'pending'), ENT_QUOTES) ?>
</span>
</a>
<?php endforeach; ?>
</div>
</aside>
<section class="admin-detail">
<?php if (!is_array($selected)): ?>
<div class="login-notice login-notice-info">
<strong>Keine Anfragen vorhanden</strong>
<p>Es liegt aktuell keine Registrierungsanfrage vor.</p>
</div>
<?php else: ?>
<div class="admin-summary">
<h2><?= htmlspecialchars((string) ($selected['display_name'] ?? $selected['username'] ?? ''), ENT_QUOTES) ?></h2>
<p>Status: <strong><?= htmlspecialchars((string) ($selected['status'] ?? 'pending'), ENT_QUOTES) ?></strong></p>
<p>Benutzername: <?= htmlspecialchars((string) ($selected['username'] ?? ''), ENT_QUOTES) ?></p>
<p>E-Mail: <?= htmlspecialchars((string) ($selected['email'] ?? ''), ENT_QUOTES) ?></p>
<p>Angelegt: <?= htmlspecialchars((string) ($selected['created_at'] ?? ''), ENT_QUOTES) ?></p>
<?php if (((string) ($selected['note'] ?? '')) !== ''): ?>
<p>Zusatzinfo: <?= nl2br(htmlspecialchars((string) ($selected['note'] ?? ''), ENT_QUOTES)) ?></p>
<?php endif; ?>
</div>
<form class="login-form" method="post" action="/admin/registrations">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES) ?>">
<input type="hidden" name="id" value="<?= htmlspecialchars((string) ($selected['id'] ?? ''), ENT_QUOTES) ?>">
<div class="login-form-grid">
<label class="login-field">
<span>uidNumber</span>
<input type="text" name="uid_number" value="<?= htmlspecialchars((string) ($provisioning['uid_number'] ?? ''), ENT_QUOTES) ?>" required>
</label>
<label class="login-field">
<span>gidNumber</span>
<input type="text" name="gid_number" value="<?= htmlspecialchars((string) ($provisioning['gid_number'] ?? ''), ENT_QUOTES) ?>" required>
</label>
<label class="login-field">
<span>homeDirectory</span>
<input type="text" name="home_directory" value="<?= htmlspecialchars((string) ($provisioning['home_directory'] ?? ('/home/' . ($selected['username'] ?? ''))), ENT_QUOTES) ?>" required>
</label>
<label class="login-field">
<span>loginShell</span>
<input type="text" name="login_shell" value="<?= htmlspecialchars((string) ($provisioning['login_shell'] ?? '/bin/sh'), ENT_QUOTES) ?>">
</label>
<label class="login-field">
<span>Display Name</span>
<input type="text" name="display_name" value="<?= htmlspecialchars((string) ($provisioning['display_name'] ?? $selected['display_name'] ?? ''), ENT_QUOTES) ?>">
</label>
<label class="login-field">
<span>CN</span>
<input type="text" name="cn" value="<?= htmlspecialchars((string) ($provisioning['cn'] ?? $selected['display_name'] ?? $selected['username'] ?? ''), ENT_QUOTES) ?>">
</label>
<label class="login-field">
<span>GECOS</span>
<input type="text" name="gecos" value="<?= htmlspecialchars((string) ($provisioning['gecos'] ?? $selected['display_name'] ?? ''), ENT_QUOTES) ?>">
</label>
<label class="login-field">
<span>apple-generateduid</span>
<input type="text" name="apple_generateduid" value="<?= htmlspecialchars((string) ($provisioning['apple_generateduid'] ?? ''), ENT_QUOTES) ?>">
</label>
<label class="login-field">
<span>sambaSID</span>
<input type="text" name="samba_sid" value="<?= htmlspecialchars((string) ($provisioning['samba_sid'] ?? ''), ENT_QUOTES) ?>">
</label>
<label class="login-field login-field-span-2">
<span>memberOf als CSV</span>
<input type="text" name="member_of_csv" value="<?= htmlspecialchars((string) ($provisioning['member_of_csv'] ?? 'cn=generaluser,cn=groups,dc=nas,dc=kusche,dc=berlin'), ENT_QUOTES) ?>">
</label>
</div>
<label class="login-field">
<span>Admin-Notiz</span>
<textarea name="admin_note" rows="4"><?= htmlspecialchars((string) ($provisioning['admin_note'] ?? ''), ENT_QUOTES) ?></textarea>
</label>
<div class="login-actions">
<button class="login-button login-button-primary" type="submit" name="action" value="approve">Freigeben und LDIF erzeugen</button>
<button class="login-button login-button-danger" type="submit" name="action" value="reject">Ablehnen</button>
</div>
<label class="login-field">
<span>Ablehnungsgrund</span>
<textarea name="reject_reason" rows="3" placeholder="Nur erforderlich, wenn du auf Ablehnen klickst."></textarea>
</label>
</form>
<?php if (((string) ($provisioning['ldif_path'] ?? '')) !== '' && is_file((string) $provisioning['ldif_path'])): ?>
<div class="login-notice login-notice-info">
<strong>LDIF-Entwurf</strong>
<p><?= htmlspecialchars((string) ($provisioning['ldif_path'] ?? ''), ENT_QUOTES) ?></p>
<pre class="login-pre"><?= htmlspecialchars((string) file_get_contents((string) $provisioning['ldif_path']), ENT_QUOTES) ?></pre>
</div>
<?php endif; ?>
<?php endif; ?>
</section>
</div>
</section>
</main>
</body>
</html>

View File

@@ -33,6 +33,14 @@ body {
backdrop-filter: blur(24px);
}
.login-panel-wide {
width: min(760px, 100%);
}
.login-panel-admin {
width: min(1180px, 100%);
}
.login-panel-top {
margin-bottom: 24px;
}
@@ -72,6 +80,9 @@ body {
border-radius: 14px;
text-decoration: none;
font-weight: 700;
border: 0;
cursor: pointer;
font: inherit;
}
.login-button-primary {
@@ -91,12 +102,189 @@ body {
border: 1px dashed rgba(255, 255, 255, 0.18);
}
.login-button-danger {
color: #fff1f2;
background: linear-gradient(180deg, #fb7185, #e11d48);
}
.login-form {
display: grid;
gap: 18px;
}
.login-form-grid {
display: grid;
gap: 14px;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.login-field {
display: grid;
gap: 8px;
}
.login-field span {
font-size: 13px;
color: #cbd5e1;
}
.login-field input,
.login-field textarea {
width: 100%;
min-height: 48px;
padding: 12px 14px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.14);
background: rgba(15, 23, 42, 0.52);
color: #f8fafc;
font: inherit;
}
.login-field textarea {
min-height: 120px;
resize: vertical;
}
.login-field-span-2 {
grid-column: span 2;
}
.login-notice {
margin-bottom: 18px;
padding: 16px 18px;
border-radius: 18px;
border: 1px solid rgba(255, 255, 255, 0.12);
}
.login-notice strong {
display: block;
margin-bottom: 6px;
}
.login-notice p {
margin: 0;
line-height: 1.5;
}
.login-notice-success {
background: rgba(20, 83, 45, 0.45);
}
.login-notice-error {
background: rgba(127, 29, 29, 0.4);
}
.login-notice-info {
background: rgba(30, 41, 59, 0.5);
}
.login-list {
margin: 0;
padding-left: 18px;
}
.login-list li + li {
margin-top: 6px;
}
.login-meta-inline {
margin-top: 8px !important;
font-size: 0.92rem;
color: #cbd5e1;
}
.login-meta {
display: grid;
gap: 14px;
margin: 0;
}
.admin-layout {
display: grid;
gap: 24px;
grid-template-columns: 320px minmax(0, 1fr);
}
.admin-sidebar h2,
.admin-summary h2 {
margin-top: 0;
}
.admin-request-list {
display: grid;
gap: 10px;
max-height: 70vh;
overflow: auto;
}
.admin-request-card {
display: grid;
gap: 6px;
padding: 14px;
border-radius: 16px;
text-decoration: none;
color: inherit;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.admin-request-card.is-active {
border-color: rgba(167, 243, 208, 0.56);
background: rgba(16, 185, 129, 0.12);
}
.admin-request-card span {
color: #cbd5e1;
font-size: 0.92rem;
}
.admin-status {
display: inline-flex;
width: fit-content;
padding: 4px 10px;
border-radius: 999px;
color: #e2e8f0 !important;
background: rgba(255, 255, 255, 0.12);
}
.admin-detail {
min-width: 0;
}
.admin-summary {
margin-bottom: 20px;
}
.admin-summary p {
margin: 0 0 10px;
line-height: 1.5;
}
.login-pre {
margin: 12px 0 0;
padding: 14px;
border-radius: 14px;
overflow: auto;
background: rgba(2, 6, 23, 0.65);
color: #dbeafe;
}
@media (max-width: 900px) {
.admin-layout {
grid-template-columns: 1fr;
}
}
@media (max-width: 640px) {
.login-form-grid {
grid-template-columns: 1fr;
}
.login-field-span-2 {
grid-column: auto;
}
}
.login-meta div {
display: grid;
gap: 4px;

View File

@@ -18,6 +18,7 @@ $previewUrl = $auth->allowDesktopPreview()
$loginScreen = [
'branding' => $auth->branding(),
'login_url' => $auth->isConfigured() ? '/auth/keycloak' : null,
'register_url' => '/auth/register',
'preview_url' => $previewUrl,
'keycloak_ready' => $auth->isConfigured(),
'callback_url' => $auth->redirectUri(),

View File

@@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
require_once dirname(__DIR__, 3) . '/src/App/bootstrap.php';
use App\ConfigLoader;
use App\RegistrationService;
session_start();
$projectRoot = dirname(__DIR__, 3);
$registration = new RegistrationService($projectRoot, ConfigLoader::load($projectRoot, 'registration'));
if (!$registration->isEnabled()) {
http_response_code(404);
echo 'Registrierung ist deaktiviert.';
exit;
}
$csrfToken = (string) ($_SESSION['registration_form_token'] ?? '');
if ($csrfToken === '') {
$csrfToken = bin2hex(random_bytes(16));
$_SESSION['registration_form_token'] = $csrfToken;
}
$errors = [];
$submitted = false;
$createdRequest = null;
$form = [
'username' => '',
'given_name' => '',
'family_name' => '',
'email' => '',
'note' => '',
];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$form = [
'username' => trim((string) ($_POST['username'] ?? '')),
'given_name' => trim((string) ($_POST['given_name'] ?? '')),
'family_name' => trim((string) ($_POST['family_name'] ?? '')),
'email' => trim((string) ($_POST['email'] ?? '')),
'note' => trim((string) ($_POST['note'] ?? '')),
];
$postedToken = (string) ($_POST['csrf_token'] ?? '');
if ($postedToken === '' || !hash_equals($csrfToken, $postedToken)) {
$errors[] = 'Die Formularprüfung ist fehlgeschlagen. Bitte versuche es erneut.';
} else {
$result = $registration->submit($form);
if (($result['success'] ?? false) !== true) {
$errors = array_values(array_map('strval', (array) ($result['errors'] ?? [])));
} else {
$submitted = true;
$createdRequest = $result['request'] ?? null;
$csrfToken = bin2hex(random_bytes(16));
$_SESSION['registration_form_token'] = $csrfToken;
$form = [
'username' => '',
'given_name' => '',
'family_name' => '',
'email' => '',
'note' => '',
];
}
}
}
?><!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Registrierung anfragen</title>
<link rel="stylesheet" href="/assets/auth/login.css">
</head>
<body>
<main class="login-shell">
<section class="login-panel login-panel-wide">
<div class="login-panel-top">
<p class="login-kicker">Kusche.Berlin</p>
<h1>Registrierung anfragen</h1>
<p class="login-copy">Neue Konten werden nicht sofort freigeschaltet. Deine Anfrage wird geprüft und erst danach für LDAP, NAS und Desktop vorbereitet.</p>
</div>
<?php if ($submitted): ?>
<div class="login-notice login-notice-success">
<strong>Anfrage gespeichert.</strong>
<p>Dein Zugang wurde noch nicht freigeschaltet. Eine berechtigte Person prüft nun die Registrierung.</p>
<?php if (is_array($createdRequest)): ?>
<p class="login-meta-inline">Vorgangs-ID: <?= htmlspecialchars((string) ($createdRequest['id'] ?? ''), ENT_QUOTES) ?></p>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if ($errors !== []): ?>
<div class="login-notice login-notice-error">
<strong>Bitte prüfen</strong>
<ul class="login-list">
<?php foreach ($errors as $error): ?>
<li><?= htmlspecialchars($error, ENT_QUOTES) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<form class="login-form" method="post" action="/auth/register">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES) ?>">
<div class="login-form-grid">
<label class="login-field">
<span>Benutzername</span>
<input type="text" name="username" value="<?= htmlspecialchars($form['username'], ENT_QUOTES) ?>" required>
</label>
<label class="login-field">
<span>E-Mail</span>
<input type="email" name="email" value="<?= htmlspecialchars($form['email'], ENT_QUOTES) ?>" required>
</label>
<label class="login-field">
<span>Vorname</span>
<input type="text" name="given_name" value="<?= htmlspecialchars($form['given_name'], ENT_QUOTES) ?>" required>
</label>
<label class="login-field">
<span>Nachname</span>
<input type="text" name="family_name" value="<?= htmlspecialchars($form['family_name'], ENT_QUOTES) ?>" required>
</label>
</div>
<label class="login-field">
<span>Zusätzliche Info</span>
<textarea name="note" rows="5" placeholder="Optional: Bezug zur Familie, Einladung, gewünschte Nutzung, Rückfragekontakt"><?= htmlspecialchars($form['note'], ENT_QUOTES) ?></textarea>
</label>
<div class="login-notice login-notice-info">
<strong>Wichtig</strong>
<p>Das Passwort wird erst nach Freigabe gesetzt. Neue Registrierungen erhalten zunächst keinen aktiven Desktop-Zugang.</p>
</div>
<div class="login-actions">
<button class="login-button login-button-primary" type="submit">Registrierung absenden</button>
<a class="login-button login-button-secondary" href="/auth/login">Zurück zum Login</a>
</div>
</form>
</section>
</main>
</body>
</html>