false, 'error' => 'invalid_request']; } $db = DB::handle(); $db->begin_transaction(); try { $event = $this->userLifecycleAuditService->findDeleteEventForRestore($auditId, true); if (!$event) { $db->rollback(); return ['ok' => false, 'error' => 'audit_event_not_found']; } if (!empty($event['restored_at'])) { $db->rollback(); return ['ok' => false, 'error' => 'audit_event_already_restored']; } $snapshot = $this->userLifecycleAuditService->decryptSnapshot($event); if (!$snapshot) { $db->rollback(); return ['ok' => false, 'error' => 'snapshot_unavailable']; } $uuid = trim((string) ($snapshot['uuid'] ?? '')); $email = trim((string) ($snapshot['email'] ?? '')); if ($uuid === '' || $email === '') { $db->rollback(); return ['ok' => false, 'error' => 'snapshot_invalid']; } if ($this->userReadRepository->findByUuid($uuid)) { $db->rollback(); return ['ok' => false, 'error' => 'restore_uuid_exists']; } if ($this->userReadRepository->findByEmail($email)) { $db->rollback(); return ['ok' => false, 'error' => 'restore_email_exists']; } $createdId = $this->userWriteRepository->create([ 'uuid' => $uuid, 'first_name' => trim((string) ($snapshot['first_name'] ?? '')), 'last_name' => trim((string) ($snapshot['last_name'] ?? '')), 'email' => $email, 'profile_description' => $this->nullableText($snapshot['profile_description'] ?? null), 'job_title' => $this->nullableText($snapshot['job_title'] ?? null), 'phone' => $this->nullableText($snapshot['phone'] ?? null), 'mobile' => $this->nullableText($snapshot['mobile'] ?? null), 'short_dial' => $this->nullableText($snapshot['short_dial'] ?? null), 'address' => $this->nullableText($snapshot['address'] ?? null), 'postal_code' => $this->nullableText($snapshot['postal_code'] ?? null), 'city' => $this->nullableText($snapshot['city'] ?? null), 'country' => $this->nullableText($snapshot['country'] ?? null), 'region' => $this->nullableText($snapshot['region'] ?? null), 'hire_date' => $this->nullableDate($snapshot['hire_date'] ?? null), 'password' => $this->randomPassword(), 'locale' => $this->nullableText($snapshot['locale'] ?? null), 'totp_secret' => '', 'theme' => $this->normalizeTheme($snapshot['theme'] ?? null), 'primary_tenant_id' => null, 'current_tenant_id' => null, 'created_by' => $actorUserId, 'active' => 0, 'active_changed_at' => gmdate('Y-m-d H:i:s'), 'active_changed_by' => $actorUserId, ]); if (!$createdId) { $db->rollback(); return ['ok' => false, 'error' => 'restore_create_failed']; } $restoredUserId = (int) $createdId; $marked = $this->userLifecycleAuditService->markDeleteEventRestored( $auditId, $actorUserId, $restoredUserId ); if (!$marked) { $db->rollback(); return ['ok' => false, 'error' => 'restore_mark_failed']; } $this->userLifecycleAuditService->logRestore( RepoQuery::uuidV4(), 'manual', [ 'deactivate_days' => (int) ($event['policy_deactivate_days'] ?? 0), 'delete_days' => (int) ($event['policy_delete_days'] ?? 0), ], $actorUserId, [ 'id' => $restoredUserId, 'uuid' => $uuid, 'email' => $email, ], 'success', null ); $db->commit(); return [ 'ok' => true, 'restored_user_id' => $restoredUserId, 'restored_user_uuid' => $uuid, 'audit_id' => $auditId, ]; } catch (\Throwable $exception) { if ($db->errno === 0) { $db->rollback(); } else { $db->rollback(); } return ['ok' => false, 'error' => 'restore_unexpected_error']; } } private function nullableText(mixed $value): ?string { $value = trim((string) $value); return $value !== '' ? $value : null; } private function nullableDate(mixed $value): ?string { $value = trim((string) $value); if ($value === '') { return null; } if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) { return null; } return $value; } private function randomPassword(): string { return bin2hex(random_bytes(24)); } private function normalizeTheme(mixed $value): string { $theme = strtolower(trim((string) $value)); $themes = appThemes(); return isset($themes[$theme]) ? $theme : appDefaultTheme(); } }