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.'); } } }