From ece7b314a405465f1545d077de208e98c2485b7b Mon Sep 17 00:00:00 2001 From: rbsetiawan Date: Tue, 23 Jun 2026 15:39:06 +0700 Subject: [PATCH] feat: implement Service Tokens for microservice Bearer token management - 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). --- server-connection/.cursor/context-index.md | 467 ++++++++++++++++++ .../Api/V1/ServiceTokenController.php | 289 +++++++++++ .../Http/Controllers/ApiClientController.php | 84 ++++ .../app/Models/ServiceToken.php | 134 +++++ .../app/Services/ServiceTokenService.php | 228 +++++++++ ..._23_130000_create_service_tokens_table.php | 39 ++ .../views/api-clients/index.blade.php | 98 +++- .../geonet-console/routes/api.php | 21 + .../geonet-console/routes/web.php | 6 + server-connection/perintah.md | 149 ++++++ 10 files changed, 1514 insertions(+), 1 deletion(-) create mode 100644 server-connection/.cursor/context-index.md create mode 100644 server-connection/geonet-console/app/Http/Controllers/Api/V1/ServiceTokenController.php create mode 100644 server-connection/geonet-console/app/Models/ServiceToken.php create mode 100644 server-connection/geonet-console/app/Services/ServiceTokenService.php create mode 100644 server-connection/geonet-console/database/migrations/2026_06_23_130000_create_service_tokens_table.php create mode 100644 server-connection/perintah.md diff --git a/server-connection/.cursor/context-index.md b/server-connection/.cursor/context-index.md new file mode 100644 index 0000000..4fcd0c9 --- /dev/null +++ b/server-connection/.cursor/context-index.md @@ -0,0 +1,467 @@ +# Context Index (AI Entry Point) + +> **Versi Dokumen:** 1.0.0 +> **Terakhir Diperbarui:** 2026-06-23 +> **Status:** Aktif +> **Pemilik:** Tim Pengembang GeoNet / GIS Portal + +--- + +# Tujuan + +Dokumen ini merupakan **titik masuk utama (Single Entry Point)** bagi seluruh AI Agent dan Developer untuk repository **server-connection**. + +Setiap AI **WAJIB** membaca dokumen ini terlebih dahulu sebelum melakukan analisis, implementasi, perubahan kode, refactoring, penghapusan, deployment, maupun pembuatan dokumentasi. + +Dokumen ini **bukan** tempat menyimpan seluruh informasi proyek, melainkan **peta navigasi** menuju seluruh dokumentasi yang tersedia. + +--- + +# Filosofi Proyek + +Proyek ini menerapkan prinsip **AI Native Development**. + +Artinya: + +* Dokumentasi adalah bagian dari source code. +* Setiap perubahan wajib terdokumentasi. +* Repository merupakan sumber kebenaran utama (Single Source of Truth). +* AI tidak boleh mengandalkan riwayat percakapan. +* Seluruh pengetahuan proyek harus tersimpan dalam repository. +* Developer sering berpindah perangkat (PC, Laptop, Workstation) — AI Agent harus tetap bisa melanjutkan tanpa riwayat chat. + +--- + +# Aturan Utama AI + +Sebelum melakukan pekerjaan apa pun, AI wajib: + +1. Membaca file ini (`context-index.md`). +2. Membaca dokumentasi yang berkaitan dengan tugas. +3. Memahami arsitektur yang ada. +4. Memastikan tidak ada konflik dengan keputusan arsitektur sebelumnya. +5. Memperbarui dokumentasi setelah implementasi selesai. +6. Mengikuti protokol sync di `perintah.md` sebelum menyatakan pekerjaan selesai. + +Implementasi tanpa dokumentasi dianggap **belum selesai**. + +--- + +# Urutan Membaca Dokumentasi + +AI wajib membaca dokumen sesuai urutan berikut: + +1. `context-index.md` (file ini) +2. `me.md` — Profil & infrastruktur +3. `perintah.md` — Protokol sync dokumentasi +4. Dokumen spesifik modul (lihat tabel Struktur Dokumentasi di bawah) +5. Rules yang relevan di `.cursor/rules/` + +Jangan membaca seluruh repository apabila tidak diperlukan. + +--- + +# Struktur Dokumentasi + +## 1. Profil & Infrastruktur + +**Lokasi:** `.cursor/me.md` + +**Berisi:** + +* Profil user (IT end-to-end: network, server, storage, GIS, licensing) +* Stack & aset yang sudah dimiliki (Laravel 12, FastAPI, Python, SQL Server, PostgreSQL) +* Monitoring & Identity (Zabbix, Windows Server 2012 R2 AD) +* Compute & Virtualization (Proxmox, VM Database, VM Docker, VM Ollama) +* Network & Storage (MikroTik CCR2004, QNAP TS-932X) +* Endpoint (laptop user, GPO deploy) +* Lingkungan development (OS dev, Docker lokal, deploy, SSH) +* Ruang lingkup tanggung jawab +* Preferensi kerja (Bahasa Indonesia, jangan duplikasi Zabbix, PostgreSQL untuk baru) +* Aplikasi di repo `server-connection` (geonet-console, databaselist, sqlservercheck) +* URL operasional +* File konteks terkait +* Rules Cursor (prioritas) +* Status proyek +* Deploy cepat + +**Ringkasan Profil User:** + +User adalah **IT Administrator** yang mengelola infrastruktur end-to-end: + +- **Network & Security:** MikroTik CCR2004 (gateway, routing, firewall) +- **Compute & Virtualization:** Proxmox (VM host), PostgreSQL/SQL Server (database), Docker (containers) +- **Identity:** Windows Server 2012 R2 Active Directory (domain user management, GPO deploy) +- **Storage:** QNAP TS-932X (NAS SMB) +- **Monitoring:** Zabbix (alerting real-time) +- **Endpoint:** Banyak laptop user (Windows, domain-joined) +- **GIS & Licensing:** Software licensing tracking + +**Preferensi Kerja:** + +- Bahasa komunikasi: **Bahasa Indonesia** +- Jangan duplikasi fungsi Zabbix — Zabbix tetap pusat alerting real-time +- PostgreSQL untuk aplikasi/CMDB baru; SQL Server untuk legacy/GIS +- Minimalkan scope perubahan; ikuti konvensi existing code +- Jangan commit kecuali diminta eksplisit +- Documentation first — sinkronkan `.cursor/` setelah sesi besar + +**Baca saat:** Selalu — konteks lingkungan & preferensi user. + +--- + +## 2. Rules Cursor + +**Lokasi:** `.cursor/rules/` + +**Berisi:** + +| File | Isi | Scope | +|------|-----|-------| +| `00-project-context.mdc` | Struktur repo, host SSH, deploy | Selalu | +| `01-enterprise-principles.mdc` | Prinsip enterprise & UX wajib | Selalu | +| `02-implementation-status.mdc` | Status fitur geonet-console (audit) | Selalu | +| `03-ai-behavior.mdc` | Perilaku agent, larangan, bahasa | Selalu | +| `04-geonet-uiux.mdc` | Standar UI/UX | `geonet-console/`, `databaselist/` | +| `05-laravel-backend.mdc` | Service layer, API, audit | `*.php` di apps | +| `06-infrastructure.mdc` | SSH, Mikrotik, skrip deploy | `scripts/`, `server/` | + +**Baca saat:** Selalu — rules aktif untuk AI Agent. + +--- + +## 3. Perintah (Sync Protocol) + +**Lokasi:** `perintah.md` (root repo) + +**Berisi:** + +* Indeks cepat operasional (profil, konteks repo, status fitur, ringkasan agent) +* Aplikasi & stack produksi +* URL produksi (domain `gisportal.id`) +* Deploy dari laptop (PowerShell scripts) +* File konteks (update saat sync) +* Rules (daftar lengkap) +* Pertanyaan sync (checklist AI wajib akhir sesi) +* Sync terakhir (changelog singkat) + +**Baca saat:** Akhir sesi / sebelum nyatakan done (wajib). + +--- + +## 4. AGENTS.md + +**Lokasi:** `AGENTS.md` + +**Berisi:** + +* Rules aktif (daftar lengkap dengan scope) +* Aplikasi utama (geonet-console, databaselist, sqlservercheck) +* Dokumentasi sumber (referensi lengkap) +* Ollama + NAS Project Search (ringkasan agent) + * Ringkasan keputusan + * Arsitektur + * API (target geonet-console) + * Timeline rekomendasi + * Checklist go-live +* Verifikasi agent aktif + +**Baca saat:** Saat kerja Ollama/NAS search atau verifikasi rules. + +--- + +## 5. Server Documentation + +**Lokasi:** `server/` + +**Berisi:** + +* `host.md` — Daftar host & IP +* Audit server (Mikrotik, database, webserver, AD-LDAP, Zabbix, QNAP, Ollama) +* Konfigurasi Mikrotik (dual-wan, failover, hybrid, split-DNS) +* `migrate-geonet-console.md` — Rencana migrasi databaselist → geonet-console +* `ollama-nas-project-search-requirements.md` — Requirement lengkap Ollama + NAS search +* Konfigurasi nginx, SQL scripts + +**Baca saat:** Saat kerja infrastruktur, Mikrotik, atau migrasi. + +--- + +## 6. README & Quick Reference + +**Lokasi:** `readme.txt` + +**Berisi:** + +* SSH commands (Mikrotik, reverse proxy, main database, AD-LDAP) +* Remote Docker Development (model kerja) +* SSH key setup +* Deploy dari laptop (PowerShell scripts) +* URL setelah deploy +* Edit .env di server + +**Baca saat:** Saat deploy atau SSH ke server. + +--- + +# Informasi Aplikasi + +## Aplikasi di Repo + +| Folder | URL Produksi | Stack Server | Status | +|--------|--------------|--------------|--------| +| `geonet-console/` | https://console.gisportal.id | `/opt/stacks/geonet-console/` | ✅ Aktif | +| `databaselist/` | https://databaselist.gisportal.id | `/opt/stacks/databaselist/` | ⚠️ Redirect ke console | +| `sqlservercheck/` | https://sqlservercheck.gisportal.id | `/opt/stacks/sqlservercheck/` | ✅ Aktif | + +## URL Produksi + +### geonet-console + +| Fungsi | URL | +|--------|-----| +| Login / dashboard | https://console.gisportal.id | +| API Clients (OAuth2 + API Tokens + Service Tokens) | https://console.gisportal.id/api-clients | + +### databaselist (redirect ke console) + +| 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 | + +### sqlservercheck + +| Fungsi | URL | +|--------|-----| +| Tool cek SQL Server | https://sqlservercheck.gisportal.id/ts | +| | https://sqlservercheck.gisportal.id/nts | + +--- + +# Service Tokens + +Service Tokens adalah fitur di `geonet-console` untuk mengelola Bearer token microservice secara terpusat. Berbeda dari OAuth2 (flow kompleks) dan API Token (untuk user/script), Service Tokens khusus untuk autentikasi service-to-service sederhana. + +## Arsitektur + +- **Tabel:** `service_tokens` (di database `geonet_console`, bukan `geonetagent_dev`) +- **Model:** `app/Models/ServiceToken.php` +- **Service:** `app/Services/ServiceTokenService.php` +- **Controller (web):** `app/Http/Controllers/ApiClientController.php` +- **Controller (API):** `app/Http/Controllers/Api/V1/ServiceTokenController.php` +- **Migration:** `database/migrations/2026_06_23_130000_create_service_tokens_table.php` +- **UI:** `/api-clients` — tab ketiga di bawah API Tokens dan OAuth2 Clients + +## Schema Tabel + +| Kolom | Tipe | Deskripsi | +|-------|------|-----------| +| `id` | UUID PK | Identifier unik | +| `name` | varchar(100) | Nama token (e.g., "GeoNetAgent-Production") | +| `service` | varchar(50) | Nama service (e.g., "GeoNetAgent", "Databaselist") | +| `token_prefix` | varchar(16) unique | 16 karakter awal untuk identifikasi di UI | +| `token_hash` | varchar(64) unique | SHA256 hash token lengkap | +| `is_active` | boolean | Status aktif/revoked | +| `expires_at` | timestamptz nullable | Waktu kedaluwarsa (null = tidak expire) | +| `created_by` | varchar(100) | Username yang membuat token | +| `created_at` | timestamptz | Waktu pembuatan | +| `last_used_at` | timestamptz nullable | Waktu terakhir digunakan | + +## API Endpoints (REST) + +| Method | Endpoint | Fungsi | +|--------|----------|--------| +| GET | `/api/v1/service-tokens` | List semua token | +| POST | `/api/v1/service-tokens` | Buat token baru | +| GET | `/api/v1/service-tokens/{id}` | Detail token | +| POST | `/api/v1/service-tokens/{id}/revoke` | Revoke (nonaktifkan) | +| POST | `/api/v1/service-tokens/{id}/regenerate` | Regenerate (buat baru, revoke lama) | +| DELETE | `/api/v1/service-tokens/{id}` | Hapus permanen | + +## Web Routes + +| Method | Route | Fungsi | +|--------|-------|--------| +| POST | `/api-clients/service-tokens` | Buat token (redirect ke `/api-clients`) | +| POST | `/api-clients/service-tokens/{id}/revoke` | Revoke token | +| POST | `/api-clients/service-tokens/{id}/regenerate` | Regenerate token | +| DELETE | `/api-clients/service-tokens/{id}` | Hapus token | + +## 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 + +--- + +# Informasi Infrastruktur + +## Host SSH + +| Host | IP | Port | User | Fungsi | +|------|-----|------|------|--------| +| Mikrotik | 10.100.1.1 / 192.168.0.1 | 255 | admin | Gateway, routing, firewall | +| Reverse proxy | 10.100.1.24 | 22 | root | Nginx, Docker, stack `/opt/stacks/*` | +| Main database | 10.100.1.25 | 22 | root | PostgreSQL, SQL Server | +| AD-LDAP | 10.100.1.40 | 22 | Administrator | Windows Server 2012 R2 AD | +| Main webserver | 10.100.1.41 | 22 | Administrator | Web server | +| Zabbix | 10.100.1.42 | 22 | root | Monitoring Server | +| Ollama (aiopr) | 10.100.1.26 | 22 | geonet/root | AI inference (POC) | +| QNAP | 10.100.1.10 | 22 | admin | NAS SMB | + +## Model Kerja + +Edit kode di laptop → deploy via SSH → build & run di server. **Kredensial `.env` hanya di server**, jangan sync dari laptop. + +## Deploy Commands + +```powershell +.\scripts\deploy-geonet-console.ps1 +.\scripts\deploy-databaselist.ps1 +.\scripts\deploy-sqlservercheck.ps1 +.\scripts\deploy-databaselist.ps1 -NoBuild # restart tanpa rebuild +``` + +--- + +# Status Fitur (geonet-console) + +## Sudah Ada + +- Laravel 11, PostgreSQL (app + audit), LDAP auth & manajemen user/grup +- Multi database dinamis (SQL Server, PostgreSQL, MySQL/MariaDB) +- Docker + deploy script + Nginx reverse proxy +- Audit log web/API + beacon page-view/duration +- API v1 + Passport OAuth2 + API token + Swagger UI +- **Service Tokens** — Bearer token management untuk microservice (GeoNetAgent, Databaselist, dll.) di `/api-clients` +- Service layer kuat (15+ services, termasuk `ServiceTokenService`) +- UI: Turbo SPA, dark/light/system, PWA install, tabel mobile action sheet + +## Partial + +- API Management (belum gateway/rate-plan penuh) +- Security (belum MFA/SSO/RBAC granular) +- Audit immutable (belum DB trigger/hash chain) +- PWA (belum push notification & background sync) +- Production hardening (Tailwind CDN di prod, default password di compose example) +- Clean architecture (Application layer ada, Domain belum) + +## Belum + +- CMS, MFA, SSO user (SAML/OIDC), Maker Checker, Four Eyes Principle +- Repository pattern, DTO formal, Event driven, Queue processing +- Approval queue UI + +--- + +# Ollama + NAS Project Search + +**Status:** Direncanakan (belum diimplementasi di geonet-console). + +**Dokumen lengkap:** `server/ollama-nas-project-search-requirements.md` + +### Ringkasan Keputusan + +| Pertanyaan | Jawaban | +|------------|---------| +| Ollama bisa langsung scan `\\10.100.1.10\project`? | **Tidak** — perlu **indexer + PostgreSQL** | +| Multi-folder (project, Marketing, dll.)? | **Ya** — via env **indexer**, bukan env Ollama | +| Host Ollama POC & CPU | **VM `10.100.1.26` (aiopr)** — dedicated Ubuntu 22.04 | +| Host Ollama GPU (target) | Migrasi **Windows 11 + GPU** (~6 bulan) | +| Model Ubuntu → Windows portable? | **Ya** — copy `~/.ollama/models/` atau `ollama pull` ulang | +| Indeks RAG pindah ke PC Windows? | **Tidak perlu** — tetap di PostgreSQL `10.100.1.25` | +| License code / tanggal expired dari PDF? | **Ya** — `license_registry` + parser terstruktur | + +### API Target (geonet-console) + +| Endpoint | Method | Fungsi | +|----------|--------|--------| +| `/api/v1/project-search/ask` | POST | Query natural language + optional `sources[]` | +| `/api/v1/project-search/index/status` | GET | Status indeks per source | +| `/api/v1/project-search/sources` | GET | Daftar source dari env | +| `/api/v1/license-search/ask` | POST | Query license natural language | +| `/api/v1/license-search/code/{code}` | GET | Lookup exact license code | +| `/api/v1/license-search/expiring` | GET | License mendekati expired | +| `/api/v1/license-search/reindex` | POST | Re-parse file (admin, audit) | + +--- + +# Checklist Sebelum Implementasi + +AI wajib memastikan: + +☐ Sudah membaca `context-index.md` + +☐ Sudah membaca `me.md` — profil & infrastruktur + +☐ Sudah membaca `perintah.md` — protokol sync + +☐ Sudah membaca rules yang relevan di `.cursor/rules/` + +☐ Sudah memahami status implementasi (`02-implementation-status.mdc`) + +☐ Sudah memahami arsitektur yang ada + +--- + +# Checklist Setelah Implementasi + +AI wajib memperbarui: + +☐ `perintah.md` — sync terakhir (wajib akhir sesi) + +☐ `me.md` — jika infrastruktur berubah + +☐ Rules yang relevan — jika workflow berubah + +☐ `AGENTS.md` — jika ada perubahan roadmap Ollama/NAS + +☐ `context-index.md` — jika ada dokumen baru + +--- + +# Prinsip Pengembangan + +Setiap perubahan harus memenuhi prinsip berikut: + +* Mudah dipahami developer baru. +* Mudah dipahami AI Agent lain (tanpa riwayat chat). +* Mudah diaudit. +* Mudah di-maintain. +* Memiliki rollback plan. +* Memiliki dokumentasi lengkap. +* Tidak menghasilkan technical debt yang tidak terdokumentasi. +* Repository adalah sumber kebenaran utama. + +--- + +# Penutup + +Repository ini dirancang agar: + +* Developer baru dapat memahami proyek dengan cepat. +* AI Agent mana pun dapat melanjutkan pekerjaan tanpa kehilangan konteks. +* Seluruh pengetahuan proyek tersimpan di dalam repository. +* Dokumentasi selalu selaras dengan implementasi. + +Repository adalah sumber kebenaran utama. + +Jika terjadi perbedaan antara implementasi dan dokumentasi, AI wajib memperbarui dokumentasi hingga keduanya konsisten. + +--- + +# Bahasa & Gaya + +* Komunikasi dengan user: **Bahasa Indonesia** +* Kode & komentar: **English** (konvensi industri) +* Dokumentasi `.cursor/`: **Bahasa Indonesia** (kecuali istilah teknis) diff --git a/server-connection/geonet-console/app/Http/Controllers/Api/V1/ServiceTokenController.php b/server-connection/geonet-console/app/Http/Controllers/Api/V1/ServiceTokenController.php new file mode 100644 index 0000000..05378a7 --- /dev/null +++ b/server-connection/geonet-console/app/Http/Controllers/Api/V1/ServiceTokenController.php @@ -0,0 +1,289 @@ +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), + ]); + } +} diff --git a/server-connection/geonet-console/app/Http/Controllers/ApiClientController.php b/server-connection/geonet-console/app/Http/Controllers/ApiClientController.php index 306e5da..9ff99b6 100644 --- a/server-connection/geonet-console/app/Http/Controllers/ApiClientController.php +++ b/server-connection/geonet-console/app/Http/Controllers/ApiClientController.php @@ -3,7 +3,9 @@ namespace App\Http\Controllers; use App\Models\ApiToken; +use App\Models\ServiceToken; use App\Services\ApiTokenService; +use App\Services\ServiceTokenService; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\View\View; @@ -14,6 +16,7 @@ class ApiClientController extends Controller { public function __construct( private readonly ApiTokenService $apiTokens, + private readonly ServiceTokenService $serviceTokens, private readonly ClientRepository $clientRepository ) { } @@ -23,9 +26,11 @@ class ApiClientController extends Controller return view('api-clients.index', [ 'apiTokens' => $this->apiTokens->listAll(), 'oauthClients' => Client::orderByDesc('created_at')->get(), + 'serviceTokens' => $this->serviceTokens->listAll(), 'scopes' => config('api.scopes'), 'newToken' => session('new_api_token'), 'newClient' => session('new_oauth_client'), + 'newServiceToken' => session('new_service_token'), ]); } @@ -94,4 +99,83 @@ class ApiClientController extends Controller return redirect()->route('api-clients.index')->with('success', 'OAuth2 client revoked.'); } + + // Service Tokens (microservice Bearer token management) + + public function storeServiceToken(Request $request): RedirectResponse + { + $data = $request->validate([ + 'name' => ['required', 'string', 'max:100'], + 'service' => ['required', 'string', 'max:50'], + 'expires_in_days' => ['nullable', 'integer', 'min:1', 'max:3650'], + ]); + + $result = $this->serviceTokens->create( + name: $data['name'], + service: $data['service'], + expiresInDays: $data['expires_in_days'] ?? null + ); + + return redirect() + ->route('api-clients.index') + ->with('success', 'Service token created. Copy it now — it will not be shown again.') + ->with('new_service_token', [ + 'name' => $result['model']->name, + 'service' => $result['model']->service, + 'token' => $result['token'], + ]); + } + + public function revokeServiceToken(Request $request, string $id): RedirectResponse + { + $token = $this->serviceTokens->getById($id); + + if (!$token) { + return redirect()->route('api-clients.index')->with('error', 'Service token not found.'); + } + + $this->serviceTokens->revoke($token); + + return redirect()->route('api-clients.index')->with('success', 'Service token revoked.'); + } + + public function regenerateServiceToken(Request $request, string $id): RedirectResponse + { + $data = $request->validate([ + 'expires_in_days' => ['nullable', 'integer', 'min:1', 'max:3650'], + ]); + + $token = $this->serviceTokens->getById($id); + + if (!$token) { + return redirect()->route('api-clients.index')->with('error', 'Service token not found.'); + } + + $result = $this->serviceTokens->regenerate( + oldToken: $token, + expiresInDays: $data['expires_in_days'] ?? null + ); + + return redirect() + ->route('api-clients.index') + ->with('success', 'Service token regenerated. Copy the new token now.') + ->with('new_service_token', [ + 'name' => $result['model']->name, + 'service' => $result['model']->service, + 'token' => $result['token'], + ]); + } + + public function destroyServiceToken(Request $request, string $id): RedirectResponse + { + $token = $this->serviceTokens->getById($id); + + if (!$token) { + return redirect()->route('api-clients.index')->with('error', 'Service token not found.'); + } + + $this->serviceTokens->delete($token); + + return redirect()->route('api-clients.index')->with('success', 'Service token deleted.'); + } } diff --git a/server-connection/geonet-console/app/Models/ServiceToken.php b/server-connection/geonet-console/app/Models/ServiceToken.php new file mode 100644 index 0000000..182ecab --- /dev/null +++ b/server-connection/geonet-console/app/Models/ServiceToken.php @@ -0,0 +1,134 @@ + '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(); + } +} diff --git a/server-connection/geonet-console/app/Services/ServiceTokenService.php b/server-connection/geonet-console/app/Services/ServiceTokenService.php new file mode 100644 index 0000000..77c846e --- /dev/null +++ b/server-connection/geonet-console/app/Services/ServiceTokenService.php @@ -0,0 +1,228 @@ +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 + */ + 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 + */ + public function getByService(string $service): array + { + return ServiceToken::where('service', $service) + ->orderByDesc('created_at') + ->get() + ->all(); + } + + /** + * Get active tokens only + * + * @return array + */ + public function getActive(): array + { + return ServiceToken::active() + ->orderByDesc('created_at') + ->get() + ->all(); + } + + /** + * Get expired tokens + * + * @return array + */ + 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; + } +} diff --git a/server-connection/geonet-console/database/migrations/2026_06_23_130000_create_service_tokens_table.php b/server-connection/geonet-console/database/migrations/2026_06_23_130000_create_service_tokens_table.php new file mode 100644 index 0000000..de908c1 --- /dev/null +++ b/server-connection/geonet-console/database/migrations/2026_06_23_130000_create_service_tokens_table.php @@ -0,0 +1,39 @@ +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'); + } +}; diff --git a/server-connection/geonet-console/resources/views/api-clients/index.blade.php b/server-connection/geonet-console/resources/views/api-clients/index.blade.php index 755bd79..5d7fa3b 100644 --- a/server-connection/geonet-console/resources/views/api-clients/index.blade.php +++ b/server-connection/geonet-console/resources/views/api-clients/index.blade.php @@ -31,7 +31,15 @@ @endif -
+ @if ($newServiceToken) +
+

New Service Token: {{ $newServiceToken['name'] }} ({{ $newServiceToken['service'] }})

+

Token: {{ $newServiceToken['token'] }}

+

Salin sekarang. Token tidak ditampilkan lagi.

+
+ @endif + +

Create API Token

@@ -79,6 +87,31 @@
+ +
+

Create Service Token

+
+ @csrf +
+ + +
+
+ + +
+
+ + +
+ +
+
@@ -122,6 +155,65 @@
+
+
+

Service Tokens ({{ $serviceTokens->where('is_active', true)->count() }})

+
+
+ + + + + + + + + + @include('partials.list-table-row-hint', ['tag' => 'th']) + + + + @forelse ($serviceTokens as $token) + + + + + + + + @include('partials.list-table-row-hint', ['tag' => 'td']) + + @empty + + @endforelse + +
NameServicePrefixStatusLast UsedAction
{{ $token->name }}{{ $token->service }}{{ $token->token_prefix }}... + + {{ !$token->is_active ? 'Revoked' : ($token->isExpired() ? 'Expired' : 'Active') }} + + {{ $token->last_used_at?->format('Y-m-d H:i') ?? '—' }} + @if ($token->is_active) +
+ @csrf + +
+
+ @csrf + +
+ @endif +
+ @csrf + @method('DELETE') + +
+
No service tokens.
+
+
+

OAuth2 Clients ({{ $oauthClients->where('revoked', false)->count() }})

@@ -178,6 +270,10 @@
OAuth2 Client
Client Credentials untuk integrasi enterprise. Request token ke /oauth/token.
+
+
Service Token
+
Bearer token untuk microservice (GeoNetAgent, Databaselist). Tanpa OAuth flow, simple Bearer auth.
+
Keamanan
Secret/token hanya ditampilkan sekali saat dibuat. Revoke segera jika bocor.
diff --git a/server-connection/geonet-console/routes/api.php b/server-connection/geonet-console/routes/api.php index d387e8a..4d68b50 100644 --- a/server-connection/geonet-console/routes/api.php +++ b/server-connection/geonet-console/routes/api.php @@ -6,6 +6,7 @@ use App\Http\Controllers\Api\V1\LdapGroupController; use App\Http\Controllers\Api\V1\LdapUserController; use App\Http\Controllers\Api\V1\MeController; use App\Http\Controllers\Api\V1\ProjectSearchController; +use App\Http\Controllers\Api\V1\ServiceTokenController; use App\Http\Controllers\Api\V1\TokenGuideController; use Illuminate\Support\Facades\Route; @@ -67,5 +68,25 @@ Route::prefix('v1')->group(function () { Route::post('/project-search/ask', [ProjectSearchController::class, 'ask']) ->middleware(['api.scope:project-search:read', 'throttle:20,1']) ->name('api.v1.project-search.ask'); + + // Service Tokens (microservice Bearer token management) + Route::get('/service-tokens', [ServiceTokenController::class, 'index']) + ->name('api.v1.service-tokens.index'); + Route::post('/service-tokens', [ServiceTokenController::class, 'store']) + ->name('api.v1.service-tokens.store'); + Route::get('/service-tokens/{id}', [ServiceTokenController::class, 'show']) + ->name('api.v1.service-tokens.show'); + Route::post('/service-tokens/{id}/revoke', [ServiceTokenController::class, 'revoke']) + ->name('api.v1.service-tokens.revoke'); + Route::post('/service-tokens/{id}/regenerate', [ServiceTokenController::class, 'regenerate']) + ->name('api.v1.service-tokens.regenerate'); + Route::delete('/service-tokens/{id}', [ServiceTokenController::class, 'destroy']) + ->name('api.v1.service-tokens.destroy'); + Route::get('/service-tokens/by-service/{service}', [ServiceTokenController::class, 'getByService']) + ->name('api.v1.service-tokens.by-service'); + Route::get('/service-tokens/active', [ServiceTokenController::class, 'getActive']) + ->name('api.v1.service-tokens.active'); + Route::get('/service-tokens/expired', [ServiceTokenController::class, 'getExpired']) + ->name('api.v1.service-tokens.expired'); }); }); diff --git a/server-connection/geonet-console/routes/web.php b/server-connection/geonet-console/routes/web.php index d3b5791..e2242be 100644 --- a/server-connection/geonet-console/routes/web.php +++ b/server-connection/geonet-console/routes/web.php @@ -89,6 +89,12 @@ Route::middleware(['auth.ldap', 'auth.admin', 'audit.web'])->group(function () { Route::delete('/api-clients/tokens/{apiToken}', [ApiClientController::class, 'destroyToken'])->name('api-clients.tokens.destroy'); Route::post('/api-clients/oauth', [ApiClientController::class, 'storeOAuthClient'])->name('api-clients.oauth.store'); Route::delete('/api-clients/oauth/{clientId}', [ApiClientController::class, 'destroyOAuthClient'])->name('api-clients.oauth.destroy'); + + // Service Tokens (microservice Bearer token management) + Route::post('/api-clients/service-tokens', [ApiClientController::class, 'storeServiceToken'])->name('api-clients.service-tokens.store'); + Route::post('/api-clients/service-tokens/{id}/revoke', [ApiClientController::class, 'revokeServiceToken'])->name('api-clients.service-tokens.revoke'); + Route::post('/api-clients/service-tokens/{id}/regenerate', [ApiClientController::class, 'regenerateServiceToken'])->name('api-clients.service-tokens.regenerate'); + Route::delete('/api-clients/service-tokens/{id}', [ApiClientController::class, 'destroyServiceToken'])->name('api-clients.service-tokens.destroy'); Route::get('/mail-settings', [MailSettingsController::class, 'index'])->name('mail-settings.index'); Route::put('/mail-settings/smtp', [MailSettingsController::class, 'updateSmtp'])->name('mail-settings.smtp.update'); diff --git a/server-connection/perintah.md b/server-connection/perintah.md new file mode 100644 index 0000000..2779899 --- /dev/null +++ b/server-connection/perintah.md @@ -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) |