asdasd
All checks were successful
Deploy / deploy-staging (push) Successful in 1m3s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-06-15 02:05:36 +02:00
parent e9b86eb7a3
commit 9325b93404
18 changed files with 1102 additions and 268 deletions

View File

@@ -1,177 +1,3 @@
<?php
declare(strict_types=1);
require_once dirname(__DIR__, 3) . '/src/App/bootstrap.php';
use App\ConfigLoader;
use App\LdapProvisioner;
$projectRoot = dirname(__DIR__, 3);
$config = ConfigLoader::load($projectRoot, 'registration');
$ldap = new LdapProvisioner($config);
$diagnostics = $ldap->diagnostics();
$serviceResult = $ldap->testServiceBind();
$messages = [];
$errors = [];
$form = ['username' => '', 'password' => '', 'new_password' => ''];
$attempt = null;
$currentUserDn = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = (string) ($_POST['action'] ?? '');
$form['username'] = trim((string) ($_POST['username'] ?? ''));
$form['password'] = (string) ($_POST['password'] ?? '');
$form['new_password'] = (string) ($_POST['new_password'] ?? '');
if ($form['username'] === '') {
$errors[] = 'Benutzername ist erforderlich.';
}
if ($form['password'] === '') {
$errors[] = 'Passwort ist erforderlich.';
}
if ($action === 'change-password' && strlen($form['new_password']) < 12) {
$errors[] = 'Das neue Passwort muss mindestens 12 Zeichen lang sein.';
}
if ($errors === []) {
$currentUserDn = $ldap->userDnForUsername($form['username']);
if ($action === 'test-login') {
$attempt = [
'action' => 'Login-Test',
'username' => $form['username'],
'user_dn' => $currentUserDn,
];
$result = $ldap->testUserCredentials($form['username'], $form['password']);
if ($result['success'] ?? false) {
$messages[] = $result['message'] . ' DN: ' . (string) ($result['dn'] ?? '');
} else {
$errors[] = (string) ($result['message'] ?? 'LDAP-Login fehlgeschlagen.');
}
} elseif ($action === 'change-password') {
$attempt = [
'action' => 'Passwortänderung',
'username' => $form['username'],
'user_dn' => $currentUserDn,
'new_password_length' => (string) strlen($form['new_password']),
];
$loginCheck = $ldap->testUserCredentials($form['username'], $form['password']);
if (!(bool) ($loginCheck['success'] ?? false)) {
$errors[] = 'Der Login mit aktuellem Passwort ist fehlgeschlagen: ' . (string) ($loginCheck['message'] ?? '');
} else {
$result = $ldap->updateUserPassword($form['username'], $form['new_password']);
if ($result['success'] ?? false) {
$messages[] = $result['message'] . ' DN: ' . (string) ($result['dn'] ?? '');
$form['password'] = '';
$form['new_password'] = '';
} else {
$errors[] = (string) ($result['message'] ?? 'Passwortänderung fehlgeschlagen.');
}
}
}
}
}
?><!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>LDAP-Test</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>LDAP-Test</h1>
<p class="login-copy">Diese Testseite läuft unabhängig vom eigentlichen Desktop-Login. Hier kannst du nur LDAP-Login und Passwortänderung inklusive Samba-Feldern prüfen.</p>
</div>
<div class="login-notice <?= ($serviceResult['success'] ?? false) ? 'login-notice-success' : 'login-notice-error' ?>">
<strong>Service-Bind</strong>
<p><?= htmlspecialchars((string) ($serviceResult['message'] ?? ''), ENT_QUOTES) ?></p>
</div>
<div class="login-notice login-notice-info">
<strong>Verbindungsdaten</strong>
<ul class="login-list">
<li>Host: <?= htmlspecialchars((string) ($diagnostics['host'] ?? ''), ENT_QUOTES) ?></li>
<li>Port: <?= htmlspecialchars((string) ($diagnostics['port'] ?? ''), ENT_QUOTES) ?></li>
<li>Bind-DN: <?= htmlspecialchars((string) ($diagnostics['bind_dn'] ?? ''), ENT_QUOTES) ?></li>
<li>Users-DN: <?= htmlspecialchars((string) ($diagnostics['users_dn'] ?? ''), ENT_QUOTES) ?></li>
<li>StartTLS: <?= htmlspecialchars((string) ($diagnostics['starttls'] ?? ''), ENT_QUOTES) ?></li>
<li>Gruppenmodus: <?= htmlspecialchars((string) ($diagnostics['group_assignment_mode'] ?? ''), ENT_QUOTES) ?></li>
<li>Gruppenattribut: <?= htmlspecialchars((string) ($diagnostics['group_member_attribute'] ?? ''), ENT_QUOTES) ?></li>
<li>PHP LDAP-Erweiterung: <?= htmlspecialchars((string) ($diagnostics['ldap_extension'] ?? ''), ENT_QUOTES) ?></li>
</ul>
</div>
<?php if ($messages !== []): ?>
<div class="login-notice login-notice-success">
<strong>Ergebnis</strong>
<ul class="login-list">
<?php foreach ($messages as $message): ?>
<li><?= htmlspecialchars($message, ENT_QUOTES) ?></li>
<?php endforeach; ?>
</ul>
</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; ?>
<?php if (is_array($attempt)): ?>
<div class="login-notice login-notice-info">
<strong>Letzter Versuch</strong>
<ul class="login-list">
<li>Aktion: <?= htmlspecialchars((string) ($attempt['action'] ?? ''), ENT_QUOTES) ?></li>
<li>Benutzername: <?= htmlspecialchars((string) ($attempt['username'] ?? ''), ENT_QUOTES) ?></li>
<li>User-DN: <?= htmlspecialchars((string) ($attempt['user_dn'] ?? ''), ENT_QUOTES) ?></li>
<?php if (isset($attempt['new_password_length'])): ?>
<li>Länge neues Passwort: <?= htmlspecialchars((string) ($attempt['new_password_length'] ?? ''), ENT_QUOTES) ?></li>
<?php endif; ?>
</ul>
</div>
<?php endif; ?>
<form class="login-form" method="post" action="/admin/ldap-test/">
<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>Passwort</span>
<input type="password" name="password" value="" required>
</label>
<label class="login-field">
<span>Neues Passwort</span>
<input type="password" name="new_password" value="" placeholder="Nur für Passwortänderung erforderlich">
</label>
<div class="login-actions">
<button class="login-button login-button-primary" type="submit" name="action" value="test-login">Login-Test</button>
<button class="login-button login-button-secondary" type="submit" name="action" value="change-password">Neues Passwort setzen</button>
</div>
</form>
</section>
</main>
</body>
</html>
http_response_code(404);
echo 'LDAP-Test ist deaktiviert.';

View File

@@ -0,0 +1,234 @@
<?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'));
$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'] ?? []))));
if (!in_array('administrators', $currentGroups, true)) {
http_response_code(403);
echo 'Kein Zugriff auf User Management.';
exit;
}
$csrfToken = (string) ($_SESSION['user_management_token'] ?? '');
if ($csrfToken === '') {
$csrfToken = bin2hex(random_bytes(16));
$_SESSION['user_management_token'] = $csrfToken;
}
$messages = [];
$errors = [];
$selectedId = trim((string) ($_GET['id'] ?? $_POST['selected_id'] ?? ''));
$manualUsername = trim((string) ($_POST['manual_username'] ?? ''));
$manualResetUsername = trim((string) ($_POST['reset_username'] ?? ''));
$manualResetPassword = (string) ($_POST['reset_password'] ?? '');
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-registration') {
$groupDns = array_values(array_filter(array_map('strval', (array) ($_POST['group_dns'] ?? []))));
$memberOfList = implode(PHP_EOL, $groupDns);
$result = $registration->approve($selectedId, [
'member_of_list' => $memberOfList,
'admin_note' => (string) ($_POST['admin_note'] ?? ''),
], (string) ($currentUser['username'] ?? 'administrator'));
if (($result['success'] ?? false) !== true) {
$errors = array_merge($errors, array_values(array_map('strval', (array) ($result['errors'] ?? []))));
} else {
$messages[] = 'Registrierung wurde freigeschaltet.';
}
} elseif ($action === 'deactivate-user') {
$result = $registration->deactivateUser($manualUsername);
($result['success'] ?? false) ? $messages[] = (string) ($result['message'] ?? '') : $errors[] = (string) ($result['message'] ?? '');
} 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'] ?? '');
} 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'] ?? '');
} 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'] ?? '');
}
}
}
}
$pendingRequests = $registration->pendingApprovals();
$availableGroups = $registration->availableGroups();
$selected = $selectedId !== ''
? $registration->find($selectedId)
: ($pendingRequests[0] ?? null);
?><!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>User Management</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>User Management</h1>
<p class="login-copy">Nur für Benutzer in der Gruppe <strong>administrators</strong>. Hier werden Neuregistrierungen freigeschaltet sowie bestehende LDAP-Benutzer aktiviert, deaktiviert oder zurückgesetzt.</p>
</div>
<?php if ($messages !== []): ?>
<div class="login-notice login-notice-success">
<strong>Ergebnis</strong>
<ul class="login-list">
<?php foreach ($messages as $message): ?>
<li><?= htmlspecialchars($message, ENT_QUOTES) ?></li>
<?php endforeach; ?>
</ul>
</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 Registrierungen</h2>
<div class="admin-request-list">
<?php foreach ($pendingRequests as $request): ?>
<a class="admin-request-card<?= (($selected['id'] ?? '') === ($request['id'] ?? '')) ? ' is-active' : '' ?>" href="/admin/users/?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"><?= htmlspecialchars((string) ($request['status'] ?? ''), ENT_QUOTES) ?></span>
</a>
<?php endforeach; ?>
</div>
</aside>
<section class="admin-detail">
<?php if (is_array($selected)): ?>
<div class="admin-summary">
<h2>Freischaltung</h2>
<p>Benutzer: <?= htmlspecialchars((string) ($selected['username'] ?? ''), ENT_QUOTES) ?></p>
<p>E-Mail: <?= htmlspecialchars((string) ($selected['email'] ?? ''), ENT_QUOTES) ?></p>
<p>Status: <?= htmlspecialchars((string) ($selected['status'] ?? ''), ENT_QUOTES) ?></p>
</div>
<form class="login-form" method="post" action="/admin/users/">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES) ?>">
<input type="hidden" name="selected_id" value="<?= htmlspecialchars((string) ($selected['id'] ?? ''), ENT_QUOTES) ?>">
<input type="hidden" name="action" value="approve-registration">
<label class="login-field">
<span>Zielgruppen für Freischaltung</span>
<div class="admin-request-list">
<?php foreach ($availableGroups as $group): ?>
<label class="login-field">
<span>
<input type="checkbox" name="group_dns[]" value="<?= htmlspecialchars($group['dn'], ENT_QUOTES) ?>">
<?= htmlspecialchars($group['label'], ENT_QUOTES) ?>
</span>
</label>
<?php endforeach; ?>
</div>
</label>
<label class="login-field">
<span>Admin-Notiz</span>
<textarea name="admin_note" rows="3"></textarea>
</label>
<div class="login-actions">
<button class="login-button login-button-primary" type="submit">Freischaltung bestätigen</button>
</div>
</form>
<?php endif; ?>
<div class="admin-summary">
<h2>Bestehenden Benutzer verwalten</h2>
</div>
<form class="login-form" method="post" action="/admin/users/">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES) ?>">
<label class="login-field">
<span>Benutzername</span>
<input type="text" name="manual_username" value="<?= htmlspecialchars($manualUsername, ENT_QUOTES) ?>" required>
</label>
<label class="login-field">
<span>Gruppen für Aktivierung</span>
<div class="admin-request-list">
<?php foreach ($availableGroups as $group): ?>
<label class="login-field">
<span>
<input type="checkbox" name="manual_group_dns[]" value="<?= htmlspecialchars($group['dn'], ENT_QUOTES) ?>">
<?= htmlspecialchars($group['label'], ENT_QUOTES) ?>
</span>
</label>
<?php endforeach; ?>
</div>
</label>
<div class="login-actions">
<button class="login-button login-button-primary" type="submit" name="action" value="activate-user">Benutzer aktivieren</button>
<button class="login-button login-button-danger" type="submit" name="action" value="deactivate-user">Benutzer deaktivieren</button>
</div>
</form>
<div class="admin-summary">
<h2>Passwort zurücksetzen</h2>
</div>
<form class="login-form" method="post" action="/admin/users/">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES) ?>">
<label class="login-field">
<span>Benutzername</span>
<input type="text" name="reset_username" value="<?= htmlspecialchars($manualResetUsername, ENT_QUOTES) ?>" required>
</label>
<label class="login-field">
<span>Neues Passwort für direkten Reset</span>
<input type="password" name="reset_password" value="" placeholder="Optional für direkten Admin-Reset">
</label>
<div class="login-actions">
<button class="login-button login-button-secondary" type="submit" name="action" value="send-reset-mail">Reset-Mail senden</button>
<button class="login-button login-button-primary" type="submit" name="action" value="set-password">Passwort direkt setzen</button>
</div>
</form>
</section>
</div>
</section>
</main>
</body>
</html>

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
require_once dirname(__DIR__, 3) . '/src/App/bootstrap.php';
use App\ConfigLoader;
use App\RegistrationService;
$projectRoot = dirname(__DIR__, 3);
$registration = new RegistrationService($projectRoot, ConfigLoader::load($projectRoot, 'registration'));
$message = null;
$error = null;
$username = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim((string) ($_POST['username'] ?? ''));
$origin = ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http') . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost');
$result = $registration->sendPasswordReset($username, $origin);
if ($result['success'] ?? false) {
$message = (string) ($result['message'] ?? '');
} else {
$error = (string) ($result['message'] ?? '');
}
}
?><!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Passwort vergessen</title>
<link rel="stylesheet" href="/assets/auth/login.css">
</head>
<body>
<main class="login-shell">
<section class="login-panel">
<div class="login-panel-top">
<p class="login-kicker">Kusche.Berlin</p>
<h1>Passwort vergessen</h1>
<p class="login-copy">Gib deinen Benutzernamen an. Wenn der LDAP-Benutzer eine E-Mail-Adresse hat, senden wir einen 8 Stunden gültigen Reset-Link.</p>
</div>
<?php if ($message !== null): ?>
<div class="login-notice login-notice-success"><p><?= htmlspecialchars($message, ENT_QUOTES) ?></p></div>
<?php endif; ?>
<?php if ($error !== null): ?>
<div class="login-notice login-notice-error"><p><?= htmlspecialchars($error, ENT_QUOTES) ?></p></div>
<?php endif; ?>
<form class="login-form" method="post" action="/auth/forgot-password/">
<label class="login-field">
<span>Benutzername</span>
<input type="text" name="username" value="<?= htmlspecialchars($username, ENT_QUOTES) ?>" required>
</label>
<div class="login-actions">
<button class="login-button login-button-primary" type="submit">Reset-Link senden</button>
<a class="login-button login-button-secondary" href="/auth/login">Zurück zum Login</a>
</div>
</form>
</section>
</main>
</body>
</html>

View File

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

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
require_once dirname(__DIR__, 3) . '/src/App/bootstrap.php';
use App\ConfigLoader;
use App\PasswordResetService;
$projectRoot = dirname(__DIR__, 3);
$service = new PasswordResetService($projectRoot, ConfigLoader::load($projectRoot, 'registration'));
$token = trim((string) ($_GET['token'] ?? $_POST['token'] ?? ''));
$record = $service->validate($token);
$message = null;
$error = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$result = $service->complete(
$token,
(string) ($_POST['password'] ?? ''),
(string) ($_POST['password_confirm'] ?? '')
);
if ($result['success'] ?? false) {
$message = (string) ($result['message'] ?? '');
$record = null;
} else {
$error = (string) ($result['message'] ?? '');
}
}
?><!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Passwort zurücksetzen</title>
<link rel="stylesheet" href="/assets/auth/login.css">
</head>
<body>
<main class="login-shell">
<section class="login-panel">
<div class="login-panel-top">
<p class="login-kicker">Kusche.Berlin</p>
<h1>Passwort zurücksetzen</h1>
<p class="login-copy">Der Link ist 8 Stunden gültig und setzt sowohl LDAP- als auch Samba-Passwort neu.</p>
</div>
<?php if ($message !== null): ?>
<div class="login-notice login-notice-success"><p><?= htmlspecialchars($message, ENT_QUOTES) ?></p></div>
<?php endif; ?>
<?php if ($error !== null): ?>
<div class="login-notice login-notice-error"><p><?= htmlspecialchars($error, ENT_QUOTES) ?></p></div>
<?php endif; ?>
<?php if ($record !== null): ?>
<form class="login-form" method="post" action="/auth/reset-password/">
<input type="hidden" name="token" value="<?= htmlspecialchars($token, ENT_QUOTES) ?>">
<label class="login-field">
<span>Neues Passwort</span>
<input type="password" name="password" required>
</label>
<label class="login-field">
<span>Neues Passwort wiederholen</span>
<input type="password" name="password_confirm" required>
</label>
<div class="login-actions">
<button class="login-button login-button-primary" type="submit">Passwort zurücksetzen</button>
</div>
</form>
<?php else: ?>
<div class="login-notice login-notice-info"><p>Der Token ist ungültig oder bereits verwendet.</p></div>
<?php endif; ?>
</section>
</main>
</body>
</html>