asdasd
This commit is contained in:
@@ -77,9 +77,7 @@
|
||||
</div>
|
||||
|
||||
<div class="kb-form-links">
|
||||
<#if (url.loginResetCredentialsUrl!'')?has_content>
|
||||
<a tabindex="6" href="${url.loginResetCredentialsUrl}">Passwort vergessen?</a>
|
||||
</#if>
|
||||
<a tabindex="6" id="kb-forgot-password-link" href="#" hidden>Passwort vergessen?</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -90,7 +88,7 @@
|
||||
|
||||
<div id="kc-registration-container" class="kb-registration">
|
||||
<div id="kc-registration">
|
||||
<a class="kb-register-button" id="kb-register-button" tabindex="8" href="#" hidden>Registrieren</a>
|
||||
<a class="kb-register-button" id="kb-register-button" tabindex="8" href="#" hidden>Neue Benutzer</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
(function () {
|
||||
const registerLink = document.getElementById('kb-register-button');
|
||||
const forgotPasswordLink = document.getElementById('kb-forgot-password-link');
|
||||
|
||||
if (!registerLink) {
|
||||
if (!registerLink && !forgotPasswordLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -14,8 +15,14 @@
|
||||
|
||||
try {
|
||||
const target = new URL(redirectUri);
|
||||
registerLink.href = target.origin + '/auth/register';
|
||||
if (registerLink) {
|
||||
registerLink.href = target.origin + '/auth/register/';
|
||||
registerLink.hidden = false;
|
||||
}
|
||||
if (forgotPasswordLink) {
|
||||
forgotPasswordLink.href = target.origin + '/auth/forgot-password/';
|
||||
forgotPasswordLink.hidden = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Registrierungslink konnte nicht vorbereitet werden.', error);
|
||||
}
|
||||
|
||||
@@ -55,6 +55,21 @@ return [
|
||||
'module_name' => 'admin',
|
||||
'summary' => 'Platzhalter fuer globale Verwaltung von Apps, Widgets, Integrationen und Suchmaschinen.',
|
||||
],
|
||||
[
|
||||
'app_id' => 'user-management',
|
||||
'title' => 'User Management',
|
||||
'icon' => 'UM',
|
||||
'entry_route' => '/admin/users/',
|
||||
'window_mode' => 'window',
|
||||
'content_mode' => 'iframe',
|
||||
'default_width' => 1180,
|
||||
'default_height' => 760,
|
||||
'supports_widget' => false,
|
||||
'supports_tray' => true,
|
||||
'module_name' => 'admin',
|
||||
'required_groups' => ['administrators'],
|
||||
'summary' => 'Benutzerverwaltung fuer Freischaltungen, Aktivierung, Deaktivierung und Passwort-Reset.',
|
||||
],
|
||||
[
|
||||
'app_id' => 'desktop-login',
|
||||
'title' => 'Desktop Login',
|
||||
|
||||
@@ -6,9 +6,18 @@ return [
|
||||
'enabled' => true,
|
||||
'storage_path' => 'data/registration-requests.json',
|
||||
'ldif_export_dir' => 'data/registration-ldif',
|
||||
'identity_counter_path' => 'data/registration-identity-counter.json',
|
||||
'reset_token_path' => 'data/password-reset-tokens.json',
|
||||
'approver_groups' => ['administrators', 'appadmin'],
|
||||
'approver_usernames' => [],
|
||||
'password_crypto_key' => getenv('REGISTRATION_PASSWORD_KEY') ?: '',
|
||||
'reset_link_ttl_hours' => 8,
|
||||
'mail' => [
|
||||
'enabled' => true,
|
||||
'from_email' => getenv('MAIL_FROM_EMAIL') ?: 'noreply@desktop.kusche.berlin',
|
||||
'from_name' => getenv('MAIL_FROM_NAME') ?: 'Kusche.Berlin Desktop',
|
||||
'log_path' => 'data/mail.log',
|
||||
],
|
||||
'ldap' => [
|
||||
'enabled' => true,
|
||||
'host' => getenv('LDAP_HOST') ?: '127.0.0.1',
|
||||
@@ -20,10 +29,24 @@ return [
|
||||
'default_groups' => [
|
||||
'cn=generaluser,cn=groups,dc=nas,dc=kusche,dc=berlin',
|
||||
],
|
||||
'pending_group_dn' => getenv('LDAP_PENDING_GROUP_DN') ?: 'cn=users-pending,cn=groups,dc=nas,dc=kusche,dc=berlin',
|
||||
'available_groups' => [
|
||||
['dn' => 'cn=administrators,cn=groups,dc=nas,dc=kusche,dc=berlin', 'label' => 'Administrators'],
|
||||
['dn' => 'cn=appadmin,cn=groups,dc=nas,dc=kusche,dc=berlin', 'label' => 'App Admin'],
|
||||
['dn' => 'cn=dockers,cn=groups,dc=nas,dc=kusche,dc=berlin', 'label' => 'Dockers'],
|
||||
['dn' => 'cn=family,cn=groups,dc=nas,dc=kusche,dc=berlin', 'label' => 'Family'],
|
||||
['dn' => 'cn=generaluser,cn=groups,dc=nas,dc=kusche,dc=berlin', 'label' => 'General User'],
|
||||
['dn' => 'cn=internalfamily,cn=groups,dc=nas,dc=kusche,dc=berlin', 'label' => 'Internal Family'],
|
||||
['dn' => 'cn=users,cn=groups,dc=nas,dc=kusche,dc=berlin', 'label' => 'Users'],
|
||||
],
|
||||
'group_assignment_mode' => getenv('LDAP_GROUP_ASSIGNMENT_MODE') ?: 'group_memberuid',
|
||||
'group_member_attribute' => getenv('LDAP_GROUP_MEMBER_ATTRIBUTE') ?: 'memberUid',
|
||||
'default_login_shell' => '/bin/sh',
|
||||
'home_directory_prefix' => '/home',
|
||||
'uid_number_start' => (int) (getenv('LDAP_UID_NUMBER_START') ?: 1000010),
|
||||
'gid_number_default' => (int) (getenv('LDAP_GID_NUMBER_DEFAULT') ?: 1000012),
|
||||
'samba_domain_sid' => getenv('LDAP_SAMBA_DOMAIN_SID') ?: 'S-1-5-21-2039032496-2228230393-4197958046',
|
||||
'samba_rid_start' => (int) (getenv('LDAP_SAMBA_RID_START') ?: 1025),
|
||||
'shadow_expire' => 1,
|
||||
'active_shadow_expire' => -1,
|
||||
'shadow_flag' => 0,
|
||||
|
||||
@@ -7,6 +7,7 @@ $headline = (string) ($loginScreen['branding']['headline'] ?? 'Desktop Login');
|
||||
$subheadline = (string) ($loginScreen['branding']['subheadline'] ?? '');
|
||||
$loginUrl = $loginScreen['login_url'] ?? null;
|
||||
$registerUrl = $loginScreen['register_url'] ?? null;
|
||||
$forgotPasswordUrl = $loginScreen['forgot_password_url'] ?? null;
|
||||
$previewUrl = $loginScreen['preview_url'] ?? null;
|
||||
$keycloakReady = (bool) ($loginScreen['keycloak_ready'] ?? false);
|
||||
$callbackUrl = (string) ($loginScreen['callback_url'] ?? '');
|
||||
@@ -41,6 +42,10 @@ $callbackUrl = (string) ($loginScreen['callback_url'] ?? '');
|
||||
<?php if ($registerUrl !== null): ?>
|
||||
<a class="login-button login-button-secondary" href="<?= htmlspecialchars($registerUrl, ENT_QUOTES) ?>">Registrierung anfragen</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($forgotPasswordUrl !== null): ?>
|
||||
<a class="login-button login-button-secondary" href="<?= htmlspecialchars($forgotPasswordUrl, ENT_QUOTES) ?>">Passwort vergessen</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<dl class="login-meta">
|
||||
|
||||
@@ -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.';
|
||||
|
||||
234
public/admin/users/index.php
Normal file
234
public/admin/users/index.php
Normal 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>
|
||||
66
public/auth/forgot-password/index.php
Normal file
66
public/auth/forgot-password/index.php
Normal 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>
|
||||
@@ -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(),
|
||||
|
||||
79
public/auth/reset-password/index.php
Normal file
79
public/auth/reset-password/index.php
Normal 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>
|
||||
@@ -29,13 +29,15 @@ final class App
|
||||
$widgetRegistry = new WidgetRegistry($this->projectRoot . '/config/widgets.php');
|
||||
$skins = SkinResolver::all();
|
||||
$activeSkin = SkinResolver::resolve($_GET['skin'] ?? null);
|
||||
$apps = AppIconResolver::decorate($registry->all(), $activeSkin);
|
||||
$windows = (new WindowManager($registry))->defaultWindows();
|
||||
$activeWidgetIds = DesktopState::defaultWidgetIds();
|
||||
$isAuthenticated = isset($_SESSION['desktop_auth']) && is_array($_SESSION['desktop_auth']);
|
||||
$authUser = $isAuthenticated && is_array($_SESSION['desktop_auth']['user'] ?? null)
|
||||
? $_SESSION['desktop_auth']['user']
|
||||
: [];
|
||||
$authGroups = array_values(array_map('strval', (array) ($authUser['groups'] ?? [])));
|
||||
$visibleApps = $this->filterAppsForGroups($registry->all(), $authGroups);
|
||||
$apps = AppIconResolver::decorate($visibleApps, $activeSkin);
|
||||
$windows = (new WindowManager($registry))->defaultWindows();
|
||||
$activeWidgetIds = DesktopState::defaultWidgetIds();
|
||||
$displayName = (string) ($authUser['name'] ?? $authUser['username'] ?? 'Gast');
|
||||
|
||||
return [
|
||||
@@ -80,4 +82,30 @@ final class App
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $apps
|
||||
* @param array<int, string> $groups
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function filterAppsForGroups(array $apps, array $groups): array
|
||||
{
|
||||
$normalizedGroups = array_map(static fn (string $group): string => strtolower(trim($group, '/')), $groups);
|
||||
|
||||
return array_values(array_filter($apps, static function (array $app) use ($normalizedGroups): bool {
|
||||
$requiredGroups = array_values(array_map('strval', (array) ($app['required_groups'] ?? [])));
|
||||
|
||||
if ($requiredGroups === []) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($requiredGroups as $group) {
|
||||
if (in_array(strtolower(trim($group, '/')), $normalizedGroups, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
51
src/App/JsonStore.php
Normal file
51
src/App/JsonStore.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
final class JsonStore
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $storagePath,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int|string, mixed>
|
||||
*/
|
||||
public function read(): array
|
||||
{
|
||||
if (!is_file($this->storagePath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$raw = file_get_contents($this->storagePath);
|
||||
|
||||
if ($raw === false || trim($raw) === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$decoded = json_decode($raw, true);
|
||||
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int|string, mixed> $payload
|
||||
*/
|
||||
public function write(array $payload): void
|
||||
{
|
||||
$directory = dirname($this->storagePath);
|
||||
|
||||
if (!is_dir($directory)) {
|
||||
mkdir($directory, 0775, true);
|
||||
}
|
||||
|
||||
file_put_contents(
|
||||
$this->storagePath,
|
||||
json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL,
|
||||
LOCK_EX
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,60 @@ final class LdapProvisioner
|
||||
return 'uid=' . $username . ',' . $baseDn;
|
||||
}
|
||||
|
||||
public function usernameExists(string $username): bool
|
||||
{
|
||||
return $this->getUserByUsername($username, ['uid']) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $attributes
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function getUserByUsername(string $username, array $attributes = []): ?array
|
||||
{
|
||||
try {
|
||||
$link = $this->connectAsService();
|
||||
$userDn = $this->userDnForUsername($username);
|
||||
$read = @ldap_read($link, $userDn, '(objectClass=*)', $attributes === [] ? ['*'] : $attributes);
|
||||
|
||||
if ($read === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$entries = ldap_get_entries($link, $read);
|
||||
|
||||
if (!is_array($entries) || (int) ($entries['count'] ?? 0) < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$entry = ['dn' => $userDn];
|
||||
|
||||
foreach ($entries[0] as $key => $value) {
|
||||
if (is_int($key) || $key === 'count') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
if ((int) ($value['count'] ?? 0) === 1) {
|
||||
$entry[$key] = (string) ($value[0] ?? '');
|
||||
} else {
|
||||
$items = [];
|
||||
foreach ($value as $subKey => $subValue) {
|
||||
if (is_int($subKey)) {
|
||||
$items[] = (string) $subValue;
|
||||
}
|
||||
}
|
||||
$entry[$key] = $items;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $entry;
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{success: bool, message: string}
|
||||
*/
|
||||
@@ -172,7 +226,12 @@ final class LdapProvisioner
|
||||
throw new RuntimeException('LDAP-Benutzer konnte nicht angelegt werden: ' . $this->lastError($link));
|
||||
}
|
||||
|
||||
$groupUpdates = $this->assignGroups($link, (string) ($request['username'] ?? ''), (string) $userDn, $provisioning);
|
||||
$groupUpdates = $this->syncGroupsForUser(
|
||||
$link,
|
||||
(string) ($request['username'] ?? ''),
|
||||
(string) $userDn,
|
||||
$this->resolveTargetGroupsFromProvisioning($provisioning)
|
||||
);
|
||||
|
||||
return [
|
||||
'dn' => $userDn,
|
||||
@@ -180,6 +239,64 @@ final class LdapProvisioner
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $targetGroups
|
||||
* @return array{success: bool, message: string, dn?: string}
|
||||
*/
|
||||
public function activateUser(string $username, array $targetGroups): array
|
||||
{
|
||||
try {
|
||||
$link = $this->connectAsService();
|
||||
$userDn = $this->userDnForUsername($username);
|
||||
$groupUpdates = $this->syncGroupsForUser($link, $username, $userDn, $targetGroups);
|
||||
|
||||
if (!@ldap_modify($link, $userDn, [
|
||||
'shadowExpire' => (string) ($this->config['ldap']['active_shadow_expire'] ?? -1),
|
||||
])) {
|
||||
throw new RuntimeException('Aktivierung im LDAP fehlgeschlagen: ' . $this->lastError($link));
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'Benutzer wurde aktiviert.',
|
||||
'dn' => $userDn,
|
||||
];
|
||||
} catch (\Throwable $exception) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $exception->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{success: bool, message: string, dn?: string}
|
||||
*/
|
||||
public function deactivateUser(string $username): array
|
||||
{
|
||||
try {
|
||||
$link = $this->connectAsService();
|
||||
$userDn = $this->userDnForUsername($username);
|
||||
|
||||
if (!@ldap_modify($link, $userDn, [
|
||||
'shadowExpire' => (string) ($this->config['ldap']['shadow_expire'] ?? 1),
|
||||
])) {
|
||||
throw new RuntimeException('Deaktivierung im LDAP fehlgeschlagen: ' . $this->lastError($link));
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'Benutzer wurde deaktiviert.',
|
||||
'dn' => $userDn,
|
||||
];
|
||||
} catch (\Throwable $exception) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $exception->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $request
|
||||
* @param array<string, mixed> $provisioning
|
||||
@@ -255,14 +372,20 @@ final class LdapProvisioner
|
||||
*/
|
||||
private function assignGroups($link, string $username, string $userDn, array $provisioning): array
|
||||
{
|
||||
$list = trim((string) ($provisioning['member_of_list'] ?? ''));
|
||||
$groupDns = $list !== ''
|
||||
? $this->parseGroupDns($list)
|
||||
: array_values(array_map('strval', (array) ($this->config['ldap']['default_groups'] ?? [])));
|
||||
return $this->syncGroupsForUser($link, $username, $userDn, $this->resolveTargetGroupsFromProvisioning($provisioning));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource|\LDAP\Connection $link
|
||||
* @param array<int, string> $targetGroups
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function syncGroupsForUser($link, string $username, string $userDn, array $targetGroups): array
|
||||
{
|
||||
$mode = (string) ($this->config['ldap']['group_assignment_mode'] ?? 'group_memberuid');
|
||||
$updated = [];
|
||||
|
||||
if ($groupDns === [] || $mode === 'none') {
|
||||
if ($targetGroups === [] || $mode === 'none') {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -272,8 +395,22 @@ final class LdapProvisioner
|
||||
|
||||
$attribute = (string) ($this->config['ldap']['group_member_attribute'] ?? 'memberUid');
|
||||
$value = $attribute === 'member' ? $userDn : $username;
|
||||
$knownGroups = array_values(array_unique(array_filter(array_merge(
|
||||
$targetGroups,
|
||||
array_map(static fn (array $group): string => (string) ($group['dn'] ?? ''), (array) ($this->config['ldap']['available_groups'] ?? [])),
|
||||
array_filter([(string) ($this->config['ldap']['pending_group_dn'] ?? '')]),
|
||||
array_values(array_map('strval', (array) ($this->config['ldap']['default_groups'] ?? [])))
|
||||
))));
|
||||
|
||||
foreach ($groupDns as $groupDn) {
|
||||
foreach ($knownGroups as $groupDn) {
|
||||
if (in_array($groupDn, $targetGroups, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ldap_mod_del($link, $groupDn, [$attribute => [$value]]);
|
||||
}
|
||||
|
||||
foreach ($targetGroups as $groupDn) {
|
||||
$result = @ldap_mod_add($link, $groupDn, [$attribute => [$value]]);
|
||||
|
||||
if ($result === false) {
|
||||
@@ -293,6 +430,21 @@ final class LdapProvisioner
|
||||
return $updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $provisioning
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function resolveTargetGroupsFromProvisioning(array $provisioning): array
|
||||
{
|
||||
$list = trim((string) ($provisioning['member_of_list'] ?? ''));
|
||||
|
||||
if ($list !== '') {
|
||||
return $this->parseGroupDns($list);
|
||||
}
|
||||
|
||||
return array_values(array_map('strval', (array) ($this->config['ldap']['default_groups'] ?? [])));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return resource|\LDAP\Connection
|
||||
*/
|
||||
|
||||
71
src/App/Mailer.php
Normal file
71
src/App/Mailer.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
final class Mailer
|
||||
{
|
||||
private JsonStore $logStore;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $config
|
||||
*/
|
||||
public function __construct(
|
||||
string $projectRoot,
|
||||
private readonly array $config,
|
||||
) {
|
||||
$logPath = $projectRoot . '/' . ltrim((string) ($this->config['mail']['log_path'] ?? 'data/mail.log'), '/');
|
||||
$this->logStore = new JsonStore($logPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{success: bool, message: string}
|
||||
*/
|
||||
public function send(string $toEmail, string $subject, string $body): array
|
||||
{
|
||||
$fromEmail = (string) ($this->config['mail']['from_email'] ?? '');
|
||||
$fromName = (string) ($this->config['mail']['from_name'] ?? '');
|
||||
$headers = [
|
||||
'MIME-Version: 1.0',
|
||||
'Content-Type: text/plain; charset=UTF-8',
|
||||
];
|
||||
|
||||
if ($fromEmail !== '') {
|
||||
$headers[] = 'From: ' . ($fromName !== '' ? sprintf('%s <%s>', $fromName, $fromEmail) : $fromEmail);
|
||||
}
|
||||
|
||||
$success = false;
|
||||
|
||||
if ((bool) ($this->config['mail']['enabled'] ?? false) && function_exists('mail')) {
|
||||
$success = @mail($toEmail, $subject, $body, implode("\r\n", $headers));
|
||||
}
|
||||
|
||||
$logged = $this->logMail($toEmail, $subject, $body);
|
||||
|
||||
if ($success) {
|
||||
return ['success' => true, 'message' => 'E-Mail wurde versendet.'];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $logged
|
||||
? 'Mailversand war nicht erfolgreich. Die Nachricht wurde im Mail-Log abgelegt.'
|
||||
: 'Mailversand war nicht erfolgreich.',
|
||||
];
|
||||
}
|
||||
|
||||
private function logMail(string $toEmail, string $subject, string $body): bool
|
||||
{
|
||||
$payload = $this->logStore->read();
|
||||
$payload[] = [
|
||||
'created_at' => gmdate('c'),
|
||||
'to' => $toEmail,
|
||||
'subject' => $subject,
|
||||
'body' => $body,
|
||||
];
|
||||
$this->logStore->write($payload);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
106
src/App/PasswordResetService.php
Normal file
106
src/App/PasswordResetService.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
final class PasswordResetService
|
||||
{
|
||||
private PasswordResetTokenStore $store;
|
||||
private Mailer $mailer;
|
||||
private LdapProvisioner $ldap;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $config
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly string $projectRoot,
|
||||
private readonly array $config,
|
||||
) {
|
||||
$tokenPath = $this->projectRoot . '/' . ltrim((string) ($this->config['reset_token_path'] ?? 'data/password-reset-tokens.json'), '/');
|
||||
$this->store = new PasswordResetTokenStore($tokenPath);
|
||||
$this->mailer = new Mailer($projectRoot, $config);
|
||||
$this->ldap = new LdapProvisioner($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{success: bool, message: string}
|
||||
*/
|
||||
public function request(string $username, string $origin): array
|
||||
{
|
||||
$username = strtolower(trim($username));
|
||||
|
||||
if ($username === '') {
|
||||
return ['success' => false, 'message' => 'Bitte einen Benutzernamen angeben.'];
|
||||
}
|
||||
|
||||
$user = $this->ldap->getUserByUsername($username, ['mail', 'displayName', 'uid']);
|
||||
|
||||
if ($user === null) {
|
||||
return ['success' => false, 'message' => 'Benutzer wurde im LDAP nicht gefunden.'];
|
||||
}
|
||||
|
||||
$email = (string) ($user['mail'] ?? '');
|
||||
|
||||
if ($email === '') {
|
||||
return ['success' => false, 'message' => 'Für diesen Benutzer ist keine E-Mail-Adresse im LDAP hinterlegt.'];
|
||||
}
|
||||
|
||||
$token = bin2hex(random_bytes(24));
|
||||
$expiresAt = gmdate('c', time() + ((int) ($this->config['reset_link_ttl_hours'] ?? 8) * 3600));
|
||||
$this->store->add([
|
||||
'token' => $token,
|
||||
'username' => $username,
|
||||
'email' => $email,
|
||||
'status' => 'active',
|
||||
'created_at' => gmdate('c'),
|
||||
'expires_at' => $expiresAt,
|
||||
]);
|
||||
|
||||
$link = rtrim($origin, '/') . '/auth/reset-password/?token=' . urlencode($token);
|
||||
$body = "Hallo,\n\nfür dein Kusche.Berlin Desktop-Konto wurde ein Passwort-Reset angefordert.\n\nLink: {$link}\n\nDer Link ist bis {$expiresAt} gültig.\n\nWenn du das nicht warst, kannst du diese E-Mail ignorieren.\n";
|
||||
$mail = $this->mailer->send($email, 'Passwort zurücksetzen', $body);
|
||||
|
||||
return $mail['success']
|
||||
? ['success' => true, 'message' => 'Reset-Link wurde per E-Mail versendet.']
|
||||
: ['success' => false, 'message' => $mail['message']];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function validate(string $token): ?array
|
||||
{
|
||||
return $this->store->findActive($token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{success: bool, message: string}
|
||||
*/
|
||||
public function complete(string $token, string $password, string $passwordConfirm): array
|
||||
{
|
||||
$record = $this->validate($token);
|
||||
|
||||
if ($record === null) {
|
||||
return ['success' => false, 'message' => 'Der Reset-Link ist ungültig oder abgelaufen.'];
|
||||
}
|
||||
|
||||
if (strlen($password) < 12) {
|
||||
return ['success' => false, 'message' => 'Das neue Passwort muss mindestens 12 Zeichen lang sein.'];
|
||||
}
|
||||
|
||||
if ($password !== $passwordConfirm) {
|
||||
return ['success' => false, 'message' => 'Die Passwörter stimmen nicht überein.'];
|
||||
}
|
||||
|
||||
$result = $this->ldap->updateUserPassword((string) ($record['username'] ?? ''), $password);
|
||||
|
||||
if (!($result['success'] ?? false)) {
|
||||
return ['success' => false, 'message' => (string) ($result['message'] ?? 'Passwortänderung fehlgeschlagen.')];
|
||||
}
|
||||
|
||||
$this->store->deactivate($token, 'used');
|
||||
|
||||
return ['success' => true, 'message' => 'Das Passwort wurde erfolgreich zurückgesetzt.'];
|
||||
}
|
||||
}
|
||||
75
src/App/PasswordResetTokenStore.php
Normal file
75
src/App/PasswordResetTokenStore.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
final class PasswordResetTokenStore
|
||||
{
|
||||
private JsonStore $store;
|
||||
|
||||
public function __construct(string $storagePath)
|
||||
{
|
||||
$this->store = new JsonStore($storagePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
$items = $this->store->read();
|
||||
return array_values(array_filter($items, 'is_array'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $record
|
||||
*/
|
||||
public function add(array $record): void
|
||||
{
|
||||
$items = $this->all();
|
||||
$items[] = $record;
|
||||
$this->store->write($items);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function findActive(string $token): ?array
|
||||
{
|
||||
foreach ($this->all() as $item) {
|
||||
if ((string) ($item['token'] ?? '') !== $token) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((string) ($item['status'] ?? 'active') !== 'active') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (strtotime((string) ($item['expires_at'] ?? '')) < time()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function deactivate(string $token, string $status): void
|
||||
{
|
||||
$items = $this->all();
|
||||
|
||||
foreach ($items as $index => $item) {
|
||||
if ((string) ($item['token'] ?? '') !== $token) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$items[$index]['status'] = $status;
|
||||
$items[$index]['used_at'] = gmdate('c');
|
||||
break;
|
||||
}
|
||||
|
||||
$this->store->write($items);
|
||||
}
|
||||
}
|
||||
42
src/App/RegistrationIdentityAllocator.php
Normal file
42
src/App/RegistrationIdentityAllocator.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
final class RegistrationIdentityAllocator
|
||||
{
|
||||
private JsonStore $store;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $config
|
||||
*/
|
||||
public function __construct(
|
||||
string $storagePath,
|
||||
private readonly array $config,
|
||||
) {
|
||||
$this->store = new JsonStore($storagePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{uid_number: string, gid_number: string, samba_sid: string}
|
||||
*/
|
||||
public function allocate(): array
|
||||
{
|
||||
$payload = $this->store->read();
|
||||
$nextUid = (int) ($payload['next_uid_number'] ?? ($this->config['ldap']['uid_number_start'] ?? 1000010));
|
||||
$nextRid = (int) ($payload['next_samba_rid'] ?? ($this->config['ldap']['samba_rid_start'] ?? 1025));
|
||||
$gidNumber = (int) ($this->config['ldap']['gid_number_default'] ?? 1000012);
|
||||
$domainSid = trim((string) ($this->config['ldap']['samba_domain_sid'] ?? ''));
|
||||
|
||||
$payload['next_uid_number'] = $nextUid + 1;
|
||||
$payload['next_samba_rid'] = $nextRid + 1;
|
||||
$this->store->write($payload);
|
||||
|
||||
return [
|
||||
'uid_number' => (string) $nextUid,
|
||||
'gid_number' => (string) $gidNumber,
|
||||
'samba_sid' => $domainSid !== '' ? $domainSid . '-' . $nextRid : '',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ final class RegistrationService
|
||||
private RegistrationLdifBuilder $ldifBuilder;
|
||||
private RegistrationSecretBox $secretBox;
|
||||
private LdapProvisioner $ldapProvisioner;
|
||||
private RegistrationIdentityAllocator $identityAllocator;
|
||||
private PasswordResetService $passwordResetService;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $config
|
||||
@@ -23,6 +25,9 @@ final class RegistrationService
|
||||
$this->ldifBuilder = new RegistrationLdifBuilder($this->config);
|
||||
$this->secretBox = new RegistrationSecretBox((string) ($this->config['password_crypto_key'] ?? ''));
|
||||
$this->ldapProvisioner = new LdapProvisioner($this->config);
|
||||
$counterPath = $this->resolveProjectPath((string) ($this->config['identity_counter_path'] ?? 'data/registration-identity-counter.json'));
|
||||
$this->identityAllocator = new RegistrationIdentityAllocator($counterPath, $this->config);
|
||||
$this->passwordResetService = new PasswordResetService($this->projectRoot, $this->config);
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
@@ -100,6 +105,10 @@ final class RegistrationService
|
||||
$errors[] = 'Für diesen Benutzernamen oder diese E-Mail existiert bereits eine offene oder freigegebene Registrierung.';
|
||||
}
|
||||
|
||||
if ($this->ldapProvisioner->usernameExists($username)) {
|
||||
$errors[] = 'Dieser Benutzername ist bereits im LDAP vergeben.';
|
||||
}
|
||||
|
||||
if ($errors !== []) {
|
||||
return [
|
||||
'success' => false,
|
||||
@@ -107,9 +116,26 @@ final class RegistrationService
|
||||
];
|
||||
}
|
||||
|
||||
$identities = $this->identityAllocator->allocate();
|
||||
$pendingGroup = trim((string) ($this->config['ldap']['pending_group_dn'] ?? ''));
|
||||
$provisioning = [
|
||||
'uid_number' => $identities['uid_number'],
|
||||
'gid_number' => $identities['gid_number'],
|
||||
'home_directory' => rtrim((string) ($this->config['ldap']['home_directory_prefix'] ?? '/home'), '/') . '/' . $username,
|
||||
'login_shell' => trim((string) ($this->config['ldap']['default_login_shell'] ?? '/bin/sh')),
|
||||
'member_of_list' => $pendingGroup,
|
||||
'samba_sid' => $identities['samba_sid'],
|
||||
'apple_generateduid' => '',
|
||||
'display_name' => $displayName,
|
||||
'cn' => $displayName !== '' ? $displayName : $username,
|
||||
'gecos' => $displayName,
|
||||
'admin_note' => '',
|
||||
'ldap_created_at' => gmdate('c'),
|
||||
];
|
||||
|
||||
$request = [
|
||||
'id' => bin2hex(random_bytes(8)),
|
||||
'status' => 'pending',
|
||||
'status' => 'pending_approval',
|
||||
'created_at' => gmdate('c'),
|
||||
'updated_at' => gmdate('c'),
|
||||
'username' => $username,
|
||||
@@ -125,8 +151,29 @@ final class RegistrationService
|
||||
'host' => (string) ($_SERVER['HTTP_HOST'] ?? ''),
|
||||
],
|
||||
'review' => null,
|
||||
'provisioning' => null,
|
||||
'provisioning' => $provisioning,
|
||||
];
|
||||
$passwordSecret = $this->secretBox->encrypt($password);
|
||||
|
||||
try {
|
||||
$ldapResult = $this->ldapProvisioner->createInactiveUser($request, $provisioning, $password);
|
||||
$request['password_secret'] = null;
|
||||
$request['provisioning']['ldap_dn'] = $ldapResult['dn'];
|
||||
$request['provisioning']['group_updates'] = $ldapResult['group_updates'];
|
||||
$request['provisioning']['status'] = 'ldap_created_inactive';
|
||||
$request['provisioning']['ldif_path'] = $this->writeLdifDraft($request['id'], $username, $this->ldifBuilder->build($request, $provisioning));
|
||||
} catch (\Throwable $exception) {
|
||||
$request['password_secret'] = $passwordSecret;
|
||||
$request['status'] = 'provisioning_failed';
|
||||
$request['provisioning']['status'] = 'provisioning_failed';
|
||||
$request['provisioning']['error'] = $exception->getMessage();
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'errors' => ['LDAP-Provisionierung bei Registrierung fehlgeschlagen: ' . $exception->getMessage()],
|
||||
'request' => $this->store->create($request),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
@@ -146,49 +193,45 @@ final class RegistrationService
|
||||
return ['success' => false, 'errors' => ['Die Registrierung wurde nicht gefunden.']];
|
||||
}
|
||||
|
||||
$uidNumber = trim((string) ($input['uid_number'] ?? ''));
|
||||
$gidNumber = trim((string) ($input['gid_number'] ?? ''));
|
||||
$homeDirectory = trim((string) ($input['home_directory'] ?? ''));
|
||||
$sambaSid = trim((string) ($input['samba_sid'] ?? ''));
|
||||
$memberOfList = trim((string) ($input['member_of_list'] ?? ''));
|
||||
$adminNote = trim((string) ($input['admin_note'] ?? ''));
|
||||
$errors = [];
|
||||
|
||||
if ($uidNumber === '' || !ctype_digit($uidNumber)) {
|
||||
$errors[] = 'uidNumber ist erforderlich und muss numerisch sein.';
|
||||
}
|
||||
|
||||
if ($gidNumber === '' || !ctype_digit($gidNumber)) {
|
||||
$errors[] = 'gidNumber ist erforderlich und muss numerisch sein.';
|
||||
}
|
||||
|
||||
if ($homeDirectory === '') {
|
||||
$errors[] = 'homeDirectory ist erforderlich.';
|
||||
if ($memberOfList === '') {
|
||||
$errors[] = 'Für die Freischaltung muss mindestens eine Zielgruppe ausgewählt werden.';
|
||||
}
|
||||
|
||||
if ($errors !== []) {
|
||||
return ['success' => false, 'errors' => $errors];
|
||||
}
|
||||
|
||||
$activate = $this->ldapProvisioner->activateUser((string) ($request['username'] ?? ''), preg_split('/\r\n|\r|\n/', $memberOfList) ?: []);
|
||||
|
||||
if (!($activate['success'] ?? false)) {
|
||||
return ['success' => false, 'errors' => [(string) ($activate['message'] ?? 'Aktivierung fehlgeschlagen.')]];
|
||||
}
|
||||
|
||||
$provisioning = [
|
||||
'uid_number' => $uidNumber,
|
||||
'gid_number' => $gidNumber,
|
||||
'home_directory' => $homeDirectory,
|
||||
'login_shell' => trim((string) ($input['login_shell'] ?? ($this->config['ldap']['default_login_shell'] ?? '/bin/sh'))),
|
||||
'uid_number' => (string) ($request['provisioning']['uid_number'] ?? ''),
|
||||
'gid_number' => (string) ($request['provisioning']['gid_number'] ?? ''),
|
||||
'home_directory' => (string) ($request['provisioning']['home_directory'] ?? ''),
|
||||
'login_shell' => (string) ($request['provisioning']['login_shell'] ?? ($this->config['ldap']['default_login_shell'] ?? '/bin/sh')),
|
||||
'member_of_list' => $memberOfList,
|
||||
'samba_sid' => $sambaSid,
|
||||
'apple_generateduid' => trim((string) ($input['apple_generateduid'] ?? '')),
|
||||
'display_name' => trim((string) ($input['display_name'] ?? (string) ($request['display_name'] ?? ''))),
|
||||
'cn' => trim((string) ($input['cn'] ?? (string) ($request['display_name'] ?? $request['username'] ?? ''))),
|
||||
'gecos' => trim((string) ($input['gecos'] ?? (string) ($request['display_name'] ?? ''))),
|
||||
'samba_sid' => (string) ($request['provisioning']['samba_sid'] ?? ''),
|
||||
'apple_generateduid' => (string) ($request['provisioning']['apple_generateduid'] ?? ''),
|
||||
'display_name' => (string) ($request['provisioning']['display_name'] ?? $request['display_name'] ?? ''),
|
||||
'cn' => (string) ($request['provisioning']['cn'] ?? $request['display_name'] ?? $request['username'] ?? ''),
|
||||
'gecos' => (string) ($request['provisioning']['gecos'] ?? $request['display_name'] ?? ''),
|
||||
'admin_note' => $adminNote,
|
||||
'ldif_generated_at' => gmdate('c'),
|
||||
'ldap_activated_at' => gmdate('c'),
|
||||
'ldap_dn' => (string) ($activate['dn'] ?? ($request['provisioning']['ldap_dn'] ?? '')),
|
||||
'status' => 'approved_active',
|
||||
];
|
||||
|
||||
$request = $this->store->update(
|
||||
$id,
|
||||
static function (array $item) use ($provisioning, $reviewer, $adminNote): array {
|
||||
$item['status'] = 'approved_pending_provisioning';
|
||||
$item['status'] = 'approved_active';
|
||||
$item['updated_at'] = gmdate('c');
|
||||
$item['review'] = [
|
||||
'reviewed_at' => gmdate('c'),
|
||||
@@ -206,54 +249,6 @@ final class RegistrationService
|
||||
return ['success' => false, 'errors' => ['Die Registrierung konnte nicht aktualisiert werden.']];
|
||||
}
|
||||
|
||||
$ldif = $this->ldifBuilder->build($request, $provisioning);
|
||||
$ldifPath = $this->writeLdifDraft((string) ($request['id'] ?? ''), (string) ($request['username'] ?? ''), $ldif);
|
||||
|
||||
try {
|
||||
$passwordSecret = (string) ($request['password_secret'] ?? '');
|
||||
|
||||
if ($passwordSecret === '') {
|
||||
throw new \RuntimeException('Für diese Registrierung ist kein Passwort mehr hinterlegt.');
|
||||
}
|
||||
|
||||
$plainPassword = $this->secretBox->decrypt($passwordSecret);
|
||||
$ldapResult = $this->ldapProvisioner->createInactiveUser($request, $provisioning, $plainPassword);
|
||||
|
||||
$request = $this->store->update(
|
||||
$id,
|
||||
static function (array $item) use ($ldifPath, $ldapResult): array {
|
||||
$item['status'] = 'ldap_created_inactive';
|
||||
$item['updated_at'] = gmdate('c');
|
||||
$item['password_secret'] = null;
|
||||
$item['provisioning']['ldif_path'] = $ldifPath;
|
||||
$item['provisioning']['status'] = 'ldap_created_inactive';
|
||||
$item['provisioning']['ldap_dn'] = $ldapResult['dn'];
|
||||
$item['provisioning']['group_updates'] = $ldapResult['group_updates'];
|
||||
|
||||
return $item;
|
||||
}
|
||||
);
|
||||
} catch (\Throwable $exception) {
|
||||
$request = $this->store->update(
|
||||
$id,
|
||||
static function (array $item) use ($ldifPath, $exception): array {
|
||||
$item['status'] = 'provisioning_failed';
|
||||
$item['updated_at'] = gmdate('c');
|
||||
$item['provisioning']['ldif_path'] = $ldifPath;
|
||||
$item['provisioning']['status'] = 'provisioning_failed';
|
||||
$item['provisioning']['error'] = $exception->getMessage();
|
||||
|
||||
return $item;
|
||||
}
|
||||
);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'errors' => ['LDAP-Provisionierung fehlgeschlagen: ' . $exception->getMessage()],
|
||||
'request' => $request,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'request' => $request,
|
||||
@@ -297,6 +292,66 @@ final class RegistrationService
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{success: bool, message: string}
|
||||
*/
|
||||
public function sendPasswordReset(string $username, string $origin): array
|
||||
{
|
||||
return $this->passwordResetService->request($username, $origin);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{success: bool, message: string}
|
||||
*/
|
||||
public function deactivateUser(string $username): array
|
||||
{
|
||||
return $this->ldapProvisioner->deactivateUser($username);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{success: bool, message: string}
|
||||
*/
|
||||
public function activateExistingUser(string $username, array $groupDns): array
|
||||
{
|
||||
return $this->ldapProvisioner->activateUser($username, $groupDns);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{success: bool, message: string}
|
||||
*/
|
||||
public function setUserPassword(string $username, string $password): array
|
||||
{
|
||||
return $this->ldapProvisioner->updateUserPassword($username, $password);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function pendingApprovals(): array
|
||||
{
|
||||
return array_values(array_filter(
|
||||
$this->all(),
|
||||
static fn (array $item): bool => in_array((string) ($item['status'] ?? ''), ['pending_approval', 'provisioning_failed'], true)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, string>>
|
||||
*/
|
||||
public function availableGroups(): array
|
||||
{
|
||||
return array_values(array_filter(
|
||||
array_map(
|
||||
static fn (array $group): array => [
|
||||
'dn' => (string) ($group['dn'] ?? ''),
|
||||
'label' => (string) ($group['label'] ?? ($group['dn'] ?? '')),
|
||||
],
|
||||
(array) ($this->config['ldap']['available_groups'] ?? [])
|
||||
),
|
||||
static fn (array $group): bool => $group['dn'] !== ''
|
||||
));
|
||||
}
|
||||
|
||||
private function writeLdifDraft(string $id, string $username, string $content): string
|
||||
{
|
||||
$directory = $this->resolveProjectPath((string) ($this->config['ldif_export_dir'] ?? 'data/registration-ldif'));
|
||||
|
||||
Reference in New Issue
Block a user