942 lines
43 KiB
Markdown
942 lines
43 KiB
Markdown
# WG Keys Exposure, Regeneration, and Debug Panel
|
|
|
|
## TL;DR
|
|
|
|
> **Quick Summary**: Fix admin bypass in devices.go handlers, expose PrivateKey/PresharedKey for admin-only device views, add regenerate-keys endpoint, add device status API, add node PublicKey editing, and create debug panel in DeviceDetail.vue — all matching wg-dashboard UX patterns.
|
|
>
|
|
> **Deliverables**:
|
|
> - Backend: admin bypass in 6 devices.go handlers
|
|
> - Backend: `POST /devices/:id/regenerate-keys` endpoint
|
|
> - Backend: `GET /devices/:id/status` real-time WG status endpoint
|
|
> - Backend: `public_key` in `UpdateServerRequest` for node editing
|
|
> - Frontend: PrivateKey/PresharedKey display (eye toggle, admin-only) in DeviceDetail
|
|
> - Frontend: Regenerate Keys button in DeviceDetail
|
|
> - Frontend: Debug panel showing connection stats (admin-only) in DeviceDetail
|
|
> - Frontend: PublicKey edit field in Servers.vue edit modal
|
|
>
|
|
> **Estimated Effort**: Large
|
|
> **Parallel Execution**: YES - 3 waves
|
|
> **Critical Path**: W1-T1 → W1-T2 → W1-T3 → W1-T4 → W1-T5 → W1-T6 → W2-T7 → W2-T8/9/10/11
|
|
|
|
---
|
|
|
|
## Context
|
|
|
|
### Original Request
|
|
User wants the NexusGuard dashboard to work like wg-dashboard:
|
|
1. Show private keys for WG nodes and device peers (admin only)
|
|
2. Allow editing node PublicKey (from other servers)
|
|
3. Allow editing device PresharedKey (regenerate, not manual text)
|
|
4. Add reset/regenerate keys button
|
|
5. Debug panel showing connection status (admin only)
|
|
6. Fix advanced settings save for Allowed IPs, DNS, PresharedKey toggle
|
|
|
|
### Interview Summary
|
|
**Key Discussions**:
|
|
- Key exposure only for admin (non-admin users cannot see keys)
|
|
- Device keys are regenerate-only (no manual text input)
|
|
- Node PublicKey can be manually edited (for keys from external servers)
|
|
- Reset Keys regenerates both PrivateKey + PresharedKey simultaneously
|
|
- Debug panel shows existing data (rx_bytes, tx_bytes, last_handshake) + new `/status` endpoint for real-time WG data
|
|
- Status endpoint is admin-only
|
|
|
|
**Model Changes**:
|
|
- `Device.PrivateKey`: Add `json:"private_key"` tag (currently no tag, serialized as PascalCase)
|
|
- `Device.PresharedKey`: Add `json:"preshared_key"` tag (currently no tag)
|
|
- Both fields must be STRIPPED from `List` responses (only included in individual `Get`)
|
|
- `WgServer.PrivateKey`: KEEP `json:"-"` — NEVER expose server private keys
|
|
- `UpdateDeviceRequest`: Add `private_key` and `preshared_key` optional fields
|
|
- `UpdateServerRequest`: Add `public_key` optional field
|
|
|
|
**New Endpoints**:
|
|
- `POST /devices/:id/regenerate-keys` — Generates new WG keypair + PSK, returns new keys
|
|
- `GET /devices/:id/status` — Real-time WG status (admin only)
|
|
|
|
### Research Findings
|
|
- `devices.go` handlers (`Get`, `Update`, `Delete`, `RegenerateToken`, `Suspend`, `Unsuspend`) all filter by `AND user_id = ?` without `isAdmin(c)` bypass — bug confirmed
|
|
- `WgServer.PrivateKey` has `json:"-"` — intentionally hidden from API
|
|
- `Device.PrivateKey`/`PresharedKey` have NO json tags — serialized as PascalCase keys
|
|
- `UpdateConfig` handler (peers.go:265) explicitly rejects PrivateKey/PresharedKey changes, referencing "Regenerate Keys feature" that doesn't exist yet
|
|
- `mapDevice()` in devices.ts doesn't map `PrivateKey`/`PresharedKey` — data is spread from `...d` but TypeScript interface doesn't declare them
|
|
- Frontend `Device` interface missing `PrivateKey`, `PresharedKey`
|
|
- `UpdateServerRequest` missing `public_key` field
|
|
- Servers.vue edit modal missing PublicKey input field
|
|
- `wgtypes.ParseKey()` available for key validation
|
|
|
|
### Metis Review
|
|
**Identified Gaps** (addressed):
|
|
- **Security**: `WgServer.PrivateKey` must keep `json:"-"` — confirmed. Never expose in List.
|
|
- **Security**: Device keys must only appear in individual GET (`/devices/:id`), not in List
|
|
- **Security**: Keys must only be accessible to admin
|
|
- **Validation**: All key inputs must be validated with `wgtypes.ParseKey()`
|
|
- **Sync**: `SyncLocalPeers()` must be called after key regeneration
|
|
- **Scope bleed**: No refactoring of admin middleware, no touching share/provisioning, no crypto dedup
|
|
|
|
---
|
|
|
|
## Work Objectives
|
|
|
|
### Core Objective
|
|
Make the NexusGuard WG dashboard feature-complete with wg-dashboard-style key visibility and debug capabilities, while maintaining enterprise security boundaries (admin-only).
|
|
|
|
### Concrete Deliverables
|
|
- Backend changes in `devices.go`, `servers.go`, `peers.go`, `models.go`
|
|
- Frontend changes in `DeviceDetail.vue`, `Servers.vue`, `devices.ts`, `servers.ts`, `server-core/main.go` (routing)
|
|
- 2 new API endpoints: `regenerate-keys`, `status`
|
|
- Admin bypass in 6 devices.go handlers
|
|
- Debug panel read-only section in DeviceDetail
|
|
|
|
### Definition of Done
|
|
- [x] `curl -X PUT -H "Authorization: Bearer $ADMIN_TOKEN" /api/v1/devices/$ID -d '{"dns":"1.1.1.1"}'` → 200, DNS updated
|
|
- [x] `curl -X PUT -H "Authorization: Bearer $NON_ADMIN_TOKEN" /api/v1/devices/$OTHER_USER_DEVICE_ID` → 404 (not found)
|
|
- [x] `curl -H "Authorization: Bearer $ADMIN_TOKEN" /api/v1/devices/$ID` → JSON includes `private_key` and `preshared_key`
|
|
- [x] `curl -H "Authorization: Bearer $ADMIN_TOKEN" /api/v1/devices` → Array items DO NOT contain `private_key` or `preshared_key`
|
|
- [x] `curl -X POST -H "Authorization: Bearer $ADMIN_TOKEN" /api/v1/devices/$ID/regenerate-keys` → 200, new private_key + preshared_key (≠ old)
|
|
- [x] `curl -H "Authorization: Bearer $ADMIN_TOKEN" /api/v1/devices/$ID/status` → 200, JSON with is_active, last_handshake, rx_bytes, tx_bytes
|
|
- [x] `curl -X PUT -H "Authorization: Bearer $ADMIN_TOKEN" /api/v1/servers/$ID -d '{"public_key":"newpub..."}'` → 200, server.public_key updated
|
|
- [x] `npm run build` passing (vue-tsc + vite build)
|
|
- [x] `go build -tags dev ./...` passing
|
|
- [x] DeviceDetail.vue shows PrivateKey/PresharedKey with eye-toggle (admin only)
|
|
- [x] DeviceDetail.vue has "Regenerate Keys" button → calls POST → shows new keys
|
|
- [x] DeviceDetail.vue has debug panel showing status data
|
|
- [x] Servers.vue edit modal has PublicKey input field
|
|
|
|
### Must Have
|
|
- Admin bypass in all 6 devices.go handlers (Get, Update, Delete, RegenerateToken, Suspend, Unsuspend)
|
|
- Json tags on Device.PrivateKey/PresharedKey
|
|
- Strip private keys from List responses
|
|
- Validate all key inputs with `wgtypes.ParseKey()`
|
|
- Call `SyncLocalPeers()` after key regeneration
|
|
- Admin-only access to keys and status
|
|
|
|
### Must NOT Have (Guardrails)
|
|
- **NEVER** remove `json:"-"` from `WgServer.PrivateKey` — security boundary
|
|
- **NEVER** return private keys in List/array endpoints
|
|
- **NEVER** log plaintext keys or encryption keys (project anti-pattern)
|
|
- **NEVER** touch `shared/crypto/encryptor.go` (known debt)
|
|
- **NEVER** touch provisioning or share handlers
|
|
- **NEVER** refactor admin middleware pattern
|
|
- **NEVER** change DB schema — all fields already exist
|
|
- **NEVER** allow non-admin users to see keys
|
|
|
|
---
|
|
|
|
## Verification Strategy
|
|
|
|
> **ZERO HUMAN INTERVENTION** — ALL verification is agent-executed. No exceptions.
|
|
|
|
### Test Decision
|
|
- **Infrastructure exists**: YES (Go tests)
|
|
- **Automated tests**: Tests-after (implementation-first, then verify with tests)
|
|
- **Framework**: `go test ./... -tags dev` for backend, `npm run build` for frontend
|
|
|
|
### QA Policy
|
|
Every task MUST include agent-executed QA scenarios.
|
|
- **Backend/API**: Bash (curl) — Send requests, assert status + response fields
|
|
- **Frontend/UI**: Playwright — Navigate, interact, assert DOM, screenshot
|
|
- **Evidence** saved to `.sisyphus/evidence/task-{N}-{scenario-slug}.{ext}`
|
|
|
|
---
|
|
|
|
## Execution Strategy
|
|
|
|
### Parallel Execution Waves
|
|
|
|
```
|
|
Wave 1 (Backend Foundation — Start Immediately):
|
|
├── T1: Add json tags to Device.PrivateKey/PresharedKey + strip from List
|
|
├── T2: Admin bypass in devices.go (Get, Update, Delete, RegenerateToken, Suspend, Unsuspend)
|
|
├── T3: UpdateDeviceRequest: add private_key/preshared_key fields + ParseKey validation
|
|
├── T4: POST /devices/:id/regenerate-keys endpoint
|
|
├── T5: GET /devices/:id/status endpoint (admin-only, real-time WG data)
|
|
└── T6: UpdateServerRequest: add public_key field + Servers.vue edit modal wiring
|
|
|
|
Wave 2 (Frontend — After Wave 1):
|
|
├── T7: Update Device interface + mapDevice in devices.ts
|
|
├── T8: DeviceDetail.vue: PrivateKey/PresharedKey display (eye toggle, admin-only)
|
|
├── T9: DeviceDetail.vue: Regenerate Keys button
|
|
├── T10: DeviceDetail.vue: Debug panel (status data section)
|
|
└── T11: Servers.vue: Add PublicKey field to edit form
|
|
|
|
Wave FINAL (Verification):
|
|
├── F1: Plan compliance audit
|
|
├── F2: Code quality review
|
|
├── F3: Real manual QA (curl + Playwright)
|
|
└── F4: Scope fidelity check
|
|
```
|
|
|
|
---
|
|
|
|
## TODOs
|
|
|
|
- [x] 1. **Add json tags to Device.PrivateKey/PresharedKey + strip from List**
|
|
|
|
**What to do**:
|
|
- In `models/models.go`, add `json:"private_key"` and `json:"preshared_key"` tags to `Device.PrivateKey` and `Device.PresharedKey`
|
|
- In `devices.go` `List()` handler, create a response type that strips `PrivateKey` and `PresharedKey` from the JSON output (or set them to empty string for non-admin / all users)
|
|
- In `devices.go` `Get()` handler, if admin include the keys, if non-admin strip them
|
|
- Pattern: use a `DeviceResponse` struct or omit fields in the c.JSON call
|
|
|
|
**Must NOT do**:
|
|
- Do NOT remove `json:"-"` from `WgServer.PrivateKey`
|
|
- Do NOT expose keys in List responses
|
|
|
|
**Recommended Agent Profile**:
|
|
- **Category**: `unspecified-high`
|
|
- Reason: Backend Go changes touching models and handlers — medium complexity
|
|
- **Skills**: none needed
|
|
|
|
**Parallelization**:
|
|
- **Can Run In Parallel**: YES
|
|
- **Parallel Group**: Wave 1 (with T2-T6)
|
|
- **Blocks**: T7 (frontend mapDevice)
|
|
- **Blocked By**: None (can start immediately)
|
|
|
|
**References**:
|
|
- `apps/server-core/internal/models/models.go:58-86` — Device struct, add json tags to lines 68-69
|
|
- `apps/server-core/api/devices.go:35-56` — List handler, strip keys from response
|
|
- `apps/server-core/api/devices.go:154-164` — Get handler, include keys for admin
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] `curl -H "Authorization: Bearer $ADMIN_TOKEN" /api/v1/devices/$ID` → JSON includes `"private_key": "..."` and `"preshared_key": "..."`
|
|
- [ ] `curl -H "Authorization: Bearer $ADMIN_TOKEN" /api/v1/devices` → Array items do NOT have `private_key` or `preshared_key` fields
|
|
- [ ] `curl -H "Authorization: Bearer $NON_ADMIN_TOKEN" /api/v1/devices/$ID` → JSON does NOT include `private_key` or `preshared_key`
|
|
- [ ] `go build -tags dev ./...` passes
|
|
|
|
**QA Scenarios**:
|
|
```
|
|
Scenario: Admin can see private keys on individual device GET
|
|
Tool: Bash (curl)
|
|
Preconditions: Admin JWT token, device ID with keys
|
|
Steps:
|
|
1. GET /api/v1/devices/$ID with admin token
|
|
2. Parse JSON response
|
|
Expected Result: Response has "private_key" (non-empty, starts with base64) and "preshared_key" (non-empty)
|
|
Evidence: .sisyphus/evidence/task-1-admin-get-keys.json
|
|
|
|
Scenario: Admin List does NOT expose private keys
|
|
Tool: Bash (curl)
|
|
Preconditions: Admin JWT token
|
|
Steps:
|
|
1. GET /api/v1/devices with admin token
|
|
2. Parse JSON response array
|
|
Expected Result: NO item in array has "private_key" or "preshared_key" fields
|
|
Evidence: .sisyphus/evidence/task-1-list-no-keys.json
|
|
|
|
Scenario: Non-admin cannot see keys
|
|
Tool: Bash (curl)
|
|
Preconditions: Non-admin JWT token, device owned by that user
|
|
Steps:
|
|
1. GET /api/v1/devices/$ID with non-admin token
|
|
Expected Result: Response does NOT include "private_key" or "preshared_key"
|
|
Evidence: .sisyphus/evidence/task-1-nonadmin-no-keys.json
|
|
```
|
|
|
|
**Commit**: YES
|
|
- Message: `feat(api): expose Device private/preshared keys for admin on individual GET, strip from List`
|
|
- Files: `apps/server-core/internal/models/models.go`, `apps/server-core/api/devices.go`
|
|
|
|
- [x] 2. **Admin bypass in devices.go handlers**
|
|
|
|
**What to do**:
|
|
- In `devices.go`, add `isAdmin(c)` checks to `Get`, `Update`, `Delete`, `RegenerateToken`, `Suspend`, `Unsuspend` handlers
|
|
- Pattern: if admin, query without `AND user_id = ?` filter; if non-admin, keep existing filter
|
|
- Follow exactly the pattern used in `List()` handler (lines 39-54)
|
|
|
|
**Must NOT do**:
|
|
- Do NOT refactor the admin check pattern — keep it inline per handler
|
|
- Do NOT change `Create` handler (already has admin bypass logic at line 83-151)
|
|
|
|
**Recommended Agent Profile**:
|
|
- **Category**: `quick`
|
|
- Reason: Simple conditional additions, well-defined pattern to copy
|
|
- **Skills**: none needed
|
|
|
|
**Parallelization**:
|
|
- **Can Run In Parallel**: YES
|
|
- **Parallel Group**: Wave 1 (with T1, T3-T6)
|
|
- **Blocks**: All frontend key-access tasks (T7-T10)
|
|
- **Blocked By**: None (can start immediately)
|
|
|
|
**References**:
|
|
- `apps/server-core/api/devices.go:39-54` — Pattern: `if isAdmin(c) { ... } else { ... }`
|
|
- `apps/server-core/api/devices.go:154-164` — `Get` handler (line 159: `WHERE id = ? AND user_id = ?`)
|
|
- `apps/server-core/api/devices.go:177-248` — `Update` handler (line 188: same filter)
|
|
- `apps/server-core/api/devices.go:250-272` — `Delete` handler
|
|
- `apps/server-core/api/devices.go:274-293` — `RegenerateToken` handler
|
|
- `apps/server-core/api/devices.go:296-338` — `Suspend`/`Unsuspend` handlers
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] Admin can GET any device (including other user's devices)
|
|
- [ ] Non-admin cannot GET another user's device (404)
|
|
- [ ] `go build -tags dev ./...` passes
|
|
- [ ] Existing tests pass (`go test ./... -tags dev`)
|
|
|
|
**QA Scenarios**:
|
|
```
|
|
Scenario: Admin retrieves another user's device
|
|
Tool: Bash (curl)
|
|
Preconditions: Admin JWT token, device owned by different user
|
|
Steps:
|
|
1. GET /api/v1/devices/$OTHER_USER_DEVICE_ID with admin token
|
|
Expected Result: Status 200, device data returned
|
|
Evidence: .sisyphus/evidence/task-2-admin-bypass-get.json
|
|
|
|
Scenario: Non-admin cannot access another user's device
|
|
Tool: Bash (curl)
|
|
Preconditions: Non-admin JWT token, device NOT owned by that user
|
|
Steps:
|
|
1. GET /api/v1/devices/$OTHER_DEVICE_ID with non-admin token
|
|
Expected Result: Status 404
|
|
Evidence: .sisyphus/evidence/task-2-nonadmin-blocked.json
|
|
|
|
Scenario: Admin can update another user's device
|
|
Tool: Bash (curl)
|
|
Preconditions: Admin token, device owned by another user
|
|
Steps:
|
|
1. PUT /api/v1/devices/$OTHER_DEVICE_ID -d '{"dns":"8.8.8.8"}' with admin token
|
|
Expected Result: Status 200
|
|
Evidence: .sisyphus/evidence/task-2-admin-bypass-update.json
|
|
```
|
|
|
|
**Commit**: YES (group with T1)
|
|
- Message: `feat(api): admin bypass in devices.go Get/Update/Delete/RegenerateToken/Suspend/Unsuspend`
|
|
- Files: `apps/server-core/api/devices.go`
|
|
|
|
- [x] 3. **Add private_key/preshared_key to UpdateDeviceRequest + ParseKey validation**
|
|
|
|
**What to do**:
|
|
- In `devices.go`, add `PrivateKey *string \`json:"private_key"\`` and `PresharedKey *string \`json:"preshared_key"\`` to `UpdateDeviceRequest` struct
|
|
- In the `Update()` handler, add processing logic for these fields:
|
|
- If `PrivateKey` is set (`!= nil`), validate with `wgtypes.ParseKey()`. If invalid, return 400.
|
|
- If `PresharedKey` is set, similarly validate with `ParseKey()`
|
|
- If `PrivateKey` is set, ALSO update `PublicKey` field with the new public key derived from the private key
|
|
- If `PresharedKey` is set to empty string `""`, that's valid (clears the PSK)
|
|
|
|
**Must NOT do**:
|
|
- Do NOT allow non-admin to update keys (the `isAdmin` bypass from T2 plus the non-admin filter will prevent this naturally since `user_id` will match non-admin's own devices only)
|
|
- Actually add an explicit `if !isAdmin(c)` check — only admin can update keys
|
|
|
|
**Recommended Agent Profile**:
|
|
- **Category**: `unspecified-high`
|
|
- Reason: Need careful validation logic with wgtypes.ParseKey
|
|
- **Skills**: none needed
|
|
|
|
**Parallelization**:
|
|
- **Can Run In Parallel**: YES
|
|
- **Parallel Group**: Wave 1 (with T1, T2, T4-T6)
|
|
- **Blocks**: T8 (frontend key display)
|
|
- **Blocked By**: None (can start immediately, but best after T2)
|
|
|
|
**References**:
|
|
- `apps/server-core/api/devices.go:166-175` — Current `UpdateDeviceRequest` struct
|
|
- `apps/server-core/api/devices.go:193-226` — Current update processing logic
|
|
- `golang.zx2c4.com/wireguard/wgctrl/wgtypes` — `ParseKey()` function
|
|
- `apps/server-core/api/peers.go:71-74` — Example of `wgtypes.GeneratePrivateKey()` usage
|
|
- `wgtypes.ParseKey(s).String()` — Validates and normalizes a key string
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] `curl -X PUT -H "Authorization: Bearer $ADMIN_TOKEN" /api/v1/devices/$ID -d '{"private_key":"yGvKQEM5..."}'` → 200 (valid key saves)
|
|
- [ ] `curl -X PUT -d '{"private_key":"invalid"}'` → 400 with validation error
|
|
- [ ] `curl -X PUT -d '{"preshared_key":"V8sKQEM5..."}'` → 200 (valid PSK saves)
|
|
- [ ] When `private_key` changes, `public_key` in response also changes
|
|
- [ ] `go build -tags dev ./...` passes
|
|
|
|
**QA Scenarios**:
|
|
```
|
|
Scenario: Admin updates device private key with valid key
|
|
Tool: Bash (curl)
|
|
Preconditions: Admin token, device ID
|
|
Steps:
|
|
1. Generate a valid WireGuard private key (or use known one)
|
|
2. PUT /api/v1/devices/$ID -d '{"private_key":"wJn3hUJvL6tPmR0sKuNxQ5yB8cDfG1aE2bH4iK7jM9="}'
|
|
Expected Result: Status 200, GET /api/v1/devices/$ID shows new private_key and matching public_key
|
|
Evidence: .sisyphus/evidence/task-3-update-private-key.json
|
|
|
|
Scenario: Reject invalid private key
|
|
Tool: Bash (curl)
|
|
Preconditions: Admin token, device ID
|
|
Steps:
|
|
1. PUT /api/v1/devices/$ID -d '{"private_key":"not-a-valid-key"}'
|
|
Expected Result: Status 400 with error containing "invalid" or "key"
|
|
Evidence: .sisyphus/evidence/task-3-invalid-key-rejected.json
|
|
```
|
|
|
|
**Commit**: YES (group with T1, T2)
|
|
- Message: `feat(api): add private_key/preshared_key to UpdateDeviceRequest with ParseKey validation`
|
|
- Files: `apps/server-core/api/devices.go`
|
|
|
|
- [x] 4. **POST /devices/:id/regenerate-keys endpoint**
|
|
|
|
**What to do**:
|
|
- In `devices.go`, add a new `RegenerateKeys` handler method on `DevicesHandler`
|
|
- Route: `POST /devices/:id/regenerate-keys` in `main.go` (add to protected group)
|
|
- Admin-only (must use `isAdmin(c)` check)
|
|
- Logic:
|
|
1. Find device by ID (with admin bypass — no user_id filter for admin)
|
|
2. Generate new WireGuard private key via `wgtypes.GeneratePrivateKey()`
|
|
3. Generate new PresharedKey via `wgtypes.GenerateKey()`
|
|
4. Compute public key from private key
|
|
5. Update device in DB: `PrivateKey`, `PublicKey`, `PresharedKey`, `DisablePresharedKey = false`
|
|
6. Call `h.syncer.SyncLocalPeers()` to propagate new public key to WireGuard interface
|
|
7. Return JSON: `{ "private_key": "...", "public_key": "...", "preshared_key": "..." }`
|
|
|
|
**Must NOT do**:
|
|
- Do NOT change `RegenerateToken` handler (different purpose — provisioning token)
|
|
- Do NOT update `rx_bytes`/`tx_bytes`/other stats — only keys
|
|
- Do NOT touch provisioning or agent config
|
|
|
|
**Recommended Agent Profile**:
|
|
- **Category**: `unspecified-high`
|
|
- Reason: New endpoint with key generation + DB update + peer sync
|
|
- **Skills**: none needed
|
|
|
|
**Parallelization**:
|
|
- **Can Run In Parallel**: YES
|
|
- **Parallel Group**: Wave 1 (with T1-T3, T5-T6)
|
|
- **Blocks**: T9 (frontend Regenerate button)
|
|
- **Blocked By**: T2 (admin bypass pattern) — for consistency
|
|
|
|
**References**:
|
|
- `apps/server-core/api/devices.go:274-293` — `RegenerateToken` for route pattern reference
|
|
- `apps/server-core/api/peers.go:71-85` — `wgtypes.GeneratePrivateKey()`, `wgtypes.GenerateKey()` usage
|
|
- `apps/server-core/api/peers.go:148` — `h.syncer.SyncLocalPeers()` call after creation
|
|
- `apps/server-core/main.go:297-311` — Route registration area
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] `curl -X POST -H "Authorization: Bearer $ADMIN_TOKEN" /api/v1/devices/$ID/regenerate-keys` → 200, JSON with new keys
|
|
- [ ] New `private_key` ≠ old `private_key`
|
|
- [ ] New `preshared_key` ≠ old `preshared_key`
|
|
- [ ] `public_key` in response matches public key derived from new private key
|
|
- [ ] Non-admin gets 403
|
|
- [ ] `go build -tags dev ./...` passes
|
|
|
|
**QA Scenarios**:
|
|
```
|
|
Scenario: Regenerate keys successfully
|
|
Tool: Bash (curl)
|
|
Preconditions: Admin token, device with existing keys
|
|
Steps:
|
|
1. GET current device keys (save old values)
|
|
2. POST /api/v1/devices/$ID/regenerate-keys with admin token
|
|
3. Parse response for new keys
|
|
4. GET device again to verify DB updated
|
|
Expected Result: New private_key ≠ old private_key, new preshared_key ≠ old preshared_key, public_key matches new private key
|
|
Evidence: .sisyphus/evidence/task-4-regenerate-keys.json
|
|
|
|
Scenario: Non-admin rejected
|
|
Tool: Bash (curl)
|
|
Preconditions: Non-admin token
|
|
Steps:
|
|
1. POST /api/v1/devices/$ID/regenerate-keys with non-admin token
|
|
Expected Result: Status 403
|
|
Evidence: .sisyphus/evidence/task-4-nonadmin-rejected.json
|
|
```
|
|
|
|
**Commit**: YES (group with T1-T3)
|
|
- Message: `feat(api): add POST /devices/:id/regenerate-keys endpoint`
|
|
- Files: `apps/server-core/api/devices.go`, `apps/server-core/main.go`
|
|
|
|
- [x] 5. **GET /devices/:id/status endpoint (admin-only, real-time WG data)**
|
|
|
|
**What to do**:
|
|
- In a new file or existing `devices.go`, add a `GetDeviceStatus` handler on `DevicesHandler`
|
|
- Route: `GET /devices/:id/status` in `main.go` (protected, admin-only)
|
|
- Admin only — use `isAdmin(c)`
|
|
- Logic:
|
|
1. Find device by ID (admin bypass — no user_id filter)
|
|
2. Gather real-time status data:
|
|
- `is_active`: From device's `IsActive` field (set by Redis heartbeat)
|
|
- `last_handshake`: From device's `LastHandshake` field
|
|
- `rx_bytes`, `tx_bytes`: From device fields (updated by heartbeat/peer sync)
|
|
- For local node devices: optionally call `h.wgmgr.GetStatus()` to verify WG interface
|
|
- `public_key`: Current device public key
|
|
- `internal_ip`: Current device IP
|
|
- `wg_server_id`: Which server it's on
|
|
- `is_suspended`: Whether suspended
|
|
3. Return JSON with all status fields
|
|
|
|
**Must NOT do**:
|
|
- Do NOT include `private_key` or `preshared_key` in status response (status is for operational data, not keys)
|
|
- Do NOT make blocking calls to external nodes — only use local data
|
|
- Do NOT pollute the regular `GET /devices/:id` response
|
|
|
|
**Recommended Agent Profile**:
|
|
- **Category**: `unspecified-high`
|
|
- Reason: New endpoint merging DB + WG status data
|
|
- **Skills**: none needed
|
|
|
|
**Parallelization**:
|
|
- **Can Run In Parallel**: YES
|
|
- **Parallel Group**: Wave 1 (with T1-T4, T6)
|
|
- **Blocks**: T10 (frontend debug panel)
|
|
- **Blocked By**: None (can start immediately)
|
|
|
|
**References**:
|
|
- `apps/server-core/api/servers.go:31-68` — `healthCheckLoop` pattern for WG status checking
|
|
- `apps/server-core/internal/models/models.go:72-81` — Device fields: IsActive, LastHandshake, RxBytes, TxBytes
|
|
- `apps/server-core/main.go:297-311` — Route registration area
|
|
- `apps/server-core/api/devices.go:154-164` — Get handler pattern for finding device
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] `curl -H "Authorization: Bearer $ADMIN_TOKEN" /api/v1/devices/$ID/status` → 200 JSON
|
|
- [ ] Response includes: `is_active`, `last_handshake`, `rx_bytes`, `tx_bytes`, `public_key`, `internal_ip`, `wg_server_id`, `is_suspended`, `name`
|
|
- [ ] Non-admin gets 403
|
|
- [ ] `go build -tags dev ./...` passes
|
|
|
|
**QA Scenarios**:
|
|
```
|
|
Scenario: Get device status as admin
|
|
Tool: Bash (curl)
|
|
Preconditions: Admin token, device ID
|
|
Steps:
|
|
1. GET /api/v1/devices/$ID/status with admin token
|
|
Expected Result: Status 200, JSON has fields: is_active (bool), last_handshake (string), rx_bytes (int), tx_bytes (int), public_key (string), internal_ip (string), is_suspended (bool)
|
|
Evidence: .sisyphus/evidence/task-5-status.json
|
|
|
|
Scenario: Non-admin cannot access status
|
|
Tool: Bash (curl)
|
|
Preconditions: Non-admin token
|
|
Steps:
|
|
1. GET /api/v1/devices/$ID/status with non-admin token
|
|
Expected Result: Status 403
|
|
Evidence: .sisyphus/evidence/task-5-status-nonadmin.json
|
|
```
|
|
|
|
**Commit**: YES (group with T1-T4)
|
|
- Message: `feat(api): add GET /devices/:id/status endpoint for real-time WG data`
|
|
- Files: `apps/server-core/api/devices.go`, `apps/server-core/main.go`
|
|
|
|
- [x] 6. **UpdateServerRequest: add public_key field + Servers.vue edit modal wiring**
|
|
|
|
**What to do**:
|
|
**Backend**:
|
|
- In `servers.go`, add `PublicKey *string \`json:"public_key"\`` to `UpdateServerRequest` struct
|
|
- In the `Update()` handler, add: `if req.PublicKey != nil { server.PublicKey = *req.PublicKey }`
|
|
- No ParseKey validation needed for server PublicKey (it's the public key of the external server, user may paste it from the server's config)
|
|
- But add basic sanity check: base64-like (44 chars)
|
|
**Frontend (partial — wiring only, actual UI field in T11)**:
|
|
- In `servers.ts` `updateServer()`, add `public_key?: string` to the parameter type
|
|
- No other frontend changes in this task (UI field will be added in T11)
|
|
|
|
**Must NOT do**:
|
|
- Do NOT expose `PrivateKey` — keep `json:"-"`
|
|
- Do NOT change `CreateServerRequest` (already has PublicKey)
|
|
|
|
**Recommended Agent Profile**:
|
|
- **Category**: `quick`
|
|
- Reason: Simple field addition, minimal logic
|
|
- **Skills**: none needed
|
|
|
|
**Parallelization**:
|
|
- **Can Run In Parallel**: YES
|
|
- **Parallel Group**: Wave 1 (with T1-T5)
|
|
- **Blocks**: T11 (frontend PublicKey input)
|
|
- **Blocked By**: None (can start immediately)
|
|
|
|
**References**:
|
|
- `apps/server-core/api/servers.go:256-274` — `UpdateServerRequest` struct
|
|
- `apps/server-core/api/servers.go:288-380` — `Update()` handler
|
|
- `apps/dashboard-ui/src/api/servers.ts:61-81` — `updateServer()` TypeScript type
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] `curl -X PUT -H "Authorization: Bearer $ADMIN_TOKEN" /api/v1/servers/$ID -d '{"public_key":"newBase64Key..."}'` → 200
|
|
- [ ] GET /api/v1/servers/$ID → `public_key` updated
|
|
- [ ] `go build -tags dev ./...` passes
|
|
|
|
**QA Scenarios**:
|
|
```
|
|
Scenario: Update server public key
|
|
Tool: Bash (curl)
|
|
Preconditions: Admin token, server ID
|
|
Steps:
|
|
1. PUT /api/v1/servers/$ID -d '{"public_key":"xTIBdKvR3W0o5Lm7cNpQ8yA2FgH6jK4sD1f9G3hJ5M="}' with admin token
|
|
2. GET /api/v1/servers to verify
|
|
Expected Result: Status 200, server.public_key == "xTIBdKvR3W0o5Lm7cNpQ8yA2FgH6jK4sD1f9G3hJ5M="
|
|
Evidence: .sisyphus/evidence/task-6-update-server-key.json
|
|
```
|
|
|
|
**Commit**: YES (group with T1-T5)
|
|
- Message: `feat(api): add public_key to UpdateServerRequest for node key editing`
|
|
- Files: `apps/server-core/api/servers.go`, `apps/dashboard-ui/src/api/servers.ts`
|
|
|
|
- [x] 7. **Update Device interface + mapDevice in devices.ts**
|
|
|
|
**What to do**:
|
|
- In `devices.ts`, add `PrivateKey?: string` and `PresharedKey?: string` to the `Device` TypeScript interface
|
|
- In `mapDevice()`, add mapping:
|
|
```typescript
|
|
PrivateKey: d.private_key,
|
|
PresharedKey: d.preshared_key,
|
|
```
|
|
- Note: These fields will only be present in individual GET responses (admin only) — frontend should handle gracefully when they're undefined
|
|
|
|
**Must NOT do**:
|
|
- Do NOT change existing field mappings
|
|
- Do NOT expose keys in device List processing
|
|
|
|
**Recommended Agent Profile**:
|
|
- **Category**: `quick`
|
|
- Reason: Simple TypeScript type changes, very straightforward
|
|
- **Skills**: none needed
|
|
|
|
**Parallelization**:
|
|
- **Can Run In Parallel**: YES
|
|
- **Parallel Group**: Wave 2 (with T8-T11)
|
|
- **Blocks**: T8, T9 (frontend key display + regenerate button)
|
|
- **Blocked By**: T1 (json tags on backend)
|
|
|
|
**References**:
|
|
- `apps/dashboard-ui/src/api/devices.ts:3-15` — `mapDevice()` function
|
|
- `apps/dashboard-ui/src/api/devices.ts:17-38` — `Device` interface
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] TypeScript compiles without errors (`vue-tsc -b`)
|
|
- [ ] `Device` interface has `PrivateKey` and `PresharedKey` as optional strings
|
|
- [ ] `mapDevice` maps `d.private_key` → `PrivateKey`
|
|
|
|
**QA Scenarios**:
|
|
```
|
|
Scenario: TypeScript compilation passes
|
|
Tool: Bash
|
|
Preconditions: Node modules installed
|
|
Steps:
|
|
1. cd apps/dashboard-ui && npx vue-tsc -b --noEmit
|
|
Expected Result: Exit code 0, no type errors
|
|
Evidence: .sisyphus/evidence/task-7-tsc-pass.txt
|
|
```
|
|
|
|
**Commit**: YES (group with T8, T9, T10)
|
|
- Message: `feat(ui): add PrivateKey/PresharedKey to Device TypeScript interface and mapDevice`
|
|
- Files: `apps/dashboard-ui/src/api/devices.ts`
|
|
|
|
- [x] 8. **DeviceDetail.vue: PrivateKey/PresharedKey display with eye toggle (admin only)**
|
|
|
|
**What to do**:
|
|
- In the DeviceDetail template, add a new section (below Allow Internet Access, inside the left column) showing:
|
|
- **Private Key**: masked by default, eye icon to toggle show/hide
|
|
- **Preshared Key**: masked by default, eye icon to toggle show/hide
|
|
- **Public Key**: always visible (read-only, already available from `device.value.PublicKey`)
|
|
- Only visible when:
|
|
- `authStore.isAdmin` is true
|
|
- `device.value.PrivateKey` is not empty
|
|
- Use same glassmorphism styling (`bg-black/30 rounded-xl border border-white/5 p-4`)
|
|
- Use a copy button next to each key (copy to clipboard)
|
|
- Masking: replace middle portion with `••••` like `yGvK...••••...J9M=`
|
|
- Eye toggle: `<button @click="showPrivateKey = !showPrivateKey">👁️</button>`
|
|
|
|
**Must NOT do**:
|
|
- Do NOT display if user is not admin
|
|
- Do NOT allow editing keys as text (regenerate-only — will be in T9)
|
|
- Do NOT expose keys in any non-admin view
|
|
|
|
**Recommended Agent Profile**:
|
|
- **Category**: `visual-engineering`
|
|
- Reason: Vue template + glassmorphism styling, conditional visibility
|
|
- **Skills**: none needed
|
|
|
|
**Parallelization**:
|
|
- **Can Run In Parallel**: YES
|
|
- **Parallel Group**: Wave 2 (with T7, T9-T11)
|
|
- **Blocks**: None
|
|
- **Blocked By**: T7 (Device interface update)
|
|
|
|
**References**:
|
|
- `apps/dashboard-ui/src/views/DeviceDetail.vue:29-38` — Existing glassmorphism `bg-black/30 rounded-xl border border-white/5` pattern
|
|
- `apps/dashboard-ui/src/views/DeviceDetail.vue:135-138` — Auth store import and `authStore.isAdmin` usage
|
|
- `apps/dashboard-ui/src/views/DeviceDetail.vue:19-22` — Existing Internal IP display area (glassmorphism pattern for key-value pairs)
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] Admin sees PublicKey, PrivateKey (masked with eye toggle), PresharedKey (masked) sections
|
|
- [ ] Non-admin does NOT see any key sections
|
|
- [ ] Eye toggle shows/hides key text
|
|
- [ ] Copy button copies key to clipboard
|
|
- [ ] `npm run build` passes
|
|
|
|
**QA Scenarios**:
|
|
```
|
|
Scenario: Admin sees key sections with eye toggle
|
|
Tool: Playwright
|
|
Preconditions: Admin logged in, viewing device detail page for a device with keys
|
|
Steps:
|
|
1. Navigate to /devices/{id}
|
|
2. Assert "Private Key" label is visible
|
|
3. Assert key text is masked (contains "••••")
|
|
4. Click eye toggle button
|
|
5. Assert key text is now unmasked (alphanumeric base64 string)
|
|
Expected Result: Keys visible with toggle functionality
|
|
Evidence: .sisyphus/evidence/task-8-admin-keys.png
|
|
|
|
Scenario: Non-admin does NOT see key sections
|
|
Tool: Playwright
|
|
Preconditions: Non-admin user logged in, view device detail
|
|
Steps:
|
|
1. Navigate to /devices/{id}
|
|
2. Assert "Private Key" text is NOT present in DOM
|
|
Expected Result: Keys not visible to non-admin
|
|
Evidence: .sisyphus/evidence/task-8-nonadmin-no-keys.png
|
|
```
|
|
|
|
**Commit**: YES (group with T7-T10)
|
|
- Message: `feat(ui): add PrivateKey/PresharedKey display with eye toggle in DeviceDetail (admin only)`
|
|
- Files: `apps/dashboard-ui/src/views/DeviceDetail.vue`
|
|
|
|
- [x] 9. **DeviceDetail.vue: Regenerate Keys button**
|
|
|
|
**What to do**:
|
|
- Add a new function `handleRegenerateKeys()` that calls a new API function `regenerateDeviceKeys(id)`
|
|
- Create `regenerateDeviceKeys` in `devices.ts`:
|
|
```typescript
|
|
export const regenerateDeviceKeys = async (id: string): Promise<{private_key: string, public_key: string, preshared_key: string}> => {
|
|
const { data } = await api.post(`/devices/${id}/regenerate-keys`)
|
|
return data
|
|
}
|
|
```
|
|
- In DeviceDetail.vue, add a "🔄 Regenerate Keys" button styled like the existing "Regenerate Token" button (line 100-101)
|
|
- Place it near the key display section (below Private Key / Preshared Key display from T8)
|
|
- On click:
|
|
1. Show confirmation dialog: "This will invalidate the current WireGuard keys. All connected peers will need to update their config. Continue?"
|
|
2. If confirmed, call `regenerateDeviceKeys(id)`
|
|
3. On success, show the new keys in a success banner (similar to "New Registration Token Generated" at line 116-123)
|
|
4. Re-load device data
|
|
|
|
**Must NOT do**:
|
|
- Do NOT allow regenerating keys for non-admin users (button should be v-if="authStore.isAdmin")
|
|
- Do NOT modify the existing "Regenerate Token" button behavior
|
|
|
|
**Recommended Agent Profile**:
|
|
- **Category**: `visual-engineering`
|
|
- Reason: Vue template + API integration + UX flow
|
|
- **Skills**: none needed
|
|
|
|
**Parallelization**:
|
|
- **Can Run In Parallel**: YES
|
|
- **Parallel Group**: Wave 2 (with T7, T8, T10, T11)
|
|
- **Blocks**: None
|
|
- **Blocked By**: T4 (regenerate-keys endpoint), T7 (Device interface)
|
|
|
|
**References**:
|
|
- `apps/dashboard-ui/src/views/DeviceDetail.vue:100-101` — Existing "Regenerate Token" button pattern (bg-blue-600/20 text-blue-400)
|
|
- `apps/dashboard-ui/src/views/DeviceDetail.vue:116-123` — Success banner pattern
|
|
- `apps/dashboard-ui/src/views/DeviceDetail.vue:186-196` — `handleRegenerate` function pattern
|
|
- `apps/dashboard-ui/src/api/devices.ts:72-75` — `regenerateToken()` as pattern for new function
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] Admin sees "Regenerate Keys" button
|
|
- [ ] Non-admin does NOT see the button
|
|
- [ ] Clicking triggers confirmation dialog
|
|
- [ ] After confirmation, new keys appear in success banner
|
|
- [ ] Keys in the detail section are updated after regeneration
|
|
- [ ] `npm run build` passes
|
|
|
|
**QA Scenarios**:
|
|
```
|
|
Scenario: Regenerate keys via button
|
|
Tool: Playwright
|
|
Preconditions: Admin logged in, viewing device with keys
|
|
Steps:
|
|
1. Note current keys shown
|
|
2. Click "Regenerate Keys" button
|
|
3. Confirm dialog appears — click "OK"
|
|
4. Wait for success banner
|
|
5. Assert success banner shows new private_key, public_key, preshared_key
|
|
Expected Result: New keys generated and displayed
|
|
Evidence: .sisyphus/evidence/task-9-regenerate-btn.png
|
|
```
|
|
|
|
**Commit**: YES (group with T7-T10)
|
|
- Message: `feat(ui): add Regenerate Keys button and API in DeviceDetail`
|
|
- Files: `apps/dashboard-ui/src/views/DeviceDetail.vue`, `apps/dashboard-ui/src/api/devices.ts`
|
|
|
|
- [x] 10. **DeviceDetail.vue: Debug panel (status data section)**
|
|
|
|
**What to do**:
|
|
- Add a "🔍 Connection Status" section in DeviceDetail.vue (below Advanced Settings if admin)
|
|
- Only visible when admin (`v-if="authStore.isAdmin"`)
|
|
- Shows real-time data from the `/status` endpoint:
|
|
- **Status**: Online/Offline badge (existing, but also show last check time)
|
|
- **Last Handshake**: formatted timestamp (from `device.value.LastHandshake`)
|
|
- **Data Transferred**: Rx / Tx bytes (formatted: KB/MB/GB)
|
|
- **Public Key**: display-only (already exists from T8)
|
|
- **Internal IP**: display-only (already exists in the page)
|
|
- **Suspended**: yes/no badge
|
|
- Add a data refresh function that calls the new `getDeviceStatus(id)` API function
|
|
- Create `getDeviceStatus` in `devices.ts`:
|
|
```typescript
|
|
export const getDeviceStatus = async (id: string): Promise<{
|
|
is_active: boolean
|
|
last_handshake: string
|
|
rx_bytes: number
|
|
tx_bytes: number
|
|
public_key: string
|
|
internal_ip: string
|
|
is_suspended: boolean
|
|
name: string
|
|
}> => {
|
|
const { data } = await api.get(`/devices/${id}/status`)
|
|
return data
|
|
}
|
|
```
|
|
- Use a glassmorphism card section with the same styling
|
|
- Add a small "Refresh" button to manually refresh the status
|
|
- Optionally auto-refresh every 30s using `setInterval` (clean up in `onUnmounted`)
|
|
|
|
**Must NOT do**:
|
|
- Do NOT show to non-admin users
|
|
- Do NOT include private keys in this section (already in T8 section)
|
|
- Do NOT create complex charts or graphs — keep it simple text-based
|
|
|
|
**Recommended Agent Profile**:
|
|
- **Category**: `visual-engineering`
|
|
- Reason: Vue template + API integration + auto-refresh logic
|
|
- **Skills**: none needed
|
|
|
|
**Parallelization**:
|
|
- **Can Run In Parallel**: YES
|
|
- **Parallel Group**: Wave 2 (with T7-T9, T11)
|
|
- **Blocks**: None
|
|
- **Blocked By**: T5 (status endpoint), T7 (Device interface)
|
|
|
|
**References**:
|
|
- `apps/dashboard-ui/src/views/DeviceDetail.vue:41-81` — Advanced Settings accordion pattern
|
|
- `apps/dashboard-ui/src/views/DeviceDetail.vue:11-14` — Existing Online/Offline badge pattern
|
|
- `apps/dashboard-ui/src/views/DeviceDetail.vue:24-26` — Existing `LastHandshake` display
|
|
- `apps/dashboard-ui/src/api/devices.ts` — Add `getDeviceStatus` function
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] Admin sees "Connection Status" section with all fields
|
|
- [ ] Non-admin does NOT see the section
|
|
- [ ] Refresh button works and updates displayed data
|
|
- [ ] Rx/Tx bytes formatted nicely (e.g., "1.5 MB" not "1500000")
|
|
- [ ] `npm run build` passes
|
|
|
|
**QA Scenarios**:
|
|
```
|
|
Scenario: Admin sees status section with live data
|
|
Tool: Playwright
|
|
Preconditions: Admin logged in, device detail page
|
|
Steps:
|
|
1. Navigate to /devices/{id}
|
|
2. Assert "Connection Status" section is visible
|
|
3. Assert fields: Status, Last Handshake, Rx/Tx bytes, Public Key
|
|
4. Click "Refresh" button
|
|
Expected Result: Status section visible with formatted data
|
|
Evidence: .sisyphus/evidence/task-10-debug-panel.png
|
|
```
|
|
|
|
**Commit**: YES (group with T7-T10)
|
|
- Message: `feat(ui): add Connection Status debug panel and getDeviceStatus API`
|
|
- Files: `apps/dashboard-ui/src/views/DeviceDetail.vue`, `apps/dashboard-ui/src/api/devices.ts`
|
|
|
|
- [x] 11. **Servers.vue: Add PublicKey field to edit modal**
|
|
|
|
**What to do**:
|
|
- In `Servers.vue` edit modal (lines 162-262), add a Public Key input field in the "Network" section
|
|
- Place it after "Public Endpoint" and before "Listen Address"
|
|
- Use the same styling as other inputs (`w-full bg-black/50 border border-white/10 rounded p-2 text-white focus:border-cyan-500 focus:outline-none`)
|
|
- Add `publicKey: srv.PublicKey || ''` to the `editForm` initialization in `openEdit()` (around line 339-359)
|
|
- Add `public_key: editForm.value.publicKey || undefined` to the `updateServer` call in `handleEditSave()` (around line 452-470)
|
|
- This works with T6 backend changes
|
|
|
|
**Must NOT do**:
|
|
- Do NOT add Private Key field (server PrivateKey has `json:"-"` and must stay hidden)
|
|
- Do NOT add PresharedKey field here (not relevant for servers)
|
|
|
|
**Recommended Agent Profile**:
|
|
- **Category**: `visual-engineering`
|
|
- Reason: Vue template form field addition
|
|
- **Skills**: none needed
|
|
|
|
**Parallelization**:
|
|
- **Can Run In Parallel**: YES
|
|
- **Parallel Group**: Wave 2 (with T7-T10)
|
|
- **Blocks**: None
|
|
- **Blocked By**: T6 (backend public_key in UpdateServerRequest)
|
|
|
|
**References**:
|
|
- `apps/dashboard-ui/src/views/Servers.vue:162-262` — Edit modal template
|
|
- `apps/dashboard-ui/src/views/Servers.vue:322-368` — `openEdit()` function, editForm initialization
|
|
- `apps/dashboard-ui/src/views/Servers.vue:442-476` — `handleEditSave()` function
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] Edit modal shows Public Key input
|
|
- [ ] Public Key input is pre-filled with current server's PublicKey
|
|
- [ ] Changing Public Key and saving updates the server
|
|
- [ ] `npm run build` passes
|
|
|
|
**QA Scenarios**:
|
|
```
|
|
Scenario: Edit server public key in modal
|
|
Tool: Playwright
|
|
Preconditions: Admin logged in, nodes page
|
|
Steps:
|
|
1. Click "Edit" on a server
|
|
2. Assert "Public Key" input field is visible and pre-filled
|
|
3. Change the value to a new key
|
|
4. Click "Save"
|
|
5. Modal closes, reopen edit to verify
|
|
Expected Result: Public key updated and persists
|
|
Evidence: .sisyphus/evidence/task-11-server-publickey.png
|
|
```
|
|
|
|
**Commit**: YES
|
|
- Message: `feat(ui): add PublicKey field to server edit modal`
|
|
- Files: `apps/dashboard-ui/src/views/Servers.vue`
|
|
|
|
---
|
|
|
|
## Final Verification Wave (MANDATORY — after ALL implementation tasks)
|
|
|
|
- [x] F1. **Plan Compliance Audit** — `oracle`
|
|
Read the plan end-to-end. For each "Must Have": verify implementation exists (read file, curl endpoint, run command). For each "Must NOT Have": search codebase for forbidden patterns — reject with file:line if found. Check evidence files exist in .sisyphus/evidence/. Compare deliverables against plan.
|
|
Output: `Must Have [N/N] | Must NOT Have [N/N] | Tasks [N/N] | VERDICT: APPROVE/REJECT`
|
|
|
|
- [x] F2. **Code Quality Review** — `unspecified-high`
|
|
Run `tsc --noEmit` + `go build -tags dev ./...` + `go vet ./...`. Review all changed files for: `as any`/`@ts-ignore`, empty catches, console.log in prod, commented-out code, unused imports. Check AI slop: excessive comments, over-abstraction, generic names. Check no PrivateKey is logged anywhere (grep for `log.*PrivateKey`, `fmt.Print.*PrivateKey`).
|
|
Output: `Build [PASS/FAIL] | Vet [PASS/FAIL] | TSC [PASS/FAIL] | Files [N clean/N issues] | VERDICT`
|
|
|
|
- [x] F3. **Real Manual QA** — `unspecified-high` (+ `playwright` skill)
|
|
Start from clean state. Execute EVERY QA scenario from EVERY task — follow exact steps, capture evidence. Test cross-task integration (admin bypass + key display working together). Test edge cases: non-admin attempts, invalid keys, regenerate on unprovisioned device. Save to `.sisyphus/evidence/final-qa/`.
|
|
Output: `Scenarios [N/N pass] | Integration [N/N] | Edge Cases [N tested] | VERDICT`
|
|
|
|
- [x] F4. **Scope Fidelity Check** — `deep`
|
|
For each task: read "What to do", read actual diff (git log/diff). Verify 1:1 — everything in spec was built (no missing), nothing beyond spec was built (no creep). Check "Must NOT do" compliance. Detect cross-task contamination: Task N touching Task M's files. Flag unaccounted changes.
|
|
Output: `Tasks [N/N compliant] | Contamination [CLEAN/N issues] | Unaccounted [CLEAN/N files] | VERDICT`
|
|
|
|
---
|
|
|
|
## Commit Strategy
|
|
|
|
| Commit | Files | Message |
|
|
|--------|-------|---------|
|
|
| #1 (T1-T6) | `models/models.go`, `api/devices.go`, `api/servers.go`, `main.go`, `api/servers.ts` | `feat(api): expose device keys for admin, admin bypass, regenerate-keys endpoint, status endpoint, server public_key editing` |
|
|
| #2 (T7-T10) | `api/devices.ts`, `views/DeviceDetail.vue` | `feat(ui): add key display with eye toggle, regenerate keys button, and connection status debug panel` |
|
|
| #3 (T11) | `views/Servers.vue` | `feat(ui): add PublicKey field to server edit modal` |
|
|
|
|
---
|
|
|
|
## Success Criteria
|
|
|
|
### Verification Commands
|
|
```bash
|
|
# Backend build
|
|
cd apps/server-core && go build -tags dev ./...
|
|
|
|
# Frontend build
|
|
cd apps/dashboard-ui && npm run build
|
|
|
|
# Tests
|
|
cd apps/server-core && go test ./... -tags dev
|
|
```
|
|
|
|
### Final Checklist
|
|
- [x] Admin can see/edit keys on individual device GET
|
|
- [x] Non-admin cannot see any keys
|
|
- [x] Keys not exposed in List responses
|
|
- [x] `POST /devices/:id/regenerate-keys` works and calls SyncLocalPeers
|
|
- [x] `GET /devices/:id/status` returns real-time data
|
|
- [x] Server PublicKey editable in edit modal
|
|
- [x] Admin bypass works for all 6 handlers
|
|
- [x] No security regressions (keys not logged, not in lists)
|
|
- [x] `json:"-"` on WgServer.PrivateKey preserved
|