Browse Source

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.
master
Budi Setiawan 3 weeks ago
parent
commit
dd7b3e6cec
  1. 26
      server-connection/.cursor/context-index.md
  2. 10
      server-connection/geonet-console/.env.example
  3. 120
      server-connection/geonet-console/app/Services/AgentTokenSyncService.php
  4. 24
      server-connection/geonet-console/app/Services/ServiceTokenService.php
  5. 13
      server-connection/geonet-console/config/database.php

26
server-connection/.cursor/context-index.md

@ -295,12 +295,30 @@ Service Tokens adalah fitur di `geonet-console` untuk mengelola Bearer token mic @@ -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
---

10
server-connection/geonet-console/.env.example

@ -133,5 +133,15 @@ PROJECT_SEARCH_OAUTH_SCOPE=project-search:read @@ -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

120
server-connection/geonet-console/app/Services/AgentTokenSyncService.php

@ -0,0 +1,120 @@ @@ -0,0 +1,120 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* AgentTokenSyncService
*
* Sinkronisasi token ke geonetagent.agent_tokens agar geonet-console
* menjadi single point of control untuk microservice Bearer tokens.
*
* geonetagent DB connection: config/database.php -> '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;
}
}

24
server-connection/geonet-console/app/Services/ServiceTokenService.php

@ -3,6 +3,7 @@ @@ -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; @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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(

13
server-connection/geonet-console/config/database.php

@ -68,6 +68,19 @@ return [ @@ -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',

Loading…
Cancel
Save