feat: complete jobcard operations and access controls

This commit is contained in:
Marco0300
2026-09-01 20:07:20 +02:00
parent 703c3ca67d
commit 168c15c6ae
12 changed files with 1241 additions and 29 deletions
+133
View File
@@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
namespace App\Domain\User;
final class UserRecord
{
/** @var list<string> */
private const DISPLAY_FIELDS = [
'id',
'name',
'email',
'role_id',
'role_name',
'is_active',
'last_login_at',
'created_at',
'updated_at',
];
private readonly PasswordPolicy $passwordPolicy;
public function __construct(?PasswordPolicy $passwordPolicy = null)
{
$this->passwordPolicy = $passwordPolicy ?? new PasswordPolicy();
}
/** @return array{name: string, email: string, role_id: int|string|null, is_active: bool|mixed} */
public function normalize(array $record): array
{
return [
'name' => trim(is_scalar($record['name'] ?? null) ? (string) $record['name'] : ''),
'email' => strtolower(trim(is_scalar($record['email'] ?? null) ? (string) $record['email'] : '')),
'role_id' => $this->normalizeRoleId($record['role_id'] ?? null),
'is_active' => $this->normalizeActive($record['is_active'] ?? $record['active'] ?? true),
];
}
/** @return array{name: string, email: string, role_id: int|string|null, is_active: bool|mixed, valid: bool, errors: array<string, string>} */
public function validate(array $record): array
{
$normalized = $this->normalize($record);
$errors = [];
if ($normalized['name'] === '') {
$errors['name'] = 'User name is required.';
} elseif (mb_strlen($normalized['name']) > 120) {
$errors['name'] = 'User name must be 120 characters or fewer.';
}
if ($normalized['email'] === '') {
$errors['email'] = 'Email address is required.';
} elseif (mb_strlen($normalized['email']) > 190) {
$errors['email'] = 'Email address must be 190 characters or fewer.';
} elseif (filter_var($normalized['email'], FILTER_VALIDATE_EMAIL) === false) {
$errors['email'] = 'Email address must be valid.';
}
if (!is_int($normalized['role_id']) || $normalized['role_id'] < 1) {
$errors['role_id'] = 'Role must be a positive integer.';
}
if (!is_bool($normalized['is_active'])) {
$errors['is_active'] = 'Active flag must be boolean.';
}
return [...$normalized, 'valid' => $errors === [], 'errors' => $errors];
}
/**
* Validates a new user and its initial password without returning the
* plaintext password in the result.
*
* @return array{name: string, email: string, role_id: int|string|null, is_active: bool|mixed, valid: bool, errors: array<string, string>}
*/
public function validateForCreate(array $record): array
{
$result = $this->validate($record);
$password = $this->passwordPolicy->validateInitial($record['password'] ?? null);
if (!$password['valid']) {
$result['errors']['password'] = 'Password requires: ' . implode(', ', $password['errors']) . '.';
$result['valid'] = false;
}
return $result;
}
/** @return array<string, mixed> */
public function display(array $record): array
{
$safe = [];
foreach (self::DISPLAY_FIELDS as $field) {
if (array_key_exists($field, $record)) {
$safe[$field] = $record[$field];
}
}
return $safe;
}
/** @return array<string, mixed> */
public function toDisplay(array $record): array
{
return $this->display($record);
}
private function normalizeRoleId(mixed $value): int|string|null
{
if (is_int($value)) {
return $value;
}
if (is_string($value) && preg_match('/^[0-9]+$/', trim($value)) === 1) {
return (int) trim($value);
}
return is_scalar($value) || $value === null ? $value : null;
}
private function normalizeActive(mixed $value): mixed
{
if (is_bool($value)) {
return $value;
}
if (is_int($value) && ($value === 0 || $value === 1)) {
return $value === 1;
}
if (is_string($value)) {
return match (strtolower(trim($value))) {
'1', 'true', 'yes', 'on' => true,
'0', 'false', 'no', 'off' => false,
default => $value,
};
}
return $value;
}
}