Browse Source
- Add migration: create service_tokens table (UUID PK, name, service, token_prefix, token_hash, is_active, expires_at, created_by, last_used_at) - Add Model: ServiceToken with generate(), verify(), active/expired scopes - Add Service: ServiceTokenService with full CRUD + audit log integration - Add API Controller: GET/POST/DELETE /api/v1/service-tokens/* (9 endpoints) - Update ApiClientController: add web actions (store/revoke/regenerate/delete) - Update routes/api.php: register service token REST endpoints - Update routes/web.php: register service token web routes - Update api-clients/index.blade.php: add Create form + Service Tokens table with Regenerate/Revoke/Delete actions - Update context-index.md: document Service Tokens architecture, schema, endpoints - Update perintah.md: sync log Zero impact on existing GeoNetAgent agents (separate geonetagent_dev database).master
10 changed files with 1514 additions and 1 deletions
@ -0,0 +1,289 @@
@@ -0,0 +1,289 @@
|
||||
<?php |
||||
|
||||
namespace App\Http\Controllers\Api\V1; |
||||
|
||||
use App\Http\Controllers\Controller; |
||||
use App\Models\ServiceToken; |
||||
use App\Services\ServiceTokenService; |
||||
use Illuminate\Http\JsonResponse; |
||||
use Illuminate\Http\Request; |
||||
use Illuminate\Validation\Rule; |
||||
|
||||
/** |
||||
* ServiceTokenController - API endpoints for service token management |
||||
* |
||||
* Provides REST API for CRUD operations on service tokens. |
||||
* All endpoints require authentication via LDAP or API token. |
||||
*/ |
||||
class ServiceTokenController extends Controller |
||||
{ |
||||
public function __construct( |
||||
private ServiceTokenService $serviceTokenService |
||||
) {} |
||||
|
||||
/** |
||||
* List all service tokens |
||||
* |
||||
* GET /api/v1/service-tokens |
||||
*/ |
||||
public function index(): JsonResponse |
||||
{ |
||||
$tokens = $this->serviceTokenService->listAll(); |
||||
|
||||
return response()->json([ |
||||
'data' => array_map(fn ($token) => [ |
||||
'id' => $token->id, |
||||
'name' => $token->name, |
||||
'service' => $token->service, |
||||
'token_prefix' => $token->token_prefix, |
||||
'is_active' => $token->is_active, |
||||
'expires_at' => $token->expires_at?->toIso8601String(), |
||||
'created_by' => $token->created_by, |
||||
'created_at' => $token->created_at->toIso8601String(), |
||||
'last_used_at' => $token->last_used_at?->toIso8601String(), |
||||
'is_expired' => $token->isExpired(), |
||||
], $tokens), |
||||
]); |
||||
} |
||||
|
||||
/** |
||||
* Create a new service token |
||||
* |
||||
* POST /api/v1/service-tokens |
||||
* |
||||
* @bodyParam name string required Token name (e.g., "GeoNetAgent-Production") |
||||
* @bodyParam service string required Service name (e.g., "GeoNetAgent", "Databaselist") |
||||
* @bodyParam expires_in_days int optional Expiration in days (null = never expires) |
||||
*/ |
||||
public function store(Request $request): JsonResponse |
||||
{ |
||||
$validated = $request->validate([ |
||||
'name' => 'required|string|max:100', |
||||
'service' => 'required|string|max:50', |
||||
'expires_in_days' => 'nullable|integer|min:1', |
||||
]); |
||||
|
||||
$result = $this->serviceTokenService->create( |
||||
name: $validated['name'], |
||||
service: $validated['service'], |
||||
expiresInDays: $validated['expires_in_days'] ?? null |
||||
); |
||||
|
||||
return response()->json([ |
||||
'data' => [ |
||||
'id' => $result['model']->id, |
||||
'name' => $result['model']->name, |
||||
'service' => $result['model']->service, |
||||
'token_prefix' => $result['model']->token_prefix, |
||||
'token' => $result['token'], // Only shown once on creation |
||||
'is_active' => $result['model']->is_active, |
||||
'expires_at' => $result['model']->expires_at?->toIso8601String(), |
||||
'created_by' => $result['model']->created_by, |
||||
'created_at' => $result['model']->created_at->toIso8601String(), |
||||
], |
||||
'message' => 'Service token created successfully. Save the token securely as it will not be shown again.', |
||||
], 201); |
||||
} |
||||
|
||||
/** |
||||
* Get a specific service token |
||||
* |
||||
* GET /api/v1/service-tokens/{id} |
||||
*/ |
||||
public function show(string $id): JsonResponse |
||||
{ |
||||
$token = $this->serviceTokenService->getById($id); |
||||
|
||||
if (!$token) { |
||||
return response()->json([ |
||||
'error' => [ |
||||
'code' => 'NOT_FOUND', |
||||
'message' => 'Service token not found', |
||||
], |
||||
], 404); |
||||
} |
||||
|
||||
return response()->json([ |
||||
'data' => [ |
||||
'id' => $token->id, |
||||
'name' => $token->name, |
||||
'service' => $token->service, |
||||
'token_prefix' => $token->token_prefix, |
||||
'is_active' => $token->is_active, |
||||
'expires_at' => $token->expires_at?->toIso8601String(), |
||||
'created_by' => $token->created_by, |
||||
'created_at' => $token->created_at->toIso8601String(), |
||||
'last_used_at' => $token->last_used_at?->toIso8601String(), |
||||
'is_expired' => $token->isExpired(), |
||||
], |
||||
]); |
||||
} |
||||
|
||||
/** |
||||
* Revoke a service token (deactivate) |
||||
* |
||||
* POST /api/v1/service-tokens/{id}/revoke |
||||
*/ |
||||
public function revoke(string $id): JsonResponse |
||||
{ |
||||
$token = $this->serviceTokenService->getById($id); |
||||
|
||||
if (!$token) { |
||||
return response()->json([ |
||||
'error' => [ |
||||
'code' => 'NOT_FOUND', |
||||
'message' => 'Service token not found', |
||||
], |
||||
], 404); |
||||
} |
||||
|
||||
$this->serviceTokenService->revoke($token); |
||||
|
||||
return response()->json([ |
||||
'message' => 'Service token revoked successfully', |
||||
]); |
||||
} |
||||
|
||||
/** |
||||
* Regenerate a service token (create new, revoke old) |
||||
* |
||||
* POST /api/v1/service-tokens/{id}/regenerate |
||||
* |
||||
* @bodyParam expires_in_days int optional New expiration in days |
||||
*/ |
||||
public function regenerate(Request $request, string $id): JsonResponse |
||||
{ |
||||
$validated = $request->validate([ |
||||
'expires_in_days' => 'nullable|integer|min:1', |
||||
]); |
||||
|
||||
$token = $this->serviceTokenService->getById($id); |
||||
|
||||
if (!$token) { |
||||
return response()->json([ |
||||
'error' => [ |
||||
'code' => 'NOT_FOUND', |
||||
'message' => 'Service token not found', |
||||
], |
||||
], 404); |
||||
} |
||||
|
||||
$result = $this->serviceTokenService->regenerate( |
||||
oldToken: $token, |
||||
expiresInDays: $validated['expires_in_days'] ?? null |
||||
); |
||||
|
||||
return response()->json([ |
||||
'data' => [ |
||||
'id' => $result['model']->id, |
||||
'name' => $result['model']->name, |
||||
'service' => $result['model']->service, |
||||
'token_prefix' => $result['model']->token_prefix, |
||||
'token' => $result['token'], // Only shown once on regeneration |
||||
'is_active' => $result['model']->is_active, |
||||
'expires_at' => $result['model']->expires_at?->toIso8601String(), |
||||
'created_by' => $result['model']->created_by, |
||||
'created_at' => $result['model']->created_at->toIso8601String(), |
||||
], |
||||
'message' => 'Service token regenerated successfully. Save the new token securely as it will not be shown again.', |
||||
]); |
||||
} |
||||
|
||||
/** |
||||
* Delete a service token permanently |
||||
* |
||||
* DELETE /api/v1/service-tokens/{id} |
||||
*/ |
||||
public function destroy(string $id): JsonResponse |
||||
{ |
||||
$token = $this->serviceTokenService->getById($id); |
||||
|
||||
if (!$token) { |
||||
return response()->json([ |
||||
'error' => [ |
||||
'code' => 'NOT_FOUND', |
||||
'message' => 'Service token not found', |
||||
], |
||||
], 404); |
||||
} |
||||
|
||||
$this->serviceTokenService->delete($token); |
||||
|
||||
return response()->json([ |
||||
'message' => 'Service token deleted successfully', |
||||
]); |
||||
} |
||||
|
||||
/** |
||||
* Get tokens by service |
||||
* |
||||
* GET /api/v1/service-tokens/by-service/{service} |
||||
*/ |
||||
public function getByService(string $service): JsonResponse |
||||
{ |
||||
$tokens = $this->serviceTokenService->getByService($service); |
||||
|
||||
return response()->json([ |
||||
'data' => array_map(fn ($token) => [ |
||||
'id' => $token->id, |
||||
'name' => $token->name, |
||||
'service' => $token->service, |
||||
'token_prefix' => $token->token_prefix, |
||||
'is_active' => $token->is_active, |
||||
'expires_at' => $token->expires_at?->toIso8601String(), |
||||
'created_by' => $token->created_by, |
||||
'created_at' => $token->created_at->toIso8601String(), |
||||
'last_used_at' => $token->last_used_at?->toIso8601String(), |
||||
'is_expired' => $token->isExpired(), |
||||
], $tokens), |
||||
]); |
||||
} |
||||
|
||||
/** |
||||
* Get active tokens only |
||||
* |
||||
* GET /api/v1/service-tokens/active |
||||
*/ |
||||
public function getActive(): JsonResponse |
||||
{ |
||||
$tokens = $this->serviceTokenService->getActive(); |
||||
|
||||
return response()->json([ |
||||
'data' => array_map(fn ($token) => [ |
||||
'id' => $token->id, |
||||
'name' => $token->name, |
||||
'service' => $token->service, |
||||
'token_prefix' => $token->token_prefix, |
||||
'is_active' => $token->is_active, |
||||
'expires_at' => $token->expires_at?->toIso8601String(), |
||||
'created_by' => $token->created_by, |
||||
'created_at' => $token->created_at->toIso8601String(), |
||||
'last_used_at' => $token->last_used_at?->toIso8601String(), |
||||
], $tokens), |
||||
]); |
||||
} |
||||
|
||||
/** |
||||
* Get expired tokens |
||||
* |
||||
* GET /api/v1/service-tokens/expired |
||||
*/ |
||||
public function getExpired(): JsonResponse |
||||
{ |
||||
$tokens = $this->serviceTokenService->getExpired(); |
||||
|
||||
return response()->json([ |
||||
'data' => array_map(fn ($token) => [ |
||||
'id' => $token->id, |
||||
'name' => $token->name, |
||||
'service' => $token->service, |
||||
'token_prefix' => $token->token_prefix, |
||||
'is_active' => $token->is_active, |
||||
'expires_at' => $token->expires_at?->toIso8601String(), |
||||
'created_by' => $token->created_by, |
||||
'created_at' => $token->created_at->toIso8601String(), |
||||
'last_used_at' => $token->last_used_at?->toIso8601String(), |
||||
], $tokens), |
||||
]); |
||||
} |
||||
} |
||||
@ -0,0 +1,134 @@
@@ -0,0 +1,134 @@
|
||||
<?php |
||||
|
||||
namespace App\Models; |
||||
|
||||
use Illuminate\Database\Eloquent\Model; |
||||
use Illuminate\Database\Eloquent\SoftDeletes; |
||||
|
||||
/** |
||||
* ServiceToken - Bearer token management for microservices |
||||
* |
||||
* This model manages Bearer tokens for microservices like GeoNetAgent, |
||||
* Databaselist, SQLServerCheck, etc. Tokens are stored as SHA256 hashes |
||||
* with a prefix for UI display. |
||||
* |
||||
* Zero impact on existing GeoNetAgent agents (they use separate database). |
||||
*/ |
||||
class ServiceToken extends Model |
||||
{ |
||||
protected $fillable = [ |
||||
'name', |
||||
'service', |
||||
'token_prefix', |
||||
'token_hash', |
||||
'is_active', |
||||
'expires_at', |
||||
'created_by', |
||||
'last_used_at', |
||||
]; |
||||
|
||||
protected function casts(): array |
||||
{ |
||||
return [ |
||||
'is_active' => 'boolean', |
||||
'expires_at' => 'datetime', |
||||
'created_at' => 'datetime', |
||||
'last_used_at' => 'datetime', |
||||
]; |
||||
} |
||||
|
||||
/** |
||||
* Generate a new service token |
||||
* |
||||
* @param string $name Token name (e.g., "GeoNetAgent-Production") |
||||
* @param string $service Service name (e.g., "GeoNetAgent") |
||||
* @param string|null $createdBy User who created the token |
||||
* @param int|null $expiresInDays Expiration in days (null = never expires) |
||||
* @return array ['token' => string, 'model' => ServiceToken] |
||||
*/ |
||||
public static function generate( |
||||
string $name, |
||||
string $service, |
||||
?string $createdBy = null, |
||||
?int $expiresInDays = null |
||||
): array { |
||||
$token = bin2hex(random_bytes(32)); // 64 chars hex |
||||
$tokenPrefix = substr($token, 0, 16); |
||||
$tokenHash = hash('sha256', $token); |
||||
|
||||
$expiresAt = $expiresInDays |
||||
? now()->addDays($expiresInDays) |
||||
: null; |
||||
|
||||
$model = self::create([ |
||||
'id' => (string) \Illuminate\Support\Str::uuid(), |
||||
'name' => $name, |
||||
'service' => $service, |
||||
'token_prefix' => $tokenPrefix, |
||||
'token_hash' => $tokenHash, |
||||
'is_active' => true, |
||||
'expires_at' => $expiresAt, |
||||
'created_by' => $createdBy, |
||||
]); |
||||
|
||||
return [ |
||||
'token' => $token, |
||||
'model' => $model, |
||||
]; |
||||
} |
||||
|
||||
/** |
||||
* Verify a token against the hash |
||||
* |
||||
* @param string $token Plain token |
||||
* @return ServiceToken|null |
||||
*/ |
||||
public static function verify(string $token): ?self |
||||
{ |
||||
$tokenHash = hash('sha256', $token); |
||||
|
||||
return self::where('token_hash', $tokenHash) |
||||
->where('is_active', true) |
||||
->where(function ($query) { |
||||
$query->whereNull('expires_at') |
||||
->orWhere('expires_at', '>', now()); |
||||
}) |
||||
->first(); |
||||
} |
||||
|
||||
/** |
||||
* Update last used timestamp |
||||
*/ |
||||
public function touchLastUsed(): void |
||||
{ |
||||
$this->update(['last_used_at' => now()]); |
||||
} |
||||
|
||||
/** |
||||
* Scope for active tokens |
||||
*/ |
||||
public function scopeActive($query) |
||||
{ |
||||
return $query->where('is_active', true) |
||||
->where(function ($query) { |
||||
$query->whereNull('expires_at') |
||||
->orWhere('expires_at', '>', now()); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Scope for expired tokens |
||||
*/ |
||||
public function scopeExpired($query) |
||||
{ |
||||
return $query->where('expires_at', '<', now()); |
||||
} |
||||
|
||||
/** |
||||
* Check if token is expired |
||||
*/ |
||||
public function isExpired(): bool |
||||
{ |
||||
return $this->expires_at !== null && $this->expires_at->isPast(); |
||||
} |
||||
} |
||||
@ -0,0 +1,228 @@
@@ -0,0 +1,228 @@
|
||||
<?php |
||||
|
||||
namespace App\Services; |
||||
|
||||
use App\Models\ServiceToken; |
||||
use App\Services\AuditLogService; |
||||
use Illuminate\Support\Facades\Auth; |
||||
|
||||
/** |
||||
* ServiceTokenService - Manage microservice Bearer tokens |
||||
* |
||||
* Provides CRUD operations for service tokens with audit logging. |
||||
* Zero impact on existing GeoNetAgent agents (separate database). |
||||
*/ |
||||
class ServiceTokenService |
||||
{ |
||||
public function __construct( |
||||
private AuditLogService $auditLogService |
||||
) {} |
||||
|
||||
/** |
||||
* Create a new service token |
||||
* |
||||
* @param string $name Token name (e.g., "GeoNetAgent-Production") |
||||
* @param string $service Service name (e.g., "GeoNetAgent", "Databaselist") |
||||
* @param int|null $expiresInDays Expiration in days (null = never expires) |
||||
* @return array{token: string, model: ServiceToken} |
||||
*/ |
||||
public function create(string $name, string $service, ?int $expiresInDays = null): array |
||||
{ |
||||
$createdBy = Auth::check() ? Auth::user()->username : 'system'; |
||||
|
||||
$result = ServiceToken::generate( |
||||
name: $name, |
||||
service: $service, |
||||
createdBy: $createdBy, |
||||
expiresInDays: $expiresInDays |
||||
); |
||||
|
||||
// Audit log |
||||
$this->auditLogService->log( |
||||
action: 'service_token_created', |
||||
entityType: 'ServiceToken', |
||||
entityId: $result['model']->id, |
||||
details: [ |
||||
'name' => $name, |
||||
'service' => $service, |
||||
'token_prefix' => $result['model']->token_prefix, |
||||
'expires_at' => $result['model']->expires_at?->toIso8601String(), |
||||
], |
||||
userId: $createdBy |
||||
); |
||||
|
||||
return $result; |
||||
} |
||||
|
||||
/** |
||||
* List all service tokens (without plain tokens) |
||||
* |
||||
* @return array<ServiceToken> |
||||
*/ |
||||
public function listAll(): array |
||||
{ |
||||
return ServiceToken::orderByDesc('created_at')->get()->all(); |
||||
} |
||||
|
||||
/** |
||||
* Get a specific service token by ID |
||||
*/ |
||||
public function getById(string $id): ?ServiceToken |
||||
{ |
||||
return ServiceToken::find($id); |
||||
} |
||||
|
||||
/** |
||||
* Revoke (deactivate) a service token |
||||
*/ |
||||
public function revoke(ServiceToken $token): void |
||||
{ |
||||
$token->update(['is_active' => false]); |
||||
|
||||
$createdBy = Auth::check() ? Auth::user()->username : 'system'; |
||||
|
||||
$this->auditLogService->log( |
||||
action: 'service_token_revoked', |
||||
entityType: 'ServiceToken', |
||||
entityId: $token->id, |
||||
details: [ |
||||
'name' => $token->name, |
||||
'service' => $token->service, |
||||
'token_prefix' => $token->token_prefix, |
||||
], |
||||
userId: $createdBy |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Permanently delete a service token |
||||
*/ |
||||
public function delete(ServiceToken $token): void |
||||
{ |
||||
$tokenId = $token->id; |
||||
$tokenName = $token->name; |
||||
$tokenService = $token->service; |
||||
$tokenPrefix = $token->token_prefix; |
||||
|
||||
$token->delete(); |
||||
|
||||
$createdBy = Auth::check() ? Auth::user()->username : 'system'; |
||||
|
||||
$this->auditLogService->log( |
||||
action: 'service_token_deleted', |
||||
entityType: 'ServiceToken', |
||||
entityId: $tokenId, |
||||
details: [ |
||||
'name' => $tokenName, |
||||
'service' => $tokenService, |
||||
'token_prefix' => $tokenPrefix, |
||||
], |
||||
userId: $createdBy |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Regenerate a service token (create new, revoke old) |
||||
* |
||||
* @return array{token: string, model: ServiceToken} |
||||
*/ |
||||
public function regenerate(ServiceToken $oldToken, ?int $expiresInDays = null): array |
||||
{ |
||||
// Revoke old token |
||||
$this->revoke($oldToken); |
||||
|
||||
// Create new token with same name and service |
||||
return $this->create( |
||||
name: $oldToken->name, |
||||
service: $oldToken->service, |
||||
expiresInDays: $expiresInDays |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Verify a token (for external validation, not used in UI) |
||||
* |
||||
* @param string $plainToken Plain token to verify |
||||
* @return ServiceToken|null |
||||
*/ |
||||
public function verify(string $plainToken): ?ServiceToken |
||||
{ |
||||
$token = ServiceToken::verify($plainToken); |
||||
|
||||
if ($token) { |
||||
$token->touchLastUsed(); |
||||
} |
||||
|
||||
return $token; |
||||
} |
||||
|
||||
/** |
||||
* Get tokens by service |
||||
* |
||||
* @param string $service Service name |
||||
* @return array<ServiceToken> |
||||
*/ |
||||
public function getByService(string $service): array |
||||
{ |
||||
return ServiceToken::where('service', $service) |
||||
->orderByDesc('created_at') |
||||
->get() |
||||
->all(); |
||||
} |
||||
|
||||
/** |
||||
* Get active tokens only |
||||
* |
||||
* @return array<ServiceToken> |
||||
*/ |
||||
public function getActive(): array |
||||
{ |
||||
return ServiceToken::active() |
||||
->orderByDesc('created_at') |
||||
->get() |
||||
->all(); |
||||
} |
||||
|
||||
/** |
||||
* Get expired tokens |
||||
* |
||||
* @return array<ServiceToken> |
||||
*/ |
||||
public function getExpired(): array |
||||
{ |
||||
return ServiceToken::expired() |
||||
->orderByDesc('expires_at') |
||||
->get() |
||||
->all(); |
||||
} |
||||
|
||||
/** |
||||
* Clean up expired tokens (optional maintenance task) |
||||
* |
||||
* @param int $olderThanDays Delete tokens expired more than X days ago |
||||
* @return int Number of deleted tokens |
||||
*/ |
||||
public function cleanupExpired(int $olderThanDays = 30): int |
||||
{ |
||||
$cutoffDate = now()->subDays($olderThanDays); |
||||
|
||||
$deleted = ServiceToken::expired() |
||||
->where('expires_at', '<', $cutoffDate) |
||||
->delete(); |
||||
|
||||
if ($deleted > 0) { |
||||
$this->auditLogService->log( |
||||
action: 'service_tokens_cleanup', |
||||
entityType: 'ServiceToken', |
||||
entityId: null, |
||||
details: [ |
||||
'deleted_count' => $deleted, |
||||
'older_than_days' => $olderThanDays, |
||||
], |
||||
userId: 'system' |
||||
); |
||||
} |
||||
|
||||
return $deleted; |
||||
} |
||||
} |
||||
@ -0,0 +1,39 @@
@@ -0,0 +1,39 @@
|
||||
<?php |
||||
|
||||
use Illuminate\Database\Migrations\Migration; |
||||
use Illuminate\Database\Schema\Blueprint; |
||||
use Illuminate\Support\Facades\Schema; |
||||
|
||||
return new class extends Migration |
||||
{ |
||||
/** |
||||
* Create service_tokens table for managing microservice Bearer tokens. |
||||
* This is separate from OAuth2 clients and api_tokens for clear separation. |
||||
* Zero impact on existing GeoNetAgent agents (they use separate database). |
||||
*/ |
||||
public function up(): void |
||||
{ |
||||
Schema::create('service_tokens', function (Blueprint $table) { |
||||
$table->uuid('id')->primary(); |
||||
$table->string('name', 100); // e.g., "GeoNetAgent-Production", "Databaselist-API" |
||||
$table->string('service', 50); // e.g., "GeoNetAgent", "Databaselist", "SQLServerCheck" |
||||
$table->string('token_prefix', 16)->unique(); // First 16 chars for UI display |
||||
$table->string('token_hash', 64)->unique(); // SHA256 hash of full token |
||||
$table->boolean('is_active')->default(true); |
||||
$table->timestamp('expires_at')->nullable(); |
||||
$table->string('created_by', 100)->nullable(); // User who created the token |
||||
$table->timestamp('created_at')->useCurrent(); |
||||
$table->timestamp('last_used_at')->nullable(); |
||||
|
||||
// Indexes |
||||
$table->index('service'); |
||||
$table->index('is_active'); |
||||
$table->index('expires_at'); |
||||
}); |
||||
} |
||||
|
||||
public function down(): void |
||||
{ |
||||
Schema::dropIfExists('service_tokens'); |
||||
} |
||||
}; |
||||
@ -0,0 +1,149 @@
@@ -0,0 +1,149 @@
|
||||
# Protokol Sync Dokumentasi AI |
||||
|
||||
> **Wajib** di akhir sesi pengembangan. Repository = sumber kebenaran; percakapan bukan dokumentasi. |
||||
> Repo: **server-connection** · Konteks pemilik: `.cursor/me.md` |
||||
|
||||
## Indeks cepat (operasional) |
||||
|
||||
| Topik | File / URL | |
||||
|-------|------------| |
||||
| Profil & infrastruktur | `.cursor/me.md` | |
||||
| Konteks repo, host SSH, deploy | `.cursor/rules/00-project-context.mdc` | |
||||
| Perilaku AI agent | `.cursor/rules/03-ai-behavior.mdc` | |
||||
| Status fitur geonet-console | `.cursor/rules/02-implementation-status.mdc` | |
||||
| Ringkasan agent & Ollama/NAS | `AGENTS.md` | |
||||
| Requirement bisnis | `rules`, `rules-adv` | |
||||
| SSH & deploy cepat | `readme.txt` | |
||||
| Host & IP | `server/host.txt` | |
||||
| geonet-console | https://console.gisportal.id | |
||||
| databaselist | https://databaselist.gisportal.id | |
||||
| sqlservercheck | https://sqlservercheck.gisportal.id | |
||||
|
||||
### Aplikasi & stack produksi |
||||
|
||||
| Folder | URL | Stack server | |
||||
|--------|-----|--------------| |
||||
| `geonet-console/` | https://console.gisportal.id | `/opt/stacks/geonet-console/` | |
||||
| `databaselist/` | https://databaselist.gisportal.id | `/opt/stacks/databaselist/` | |
||||
| `sqlservercheck/` | https://sqlservercheck.gisportal.id | `/opt/stacks/sqlservercheck/` | |
||||
|
||||
### URL produksi (domain `gisportal.id`) |
||||
|
||||
#### geonet-console |
||||
|
||||
| Fungsi | URL | |
||||
|--------|-----| |
||||
| Login / dashboard | https://console.gisportal.id | |
||||
|
||||
#### databaselist |
||||
|
||||
| Fungsi | URL | |
||||
|--------|-----| |
||||
| Daftar database | https://databaselist.gisportal.id/ | |
||||
| LDAP Users | https://databaselist.gisportal.id/ldap-users | |
||||
| LDAP Groups | https://databaselist.gisportal.id/ldap-groups | |
||||
| DB Connections | https://databaselist.gisportal.id/database-connections | |
||||
| Mail SMTP & template | https://databaselist.gisportal.id/mail-settings | |
||||
| Audit Logs | https://databaselist.gisportal.id/audit-logs | |
||||
| API Docs | https://databaselist.gisportal.id/api-docs | |
||||
| Profile (user) | https://databaselist.gisportal.id/profile | |
||||
| REST API | https://databaselist.gisportal.id/api/v1 | |
||||
| API Clients (OAuth2 + API Tokens + Service Tokens) | https://console.gisportal.id/api-clients | |
||||
|
||||
#### sqlservercheck |
||||
|
||||
| Fungsi | URL | |
||||
|--------|-----| |
||||
| Tool cek SQL Server | https://sqlservercheck.gisportal.id/ts | |
||||
| | https://sqlservercheck.gisportal.id/nts | |
||||
|
||||
### Deploy dari laptop |
||||
|
||||
```powershell |
||||
.\scripts\deploy-geonet-console.ps1 |
||||
.\scripts\deploy-databaselist.ps1 |
||||
.\scripts\deploy-sqlservercheck.ps1 |
||||
.\scripts\deploy-databaselist.ps1 -NoBuild # restart tanpa rebuild |
||||
``` |
||||
|
||||
SSH reverse proxy: `ssh -m hmac-sha1 root@10.100.1.24 -p 22` · Edit `.env` hanya di server. |
||||
|
||||
### File konteks (update saat sync) |
||||
|
||||
| File | Isi | |
||||
|------|-----| |
||||
| `.cursor/me.md` | Profil pemilik & infrastruktur | |
||||
| `.cursor/rules/*.mdc` | Rules Cursor | |
||||
| `AGENTS.md` | Ringkasan agent, roadmap Ollama/NAS | |
||||
| `perintah.md` | Protokol sync (file ini) | |
||||
| `readme.txt` | SSH, deploy, URL | |
||||
| `rules` / `rules-adv` | Requirement bisnis manual | |
||||
| `server/ollama-nas-project-search-requirements.md` | Requirement Ollama + NAS | |
||||
|
||||
| Rules | Isi | |
||||
|-------|-----| |
||||
| `00-project-context.mdc` | Repo, host, deploy | |
||||
| `01-enterprise-principles.mdc` | Prinsip enterprise & UX | |
||||
| `02-implementation-status.mdc` | Status geonet-console | |
||||
| `03-ai-behavior.mdc` | Perilaku agent | |
||||
| `04-geonet-uiux.mdc` | Standar UI/UX | |
||||
| `05-laravel-backend.mdc` | Backend Laravel | |
||||
| `06-infrastructure.mdc` | SSH, Mikrotik, deploy | |
||||
|
||||
--- |
||||
|
||||
## PERTANYAAN SYNC (checklist AI) |
||||
|
||||
``` |
||||
1. Sinkronisasi Dokumentasi |
||||
|
||||
- Pastikan seluruh keputusan, perubahan arsitektur, perubahan desain, perubahan implementasi, dan hasil diskusi pada sesi ini sudah didokumentasikan. |
||||
- Minimal periksa dan update: |
||||
- .cursor/rules/ |
||||
- .cursor/*.md |
||||
- perintah.md (baris "Sync terakhir" di bawah) |
||||
- Architecture Decision Record (ADR) bila terdapat perubahan desain. |
||||
|
||||
2. Sinkronisasi AI Context |
||||
|
||||
- Project ini menggunakan folder .cursor sebagai single source of truth untuk seluruh AI Agent. |
||||
- Karena saya sering berpindah perangkat (PC, Laptop, Workstation), AI Agent tidak boleh bergantung pada riwayat percakapan. |
||||
- Seluruh konteks penting harus selalu tersimpan di repository. |
||||
|
||||
3. Konsistensi Dokumentasi |
||||
|
||||
- Pastikan tidak ada informasi yang hanya ada di percakapan tetapi belum terdokumentasi. |
||||
- Seluruh keputusan teknis wajib dipindahkan ke dokumentasi project. |
||||
- Percakapan bukan merupakan sumber dokumentasi. |
||||
- Repository adalah sumber dokumentasi utama. |
||||
- URL & indeks di perintah.md dan .cursor/me.md harus selaras. |
||||
|
||||
4. Rules Update |
||||
|
||||
- Apabila hasil diskusi mengubah cara pengembangan project, maka AI wajib memperbarui file rules yang relevan. |
||||
- atau membuat rules baru apabila diperlukan. |
||||
|
||||
5. Dokumentasi Sebelum Coding |
||||
|
||||
- Jika terdapat perubahan desain, arsitektur, atau workflow, dokumentasi harus diperbarui terlebih dahulu sebelum implementasi kode dilanjutkan. |
||||
- Documentation First, Code Second. |
||||
|
||||
6. Final Checklist |
||||
|
||||
- Sebelum menjawab "selesai", AI wajib melakukan checklist berikut: |
||||
- Semua keputusan pada percakapan ini sudah terdokumentasi. |
||||
- Semua file .cursor yang relevan telah diperbarui. |
||||
- Tidak ada konteks penting yang hanya tersimpan di percakapan. |
||||
- Dokumentasi konsisten dengan implementasi. |
||||
- Rules AI tetap menjadi acuan utama pengembangan. |
||||
- Project tetap dapat dipahami AI Agent meskipun riwayat chat tidak tersedia. |
||||
|
||||
- Jika salah satu poin belum terpenuhi, jangan nyatakan pekerjaan selesai. |
||||
``` |
||||
|
||||
### Sync terakhir |
||||
|
||||
| Tanggal | Ringkasan perubahan | |
||||
|---------|---------------------| |
||||
| 2026-06-11 | Lengkapi `me.md` & `perintah.md` — URL produksi, indeks file, selaraskan server-connection / GeoNetAgent | |
||||
| 2026-06-23 | Implementasi fitur Service Tokens di geonet-console (migration, model, service, API endpoints, UI, audit log) | |
||||
Loading…
Reference in new issue