365 lines
12 KiB
PHP
365 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
final class UserRepository
|
|
{
|
|
private string $path;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->path = storage_path('system/users.json');
|
|
}
|
|
|
|
public function hasAnyUsers(): bool
|
|
{
|
|
return count($this->all()) > 0;
|
|
}
|
|
|
|
public function all(): array
|
|
{
|
|
$data = decode_json_file($this->path, ['users' => []]);
|
|
|
|
return array_values(array_filter($data['users'] ?? [], 'is_array'));
|
|
}
|
|
|
|
public function find(string $username): ?array
|
|
{
|
|
$needle = normalize_username($username);
|
|
|
|
foreach ($this->all() as $user) {
|
|
if (($user['username'] ?? '') === $needle) {
|
|
return $user;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public function verify(string $username, string $password): ?array
|
|
{
|
|
$user = $this->find($username) ?? [];
|
|
|
|
if ($user === null) {
|
|
return null;
|
|
}
|
|
|
|
if (!password_verify($password, (string) ($user['password_hash'] ?? ''))) {
|
|
return null;
|
|
}
|
|
|
|
return $user;
|
|
}
|
|
|
|
public function findByRememberToken(string $selector, string $validator): ?array
|
|
{
|
|
$validatorHash = hash('sha256', $validator);
|
|
$now = time();
|
|
|
|
foreach ($this->all() as $user) {
|
|
$token = $user['remember_token'] ?? null;
|
|
|
|
if (!is_array($token) || !hash_equals((string) ($token['selector'] ?? ''), $selector)) {
|
|
continue;
|
|
}
|
|
|
|
$expiresAt = strtotime((string) ($token['expires_at'] ?? '')) ?: 0;
|
|
|
|
if ($expiresAt < $now) {
|
|
return null;
|
|
}
|
|
|
|
if (!hash_equals((string) ($token['validator_hash'] ?? ''), $validatorHash)) {
|
|
return null;
|
|
}
|
|
|
|
return $user;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public function storeRememberToken(string $username, string $selector, string $validatorHash, int $expiresAt): void
|
|
{
|
|
$normalized = normalize_username($username);
|
|
$users = $this->all();
|
|
$updated = false;
|
|
|
|
foreach ($users as &$user) {
|
|
if (($user['username'] ?? '') !== $normalized) {
|
|
continue;
|
|
}
|
|
|
|
$user['remember_token'] = [
|
|
'selector' => $selector,
|
|
'validator_hash' => $validatorHash,
|
|
'expires_at' => date(DATE_ATOM, $expiresAt),
|
|
'created_at' => date(DATE_ATOM),
|
|
];
|
|
$user['updated_at'] = date(DATE_ATOM);
|
|
$updated = true;
|
|
break;
|
|
}
|
|
unset($user);
|
|
|
|
if (!$updated) {
|
|
throw new RuntimeException('Der Remember-Me-Token konnte keinem Benutzer zugeordnet werden.');
|
|
}
|
|
|
|
$this->write(['users' => $users]);
|
|
}
|
|
|
|
public function clearRememberToken(string $username): void
|
|
{
|
|
$normalized = normalize_username($username);
|
|
$users = $this->all();
|
|
$updated = false;
|
|
|
|
foreach ($users as &$user) {
|
|
if (($user['username'] ?? '') !== $normalized || !array_key_exists('remember_token', $user)) {
|
|
continue;
|
|
}
|
|
|
|
unset($user['remember_token']);
|
|
$user['updated_at'] = date(DATE_ATOM);
|
|
$updated = true;
|
|
break;
|
|
}
|
|
unset($user);
|
|
|
|
if ($updated) {
|
|
$this->write(['users' => $users]);
|
|
}
|
|
}
|
|
|
|
public function findByHealthImportToken(string $token): ?array
|
|
{
|
|
$tokenHash = hash('sha256', $token);
|
|
|
|
foreach ($this->all() as $user) {
|
|
$config = $user['health_import'] ?? null;
|
|
|
|
if (!is_array($config) || empty($config['enabled'])) {
|
|
continue;
|
|
}
|
|
|
|
if (hash_equals((string) ($config['token_hash'] ?? ''), $tokenHash)) {
|
|
return $user;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public function healthImportConfig(string $username): array
|
|
{
|
|
$user = $this->find($username);
|
|
$config = is_array($user['health_import'] ?? null) ? $user['health_import'] : [];
|
|
|
|
return [
|
|
'enabled' => !empty($config['enabled']),
|
|
'token_prefix' => (string) ($config['token_prefix'] ?? ''),
|
|
'created_at' => (string) ($config['created_at'] ?? ''),
|
|
'last_import_at' => (string) ($config['last_import_at'] ?? ''),
|
|
'last_status' => (string) ($config['last_status'] ?? ''),
|
|
'last_message' => (string) ($config['last_message'] ?? ''),
|
|
'progress_done' => max(0, (int) ($config['progress_done'] ?? 0)),
|
|
'progress_total' => max(0, (int) ($config['progress_total'] ?? 0)),
|
|
'started_at' => (string) ($config['started_at'] ?? ''),
|
|
'updated_at' => (string) ($config['updated_at'] ?? ''),
|
|
'finished_at' => (string) ($config['finished_at'] ?? ''),
|
|
];
|
|
}
|
|
|
|
public function issueHealthImportToken(string $username): string
|
|
{
|
|
$token = 'mood_health_' . bin2hex(random_bytes(24));
|
|
$normalized = normalize_username($username);
|
|
$users = $this->all();
|
|
$updated = false;
|
|
|
|
foreach ($users as &$user) {
|
|
if (($user['username'] ?? '') !== $normalized) {
|
|
continue;
|
|
}
|
|
|
|
$currentConfig = is_array($user['health_import'] ?? null) ? $user['health_import'] : [];
|
|
$user['health_import'] = [
|
|
'enabled' => true,
|
|
'token_hash' => hash('sha256', $token),
|
|
'token_prefix' => substr($token, 0, 18),
|
|
'created_at' => date(DATE_ATOM),
|
|
'last_import_at' => (string) ($currentConfig['last_import_at'] ?? ''),
|
|
'last_status' => (string) ($currentConfig['last_status'] ?? ''),
|
|
'last_message' => (string) ($currentConfig['last_message'] ?? ''),
|
|
'progress_done' => max(0, (int) ($currentConfig['progress_done'] ?? 0)),
|
|
'progress_total' => max(0, (int) ($currentConfig['progress_total'] ?? 0)),
|
|
'started_at' => (string) ($currentConfig['started_at'] ?? ''),
|
|
'updated_at' => (string) ($currentConfig['updated_at'] ?? ''),
|
|
'finished_at' => (string) ($currentConfig['finished_at'] ?? ''),
|
|
];
|
|
$user['updated_at'] = date(DATE_ATOM);
|
|
$updated = true;
|
|
break;
|
|
}
|
|
unset($user);
|
|
|
|
if (!$updated) {
|
|
throw new RuntimeException('Der Health-Import-Token konnte keinem Benutzer zugeordnet werden.');
|
|
}
|
|
|
|
$this->write(['users' => $users]);
|
|
|
|
return $token;
|
|
}
|
|
|
|
public function revokeHealthImportToken(string $username): void
|
|
{
|
|
$normalized = normalize_username($username);
|
|
$users = $this->all();
|
|
$updated = false;
|
|
|
|
foreach ($users as &$user) {
|
|
if (($user['username'] ?? '') !== $normalized || !array_key_exists('health_import', $user)) {
|
|
continue;
|
|
}
|
|
|
|
unset($user['health_import']);
|
|
$user['updated_at'] = date(DATE_ATOM);
|
|
$updated = true;
|
|
break;
|
|
}
|
|
unset($user);
|
|
|
|
if ($updated) {
|
|
$this->write(['users' => $users]);
|
|
}
|
|
}
|
|
|
|
public function recordHealthImport(string $username, string $status, string $message): void
|
|
{
|
|
$normalized = normalize_username($username);
|
|
$users = $this->all();
|
|
$updated = false;
|
|
|
|
foreach ($users as &$user) {
|
|
if (($user['username'] ?? '') !== $normalized) {
|
|
continue;
|
|
}
|
|
|
|
$config = is_array($user['health_import'] ?? null) ? $user['health_import'] : [];
|
|
$config['last_import_at'] = date(DATE_ATOM);
|
|
$config['last_status'] = $status;
|
|
$config['last_message'] = substr($message, 0, 240);
|
|
$config['updated_at'] = date(DATE_ATOM);
|
|
if ($status !== 'running') {
|
|
$config['finished_at'] = date(DATE_ATOM);
|
|
if ($status === 'ok') {
|
|
$total = max(0, (int) ($config['progress_total'] ?? 0));
|
|
$config['progress_done'] = $total > 0 ? $total : max(0, (int) ($config['progress_done'] ?? 0));
|
|
}
|
|
}
|
|
$user['health_import'] = $config;
|
|
$user['updated_at'] = date(DATE_ATOM);
|
|
$updated = true;
|
|
break;
|
|
}
|
|
unset($user);
|
|
|
|
if ($updated) {
|
|
$this->write(['users' => $users]);
|
|
}
|
|
}
|
|
|
|
public function recordHealthImportProgress(string $username, string $message, int $done, int $total, ?string $startedAt = null): void
|
|
{
|
|
$normalized = normalize_username($username);
|
|
$users = $this->all();
|
|
$updated = false;
|
|
|
|
foreach ($users as &$user) {
|
|
if (($user['username'] ?? '') !== $normalized) {
|
|
continue;
|
|
}
|
|
|
|
$config = is_array($user['health_import'] ?? null) ? $user['health_import'] : [];
|
|
$config['last_status'] = 'running';
|
|
$config['last_message'] = substr($message, 0, 240);
|
|
$config['progress_done'] = max(0, min($done, max($total, 0)));
|
|
$config['progress_total'] = max(0, $total);
|
|
$config['started_at'] = $startedAt ?? (string) ($config['started_at'] ?? date(DATE_ATOM));
|
|
$config['updated_at'] = date(DATE_ATOM);
|
|
$config['finished_at'] = '';
|
|
$user['health_import'] = $config;
|
|
$user['updated_at'] = date(DATE_ATOM);
|
|
$updated = true;
|
|
break;
|
|
}
|
|
unset($user);
|
|
|
|
if ($updated) {
|
|
$this->write(['users' => $users]);
|
|
}
|
|
}
|
|
|
|
public function create(string $username, string $password, bool $isAdmin = false): array
|
|
{
|
|
$normalized = normalize_username($username);
|
|
|
|
if ($normalized === '' || $this->find($normalized) !== null) {
|
|
throw new RuntimeException('Benutzername existiert bereits oder ist ungültig.');
|
|
}
|
|
|
|
$users = $this->all();
|
|
$users[] = [
|
|
'username' => $normalized,
|
|
'password_hash' => password_hash($password, PASSWORD_DEFAULT),
|
|
'is_admin' => $isAdmin,
|
|
'created_at' => date(DATE_ATOM),
|
|
];
|
|
|
|
$this->write(['users' => $users]);
|
|
|
|
$createdUser = $this->find($normalized);
|
|
|
|
if ($createdUser === null) {
|
|
throw new RuntimeException('Der Account konnte nicht gespeichert werden.');
|
|
}
|
|
|
|
return $createdUser;
|
|
}
|
|
|
|
public function changePassword(string $username, string $password): void
|
|
{
|
|
$normalized = normalize_username($username);
|
|
$users = $this->all();
|
|
|
|
foreach ($users as &$user) {
|
|
if (($user['username'] ?? '') === $normalized) {
|
|
$user['password_hash'] = password_hash($password, PASSWORD_DEFAULT);
|
|
$user['updated_at'] = date(DATE_ATOM);
|
|
}
|
|
}
|
|
unset($user);
|
|
|
|
$this->write(['users' => $users]);
|
|
}
|
|
|
|
private function write(array $payload): void
|
|
{
|
|
if (!is_dir(dirname($this->path))) {
|
|
mkdir(dirname($this->path), 0775, true);
|
|
}
|
|
|
|
$bytes = file_put_contents(
|
|
$this->path,
|
|
json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
|
LOCK_EX
|
|
);
|
|
|
|
if ($bytes === false) {
|
|
throw new RuntimeException('Die Benutzerdatei konnte nicht geschrieben werden. Bitte prüfe die Schreibrechte von storage/system.');
|
|
}
|
|
}
|
|
}
|