loadMissing('serverAgent'); $meta = (array) ($order->meta ?? []); $agentMeta = (array) ($meta['server_agent'] ?? []); $bootstrapToken = $this->decryptBootstrapToken((string) ($agentMeta['bootstrap_token_encrypted'] ?? '')); if ($bootstrapToken === null) { $bootstrapToken = Str::random(64); } $agent = $order->serverAgent ?: new ServerAgent([ 'customer_hosting_order_id' => $order->id, 'uuid' => (string) Str::uuid(), 'status' => ServerAgent::STATUS_PENDING_REGISTRATION, ]); $agent->fill([ 'bootstrap_token_hash' => hash('sha256', $bootstrapToken), ]); $agent->save(); $agentMeta = array_merge($agentMeta, [ 'agent_id' => $agent->id, 'agent_uuid' => $agent->uuid, 'bootstrap_token_encrypted' => encrypt($bootstrapToken), 'registration_url' => route('api.server-agents.register'), 'heartbeat_url' => route('api.server-agents.heartbeat'), 'task_progress_url_template' => url('/api/server-agents/tasks/{task}/progress'), 'task_complete_url_template' => url('/api/server-agents/tasks/{task}/complete'), 'task_fail_url_template' => url('/api/server-agents/tasks/{task}/fail'), ]); $meta['server_agent'] = $agentMeta; $meta['server_panel_runtime'] = $this->withRuntimeAgentState( (array) ($meta['server_panel_runtime'] ?? []), $agent ); $order->update(['meta' => $meta]); return [ 'agent' => $agent->fresh(), 'bootstrap_token' => $bootstrapToken, 'registration_url' => $agentMeta['registration_url'], 'heartbeat_url' => $agentMeta['heartbeat_url'], ]; } /** * @param array $attributes * @return array{agent: ServerAgent, access_token: string} */ public function register(CustomerHostingOrder $order, string $bootstrapToken, array $attributes): array { $bootstrap = $this->ensureBootstrap($order); $agent = $bootstrap['agent']; if (! hash_equals((string) $agent->bootstrap_token_hash, hash('sha256', $bootstrapToken))) { throw ValidationException::withMessages([ 'bootstrap_token' => 'The bootstrap token is invalid.', ]); } $accessToken = Str::random(80); $agent->fill([ 'status' => ServerAgent::STATUS_ONLINE, 'hostname' => (string) ($attributes['hostname'] ?? $agent->hostname), 'agent_version' => (string) ($attributes['agent_version'] ?? $agent->agent_version), 'platform' => (string) ($attributes['platform'] ?? $agent->platform), 'capabilities' => array_values(array_unique((array) ($attributes['capabilities'] ?? []))), 'metadata' => (array) ($attributes['metadata'] ?? $agent->metadata ?? []), 'access_token_hash' => hash('sha256', $accessToken), 'access_token_issued_at' => now(), 'registered_at' => $agent->registered_at ?? now(), 'last_heartbeat_at' => now(), 'last_seen_ip' => (string) ($attributes['last_seen_ip'] ?? $agent->last_seen_ip), ]); $agent->save(); $this->syncOrderRuntime($order->fresh(), $agent->fresh()); return [ 'agent' => $agent->fresh(), 'access_token' => $accessToken, ]; } /** * @param array $attributes */ public function recordHeartbeat(ServerAgent $agent, array $attributes = []): ServerAgent { $agent->fill([ 'status' => ServerAgent::STATUS_ONLINE, 'hostname' => (string) ($attributes['hostname'] ?? $agent->hostname), 'agent_version' => (string) ($attributes['agent_version'] ?? $agent->agent_version), 'platform' => (string) ($attributes['platform'] ?? $agent->platform), 'capabilities' => array_values(array_unique((array) ($attributes['capabilities'] ?? $agent->capabilities ?? []))), 'metadata' => array_replace((array) ($agent->metadata ?? []), (array) ($attributes['metadata'] ?? [])), 'last_heartbeat_at' => now(), 'last_seen_ip' => (string) ($attributes['last_seen_ip'] ?? $agent->last_seen_ip), ]); $agent->save(); if ($agent->relationLoaded('order')) { $this->syncOrderRuntime($agent->order, $agent); } else { $this->syncOrderRuntime($agent->order()->firstOrFail(), $agent); } return $agent->fresh(); } public function syncOrderRuntime(CustomerHostingOrder $order, ServerAgent $agent): void { $meta = (array) ($order->meta ?? []); $meta['server_agent'] = array_merge((array) ($meta['server_agent'] ?? []), [ 'agent_id' => $agent->id, 'agent_uuid' => $agent->uuid, 'status' => $agent->status, 'hostname' => $agent->hostname, 'agent_version' => $agent->agent_version, 'platform' => $agent->platform, 'capabilities' => (array) ($agent->capabilities ?? []), 'registered_at' => optional($agent->registered_at)->toIso8601String(), 'last_heartbeat_at' => optional($agent->last_heartbeat_at)->toIso8601String(), ]); $meta['server_panel_runtime'] = $this->withRuntimeAgentState( (array) ($meta['server_panel_runtime'] ?? []), $agent ); $order->update(['meta' => $meta]); } /** * @param array $runtime * @return array */ private function withRuntimeAgentState(array $runtime, ServerAgent $agent): array { $current = array_values(array_unique(array_merge( (array) ($runtime['current_capabilities'] ?? []), ['Agent registration', 'Async task dispatch'] ))); if ($agent->status === ServerAgent::STATUS_ONLINE) { $current = array_values(array_unique(array_merge($current, [ 'Files', 'Databases', 'Domains', 'SSL', 'PHP', 'Cron', 'Logs', 'Site management', ]))); } $runtime['hosting_panel_ready'] = $agent->registered_at !== null; $runtime['future_hosting_panel_status'] = $agent->registered_at !== null ? 'agent_registered' : ($runtime['future_hosting_panel_status'] ?? 'planned'); $runtime['agent_status'] = $agent->status; $runtime['current_capabilities'] = $current; $runtime['secondary_message'] = $agent->registered_at !== null ? 'The server agent is registered. Managed tasks for files, databases, domains, SSL, PHP, cron, logs, and site actions are now queued through Ladill.' : ($runtime['secondary_message'] ?? null); return $runtime; } private function decryptBootstrapToken(string $encryptedToken): ?string { if ($encryptedToken === '') { return null; } try { $token = decrypt($encryptedToken); } catch (\Throwable) { return null; } return is_string($token) && $token !== '' ? $token : null; } }