diff --git a/.sisyphus/plans/post-deploy-fixes.md b/.sisyphus/plans/post-deploy-fixes.md new file mode 100644 index 0000000..7baaba1 --- /dev/null +++ b/.sisyphus/plans/post-deploy-fixes.md @@ -0,0 +1,387 @@ +# Post-Deploy Fixes + Config Delivery Plan + +## TL;DR + +> **Quick Summary**: Fix 3 immediate bugs (DNS clear, peer detection debug, peer IP change) and design secure config delivery for devices. +> +> **Deliverables**: +> - [x] DNS override can be cleared to empty (frontend fix) +> - [ ] Debug procedure documented for peer detection failures +> - [x] InternalIP editable from DeviceDetail.vue +> - [x] Firewall port range can be empty (apply to all ports) +> - [ ] Encrypted config delivery endpoint + companion plaintext endpoint +> +> **Estimated Effort**: Medium +> **Parallel Execution**: YES - 2 waves +> **Critical Path**: T1 → [T2] → T3 + +--- + +## Context + +### Original Issues + +1. **DNS tidak bisa dikosongkan**: Ketika user kosongkan field DNS Override di DeviceDetail.vue lalu save, `advForm.value.dns` jadi `""`, tapi `"" || undefined` di JavaScript menghasilkan `undefined`. Akibatnya field `dns` tidak terkirim ke API, nilai DB tidak berubah. + +2. **Peer tidak mendeteksi koneksi**: Device official WG client tetap Offline meski terkoneksi. Perlu debugging multi-layer: (1) cek collector jalan, (2) cek kernel handshake, (3) cek frontend isDeviceOnline(). + +3. **Tidak bisa ganti peer IP**: `UpdateDeviceRequest` tidak memiliki field `InternalIP`. IP hanya dialokasikan sekali oleh IPAM saat provisioning. + +4. **Config delivery**: Ingin endpoint aman untuk device ambil config, tapi juga bisa debug plaintext untuk WireGuard client manual. + +### Current State + +- **DNS backend**: `devices.go:236-238` — sudah handle `req.DNS != nil`, termasuk empty string ✅ +- **DNS frontend**: `DeviceDetail.vue:300` — `dns: advForm.value.dns || undefined` ❌ bug +- **Peer IP**: `UpdateDeviceRequest` di `devices.go:190` — TIDAK ada field `InternalIP` +- **Config endpoints**: + - `GET /devices/:id/config` → plaintext (admin only) + - `POST /provision` → `encrypted_config` AES-256-GCM (token-based) + - `GET /devices/:id/config/qr` → QR image (admin only) + - **Tidak ada endpoint publik untuk device mengambil config sendiri** + +--- + +## Work Objectives + +### Objective A: Fix Bugs (Issue 1) + +**Must Have**: +- [ ] DNS override bisa dikosongkan dan tersimpan sebagai `""` di DB +- [ ] `npm run build` passes + +**Must NOT Have**: +- [ ] Tidak mengubah struktur API atau backend + +### Objective B: Debug Guide (Issue 2) + +**Must Have**: +- [ ] Step-by-step debugging procedure untuk peer detection +- [ ] Commands untuk tiap layer (kernel, collector, DB, frontend) + +### Objective C: Peer IP Change (Issue 3) + +**Must Have**: +- [ ] Admin bisa mengubah `internal_ip` device via API +- [ ] API memvalidasi format IP sebelum update +- [ ] API mengecek duplikasi IP di IPAM sebelum update +- [ ] Firewall rule re-sync jika IP berubah +- [ ] Frontend input field untuk InternalIP di DeviceDetail.vue + +**Must NOT Have**: +- [ ] Jangan override alokasi IPAM untuk device yang belum provisioning +- [ ] Jangan izinkan non-admin mengganti IP + +### Objective D: Config Delivery (Issue 4) + +**Questions to resolve** (akan ditanyakan ke user): +- Apakah device perlu endpoint autentikasi sendiri (token/API key) untuk ambil config? +- Atau cukup admin ambilkan seperti sekarang? +- Encrypted payload (AES-GCM) atau cukup HTTPS saja? +- Perlu QR code juga atau config text saja? + +**Must Have** (asumsi awal): +- [ ] Endpoint untuk device mengambil config sendiri (setelah provisioning) +- [ ] Opsi plaintext untuk debugging +- [ ] Opsi encrypted payload untuk production + +--- + +## Verification Strategy + +> **ZERO HUMAN INTERVENTION** — ALL verification is agent-executed. + +- **Frontend**: `npm run build` + Playwright (cek DNS save + IP input) +- **Backend**: `go build -tags dev ./...` + curl API test +- **Config**: curl endpoint + validasi response format + +--- + +## Execution Strategy + +``` +Wave 1 (Parallel — all remaining work): +├── T1: Fix DNS clear bug (frontend) [DONE] +├── T2: Write debugging docs [INFORMATIONAL] +├── T3: Add InternalIP change to backend + frontend +└── T4: Fix firewall port range to allow empty (models + API + frontend + nftables) + +Wave 2 (Future — separate plan): +├── Device-Agent Registration + Approval flow +└── Encrypted config delivery endpoint +``` + +--- + +## TODOs + +- [ ] 1. Fix DNS clear bug + + **What to do**: + - File: `apps/dashboard-ui/src/views/DeviceDetail.vue` baris 300 + - Ubah `dns: advForm.value.dns || undefined` menjadi `dns: advForm.value.dns === '' ? '' : advForm.value.dns || undefined` + - Run `npm run build` untuk verifikasi + + **Must NOT do**: + - Jangan ubah backend — sudah support empty string + + **Parallelization**: Wave 1, parallel with T2, T3 + + **Acceptance Criteria**: + - [ ] `npm run build` passes + - [ ] Set DNS → kosongkan → save → reload → DNS tetap kosong + + **QA Scenarios**: + ``` + Scenario: Clear DNS override saves correctly + Tool: Bash (curl) + Preconditions: Device with existing DNS + Steps: + 1. curl -X PUT /api/v1/devices/$ID -H "Content-Type: application/json" -d '{"dns": ""}' -H "Authorization: Bearer $TOKEN" + 2. curl -X GET /api/v1/devices/$ID -H "Authorization: Bearer $TOKEN" + Expected Result: Response JSON includes "dns": "" or null + Evidence: .sisyphus/evidence/postdeploy-t1-dns-clear.txt + + Scenario: Build passes + Tool: Bash + Steps: cd apps/dashboard-ui && npm run build + Expected Result: vue-tsc + vite build succeeds + Evidence: .sisyphus/evidence/postdeploy-t1-build.txt + ``` + + **Commit**: YES — `fix(ui): allow clearing DNS override to empty` + +- [ ] 2. Document peer detection debugging procedure + + **What to do**: + Create debugging doc covering 5 layers: + 1. Kernel: `sudo wg show` — peer handshake dari kernel + 2. Collector: `docker logs` grep handshake — apakah collector jalan + 3. Node type: query DB `wg_servers.name` — local node atau remote + 4. DB data: `psql` cek `is_active`, `last_handshake`, `client_info` + 5. Status API: `curl /devices/:id/status` — realtime status dari API + + **Parallelization**: Wave 1, parallel + + **Acceptance Criteria**: + - [ ] Setiap langkah punya command exact yang bisa di-copy-paste + - [ ] Setiap langkah punya expected output + + **Commit**: NO (informational) + +- [x] 3. Add InternalIP to UpdateDeviceRequest + handler + UI + + **What to do**: + + **Backend** — `apps/server-core/api/devices.go`: + - Tambah `InternalIP *string json:"internal_ip"` ke `UpdateDeviceRequest` struct + - Di Update handler, setelah blok DNS (line ~238), tambah: + ```go + if req.InternalIP != nil { + ip := net.ParseIP(*req.InternalIP) + if ip == nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid internal_ip format"}) + return + } + // Check duplicate IP across all devices + servers + var count int64 + h.db.Model(&models.Device{}).Where("internal_ip = ? AND id != ?", *req.InternalIP, id).Count(&count) + if count > 0 { + c.JSON(http.StatusConflict, gin.H{"error": "internal_ip already in use"}) + return + } + updates["internal_ip"] = *req.InternalIP + // Re-sync firewall if endpoint_allowed_ips also changed or existed + if req.EndpointAllowedIPs != nil && *req.EndpointAllowedIPs != "" { + destCIDR := *req.EndpointAllowedIPs + // Remove old rule, add new rule + h.fw.AddForwardRule(device.Name, ip, destCIDR) + } + } + ``` + + **Frontend** — `apps/dashboard-ui/src/views/DeviceDetail.vue`: + - Di advanced settings section, tambah input field: + ```html +