From dd7b3e6cecb23cd11236676833c4640a45dec42d Mon Sep 17 00:00:00 2001 From: rbsetiawan Date: Tue, 23 Jun 2026 16:15:12 +0700 Subject: [PATCH] feat: Service Tokens sync to geonetagent_dev.agent_tokens (Opsi C) - Add AgentTokenSyncService: push create/revoke/delete to geonetagent_dev.agent_tokens - Update ServiceTokenService: inject AgentTokenSyncService, sync on create/revoke/delete - Add database connection 'geonetagent' to config/database.php - Add GEONETAGENT_DB_* env vars to .env.example (username kosong = sync disabled) - Update context-index.md: document sync architecture and flow - Grant DELETE on agent_tokens to geonet_portal (applied on 10.100.1.25) - Set geonet_portal password on DB server (stored in server .env only) Alur: UI Create Token -> INSERT service_tokens (audit) + INSERT agent_tokens (validasi collector) Fallback: jika GEONETAGENT_DB_USERNAME kosong, sync di-skip, geonet-console tetap jalan normal. --- server-connection/.cursor/context-index.md | 26 +++- server-connection/geonet-console/.env.example | 10 ++ .../app/Services/AgentTokenSyncService.php | 120 ++++++++++++++++++ .../app/Services/ServiceTokenService.php | 24 +++- .../geonet-console/config/database.php | 13 ++ 5 files changed, 188 insertions(+), 5 deletions(-) create mode 100644 server-connection/geonet-console/app/Services/AgentTokenSyncService.php diff --git a/server-connection/.cursor/context-index.md b/server-connection/.cursor/context-index.md index 4fcd0c9..1cdc840 100644 --- a/server-connection/.cursor/context-index.md +++ b/server-connection/.cursor/context-index.md @@ -295,12 +295,30 @@ Service Tokens adalah fitur di `geonet-console` untuk mengelola Bearer token mic | POST | `/api-clients/service-tokens/{id}/regenerate` | Regenerate token | | DELETE | `/api-clients/service-tokens/{id}` | Hapus token | +## Sinkronisasi ke geonetagent_dev (Opsi C) + +geonet-console bertindak sebagai **single point of control** — saat token dibuat/direvoke/dihapus di UI, perubahan otomatis disinkronkan ke `geonetagent_dev.agent_tokens` sehingga GeoNetAgent collector langsung merespons. + +| Komponen | Peran | +|----------|-------| +| `AgentTokenSyncService` | Push create/revoke/delete ke `geonetagent_dev.agent_tokens` | +| DB connection `geonetagent` | Config di `config/database.php`, env: `GEONETAGENT_DB_*` | +| DB user | `geonet_portal` — privileges: INSERT, SELECT, UPDATE, DELETE pada `agent_tokens` | +| Fallback | Jika `GEONETAGENT_DB_USERNAME` kosong, sync di-skip (warning log), geonet-console tetap jalan | + +**Alur setelah implementasi:** +``` +UI Create Token (geonet-console) + → INSERT ke service_tokens (geonet_console DB) ← audit trail + → INSERT ke agent_tokens (geonetagent_dev DB) ← validasi aktual collector + → Token ditampilkan sekali → user copy ke config.json agent +``` + ## Zero Impact pada GeoNetAgent -- Database `service_tokens` ada di `geonet_console`, **bukan** di `geonetagent_dev` -- GeoNetAgent collector (`auth.py`) tetap membaca tabel `agent_tokens` di `geonetagent_dev` -- Tidak ada perubahan pada kode collector atau agent PowerShell -- Deploy fitur ini **tidak mengganggu** agent yang sudah terinstall +- GeoNetAgent collector (`auth.py`) tetap membaca `agent_tokens` di `geonetagent_dev` — tidak ada perubahan kode +- Tidak ada perubahan pada agent PowerShell +- Sync bersifat additive — jika geonet-console tidak bisa connect ke `geonetagent_dev`, token di `service_tokens` tetap tersimpan dan error hanya di-log --- diff --git a/server-connection/geonet-console/.env.example b/server-connection/geonet-console/.env.example index 4d63d3a..ebc5690 100644 --- a/server-connection/geonet-console/.env.example +++ b/server-connection/geonet-console/.env.example @@ -133,5 +133,15 @@ PROJECT_SEARCH_OAUTH_SCOPE=project-search:read +# GeoNetAgent DB sync (PostgreSQL 10.100.1.25) +# Kosongkan GEONETAGENT_DB_USERNAME untuk menonaktifkan sync (safe default) +GEONETAGENT_DB_HOST=10.100.1.25 +GEONETAGENT_DB_PORT=5432 +GEONETAGENT_DB_DATABASE=geonetagent +GEONETAGENT_DB_USERNAME= +GEONETAGENT_DB_PASSWORD= + + + TZ=Asia/Jakarta diff --git a/server-connection/geonet-console/app/Services/AgentTokenSyncService.php b/server-connection/geonet-console/app/Services/AgentTokenSyncService.php new file mode 100644 index 0000000..82b0386 --- /dev/null +++ b/server-connection/geonet-console/app/Services/AgentTokenSyncService.php @@ -0,0 +1,120 @@ + 'geonetagent' + * Env vars: GEONETAGENT_DB_HOST, GEONETAGENT_DB_DATABASE, GEONETAGENT_DB_USERNAME, GEONETAGENT_DB_PASSWORD + * + * Jika koneksi tidak tersedia (env kosong), sync di-skip dengan warning log + * sehingga geonet-console tetap berjalan normal. + */ +class AgentTokenSyncService +{ + private bool $enabled; + + public function __construct() + { + $this->enabled = !empty(env('GEONETAGENT_DB_USERNAME')); + } + + /** + * Tambah token ke geonetagent.agent_tokens + * + * @param string $name Token name (e.g. "GeoNetAgent-Production") + * @param string $tokenHash SHA256 hash dari plain token + * @return bool true jika berhasil, false jika skip/gagal + */ + public function addToken(string $name, string $tokenHash): bool + { + if (!$this->enabled) { + Log::warning('AgentTokenSyncService: GEONETAGENT_DB_USERNAME tidak di-set, sync di-skip.'); + return false; + } + + try { + DB::connection('geonetagent')->table('agent_tokens')->insert([ + 'id' => (string) \Illuminate\Support\Str::uuid(), + 'name' => $name, + 'token_hash' => $tokenHash, + 'is_active' => true, + 'created_at' => now(), + 'last_used_at' => null, + ]); + return true; + } catch (\Throwable $e) { + Log::error('AgentTokenSyncService: gagal insert ke agent_tokens', [ + 'error' => $e->getMessage(), + 'name' => $name, + ]); + return false; + } + } + + /** + * Revoke token di geonetagent.agent_tokens berdasarkan hash + * + * @param string $tokenHash SHA256 hash dari plain token + * @return bool + */ + public function revokeToken(string $tokenHash): bool + { + if (!$this->enabled) { + Log::warning('AgentTokenSyncService: GEONETAGENT_DB_USERNAME tidak di-set, revoke sync di-skip.'); + return false; + } + + try { + DB::connection('geonetagent')->table('agent_tokens') + ->where('token_hash', $tokenHash) + ->update(['is_active' => false]); + return true; + } catch (\Throwable $e) { + Log::error('AgentTokenSyncService: gagal revoke di agent_tokens', [ + 'error' => $e->getMessage(), + ]); + return false; + } + } + + /** + * Hapus token dari geonetagent.agent_tokens berdasarkan hash + * + * @param string $tokenHash + * @return bool + */ + public function deleteToken(string $tokenHash): bool + { + if (!$this->enabled) { + return false; + } + + try { + DB::connection('geonetagent')->table('agent_tokens') + ->where('token_hash', $tokenHash) + ->delete(); + return true; + } catch (\Throwable $e) { + Log::error('AgentTokenSyncService: gagal delete di agent_tokens', [ + 'error' => $e->getMessage(), + ]); + return false; + } + } + + /** + * Cek apakah sinkronisasi aktif (env terkonfigurasi) + */ + public function isEnabled(): bool + { + return $this->enabled; + } +} diff --git a/server-connection/geonet-console/app/Services/ServiceTokenService.php b/server-connection/geonet-console/app/Services/ServiceTokenService.php index 77c846e..4ba0ae0 100644 --- a/server-connection/geonet-console/app/Services/ServiceTokenService.php +++ b/server-connection/geonet-console/app/Services/ServiceTokenService.php @@ -3,6 +3,7 @@ namespace App\Services; use App\Models\ServiceToken; +use App\Services\AgentTokenSyncService; use App\Services\AuditLogService; use Illuminate\Support\Facades\Auth; @@ -15,7 +16,8 @@ use Illuminate\Support\Facades\Auth; class ServiceTokenService { public function __construct( - private AuditLogService $auditLogService + private AuditLogService $auditLogService, + private AgentTokenSyncService $agentTokenSync ) {} /** @@ -37,6 +39,12 @@ class ServiceTokenService expiresInDays: $expiresInDays ); + // Sync ke geonetagent.agent_tokens jika service adalah GeoNetAgent + $synced = false; + if ($this->agentTokenSync->isEnabled()) { + $synced = $this->agentTokenSync->addToken($name, $result['model']->token_hash); + } + // Audit log $this->auditLogService->log( action: 'service_token_created', @@ -47,6 +55,7 @@ class ServiceTokenService 'service' => $service, 'token_prefix' => $result['model']->token_prefix, 'expires_at' => $result['model']->expires_at?->toIso8601String(), + 'agent_token_synced' => $synced, ], userId: $createdBy ); @@ -79,6 +88,12 @@ class ServiceTokenService { $token->update(['is_active' => false]); + // Sync revoke ke geonetagent.agent_tokens + $synced = false; + if ($this->agentTokenSync->isEnabled()) { + $synced = $this->agentTokenSync->revokeToken($token->token_hash); + } + $createdBy = Auth::check() ? Auth::user()->username : 'system'; $this->auditLogService->log( @@ -89,6 +104,7 @@ class ServiceTokenService 'name' => $token->name, 'service' => $token->service, 'token_prefix' => $token->token_prefix, + 'agent_token_synced' => $synced, ], userId: $createdBy ); @@ -103,9 +119,15 @@ class ServiceTokenService $tokenName = $token->name; $tokenService = $token->service; $tokenPrefix = $token->token_prefix; + $tokenHash = $token->token_hash; $token->delete(); + // Sync delete ke geonetagent.agent_tokens + if ($this->agentTokenSync->isEnabled()) { + $this->agentTokenSync->deleteToken($tokenHash); + } + $createdBy = Auth::check() ? Auth::user()->username : 'system'; $this->auditLogService->log( diff --git a/server-connection/geonet-console/config/database.php b/server-connection/geonet-console/config/database.php index d441a0e..187e884 100644 --- a/server-connection/geonet-console/config/database.php +++ b/server-connection/geonet-console/config/database.php @@ -68,6 +68,19 @@ return [ 'search_path' => 'public', 'sslmode' => 'prefer', ], + 'geonetagent' => [ + 'driver' => 'pgsql', + 'host' => env('GEONETAGENT_DB_HOST', '10.100.1.25'), + 'port' => env('GEONETAGENT_DB_PORT', '5432'), + 'database' => env('GEONETAGENT_DB_DATABASE', 'geonetagent'), + 'username' => env('GEONETAGENT_DB_USERNAME', ''), + 'password' => env('GEONETAGENT_DB_PASSWORD', ''), + 'charset' => 'utf8', + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'public', + 'sslmode' => 'prefer', + ], ], 'migrations' => [ 'table' => 'migrations',