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,274 @@
<?php
declare(strict_types=1);
namespace App;
final class RegistrationService
{
private RegistrationStore $store;
private RegistrationLdifBuilder $ldifBuilder;
/**
* @param array<string, mixed> $config
*/
public function __construct(
private readonly string $projectRoot,
private readonly array $config,
) {
$storagePath = $this->resolveProjectPath((string) ($this->config['storage_path'] ?? 'data/registration-requests.json'));
$this->store = new RegistrationStore($storagePath);
$this->ldifBuilder = new RegistrationLdifBuilder($this->config);
}
public function isEnabled(): bool
{
return (bool) ($this->config['enabled'] ?? false);
}
/**
* @return array<int, array<string, mixed>>
*/
public function all(): array
{
return $this->store->all();
}
/**
* @return array<string, mixed>|null
*/
public function find(string $id): ?array
{
return $this->store->find($id);
}
/**
* @param array<string, string> $input
* @return array{success: bool, errors?: array<int, string>, request?: array<string, mixed>}
*/
public function submit(array $input): array
{
$username = strtolower(trim((string) ($input['username'] ?? '')));
$givenName = trim((string) ($input['given_name'] ?? ''));
$familyName = trim((string) ($input['family_name'] ?? ''));
$email = strtolower(trim((string) ($input['email'] ?? '')));
$note = trim((string) ($input['note'] ?? ''));
$displayName = trim($givenName . ' ' . $familyName);
$errors = [];
if (!preg_match('/^[a-z0-9._-]{3,32}$/', $username)) {
$errors[] = 'Der Benutzername muss 3 bis 32 Zeichen lang sein und darf nur Kleinbuchstaben, Zahlen, Punkt, Minus oder Unterstrich enthalten.';
}
if ($givenName === '') {
$errors[] = 'Der Vorname ist erforderlich.';
}
if ($familyName === '') {
$errors[] = 'Der Nachname ist erforderlich.';
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = 'Bitte gib eine gültige E-Mail-Adresse an.';
}
if ($note !== '' && mb_strlen($note) > 2000) {
$errors[] = 'Die Zusatzinfo ist zu lang.';
}
$conflict = $this->store->findConflict($username, $email);
if ($conflict !== null) {
$errors[] = 'Für diesen Benutzernamen oder diese E-Mail existiert bereits eine offene oder freigegebene Registrierung.';
}
if ($errors !== []) {
return [
'success' => false,
'errors' => $errors,
];
}
$request = [
'id' => bin2hex(random_bytes(8)),
'status' => 'pending',
'created_at' => gmdate('c'),
'updated_at' => gmdate('c'),
'username' => $username,
'given_name' => $givenName,
'family_name' => $familyName,
'display_name' => $displayName,
'email' => $email,
'note' => $note,
'origin' => [
'ip' => (string) ($_SERVER['REMOTE_ADDR'] ?? ''),
'user_agent' => (string) ($_SERVER['HTTP_USER_AGENT'] ?? ''),
'host' => (string) ($_SERVER['HTTP_HOST'] ?? ''),
],
'review' => null,
'provisioning' => null,
];
return [
'success' => true,
'request' => $this->store->create($request),
];
}
/**
* @param array<string, string> $input
* @return array{success: bool, errors?: array<int, string>, request?: array<string, mixed>}
*/
public function approve(string $id, array $input, string $reviewer): array
{
$request = $this->store->find($id);
if ($request === null) {
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'] ?? ''));
$memberOfCsv = trim((string) ($input['member_of_csv'] ?? ''));
$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 ($errors !== []) {
return ['success' => false, 'errors' => $errors];
}
$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'))),
'member_of_csv' => $memberOfCsv,
'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'] ?? ''))),
'admin_note' => $adminNote,
'ldif_generated_at' => gmdate('c'),
];
$request = $this->store->update(
$id,
static function (array $item) use ($provisioning, $reviewer, $adminNote): array {
$item['status'] = 'approved_pending_provisioning';
$item['updated_at'] = gmdate('c');
$item['review'] = [
'reviewed_at' => gmdate('c'),
'reviewed_by' => $reviewer,
'decision' => 'approved',
'note' => $adminNote,
];
$item['provisioning'] = $provisioning;
return $item;
}
);
if ($request === null) {
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);
$request = $this->store->update(
$id,
static function (array $item) use ($ldifPath): array {
$item['updated_at'] = gmdate('c');
$item['provisioning']['ldif_path'] = $ldifPath;
$item['provisioning']['status'] = 'ldif_generated';
return $item;
}
);
return [
'success' => true,
'request' => $request,
];
}
/**
* @return array{success: bool, errors?: array<int, string>, request?: array<string, mixed>}
*/
public function reject(string $id, string $reason, string $reviewer): array
{
$reason = trim($reason);
if ($reason === '') {
return ['success' => false, 'errors' => ['Bitte gib einen Ablehnungsgrund an.']];
}
$request = $this->store->update(
$id,
static function (array $item) use ($reason, $reviewer): array {
$item['status'] = 'rejected';
$item['updated_at'] = gmdate('c');
$item['review'] = [
'reviewed_at' => gmdate('c'),
'reviewed_by' => $reviewer,
'decision' => 'rejected',
'note' => $reason,
];
return $item;
}
);
if ($request === null) {
return ['success' => false, 'errors' => ['Die Registrierung wurde nicht gefunden.']];
}
return [
'success' => true,
'request' => $request,
];
}
private function writeLdifDraft(string $id, string $username, string $content): string
{
$directory = $this->resolveProjectPath((string) ($this->config['ldif_export_dir'] ?? 'data/registration-ldif'));
if (!is_dir($directory)) {
mkdir($directory, 0775, true);
}
$filename = $id . '-' . preg_replace('/[^a-z0-9._-]+/i', '-', $username) . '.ldif';
$path = rtrim($directory, '/') . '/' . $filename;
file_put_contents($path, $content, LOCK_EX);
return $path;
}
private function resolveProjectPath(string $path): string
{
if ($path === '') {
return $this->projectRoot . '/data/registration-requests.json';
}
if (str_starts_with($path, '/')) {
return $path;
}
return $this->projectRoot . '/' . ltrim($path, '/');
}
}