Files
Nexus-Guard-Suite/.sisyphus/plans/sharelink-presharedkey-fixes.md
T
datadunia 9c30fbd506
NexusGuard CI / server-core-test (push) Failing after 33s
NexusGuard CI / device-agent-test (push) Failing after 28s
NexusGuard CI / dashboard-ui-build (push) Failing after 29s
chore: add new plan docs, update submodule refs
2026-05-27 02:44:12 +07:00

1078 lines
39 KiB
Markdown

# Share Link Fix + PresharedKey Disable Feature
## TL;DR
> **Quick Summary**: Fix share link "always expired" bug (URL mismatch) + add peer-level PresharedKey disable toggle with server default setting + fix device-agent dropped PresharedKey.
>
> **Deliverables**:
> - Share link fix: `ShareConfig.vue` — use `fetch()` at root path, not Axios
> - PSK disable: Device model `DisablePresharedKey bool`, WgServer model `DefaultDisablePresharedKey bool` + all creation/generation paths + frontend toggles
> - Agent PSK: `WireGuardConfig.PresharedKey` field + UAPI emission
>
> **Estimated Effort**: Medium (8 files backend, 4 files frontend, 2 files agent)
> **Parallel Execution**: YES — 3 waves
> **Critical Path**: Model migration → Backend handlers → Frontend toggles → Agent structs → Final QA
---
## Context
### Original Request
1. **Bug**: Share config link selalu expired — recipient always sees "Link Expired or Invalid"
2. **Feature**: Peer dapat disable PresharedKey; server default disable PSK untuk peer baru
### Metis Analysis Key Findings
- **Share bug root cause**: URL mismatch — `ShareConfig.vue` calls `api.get('/share/:token')` which adds `/api/v1` prefix via Axios baseURL, but server route is at root `GET /share/:token`
- **Share bug secondary concern**: Server returns generic 404 for both expired AND invalid — error message doesn't distinguish
- **PSK disable scope**: 3 creation paths (peer, device, provisioning) + 3 config generators (peer config, share config, provisioning response) + agent UAPI
- **Precedence**: `device.DisablePresharedKey` > `server.DefaultDisablePresharedKey` > `false` (keep PSK)
- **Migration**: GORM AutoMigrate with `default:false` tags; existing rows get `false`
- **Agent**: Missing `PresharedKey` field in `WireGuardConfig` struct; needs to handle empty gracefully
### Current State
- **Share link**: `PeerConfigModal.vue` creates share at admin dashboard → generates URL like `/share/TOKEN` → recipient opens → SPA at `/share/TOKEN``ShareConfig.vue` calls `api.get('/share/TOKEN')` → WRONG URL → 404 → "Expired"
- **PresharedKey**: Always generated via `wgtypes.GenerateKey()` in all 3 creation paths. Always included in config text. No toggle to disable. Device-agent `WireGuardConfig` struct doesn't parse it.
- **WireGuard kernel**: `wgmanager_linux.go` already handles empty PresharedKey correctly (zero-value key = no PSK)
---
## Work Objectives
### Core Objective
1. Fix share link so recipients can actually retrieve the config
2. Allow admin to disable PresharedKey per-peer and set server-wide default
### Concrete Deliverables
- `apps/dashboard-ui/src/views/ShareConfig.vue` — use `fetch()` instead of Axios
- `apps/server-core/internal/models/models.go` — add `DisablePresharedKey` to Device, `DefaultDisablePresharedKey` to WgServer
- `apps/server-core/api/peers.go` — conditional PSK in CreatePeer + getDeviceConfig
- `apps/server-core/api/devices.go` — DisablePresharedKey in UpdateDeviceRequest + handler
- `apps/server-core/api/share.go` — conditional PSK in share config
- `apps/server-core/api/provisioning.go` — conditional PSK generation + payload
- `apps/dashboard-ui/src/components/AddPeerModal.vue` — PSK toggle
- `apps/dashboard-ui/src/views/DeviceDetail.vue` — PSK toggle
- `apps/dashboard-ui/src/views/Servers.vue` — default PSK setting
- `apps/device-agent/internal/client/provisioning.go` — add PresharedKey field
- `apps/device-agent/internal/tunnel/wireguard.go` — conditional preshared_key in UAPI
### Definition of Done
- [ ] Share link: curl `GET /share/{VALID_TOKEN}` → 200 with config_text
- [ ] Share link: curl `GET /share/{EXPIRED_TOKEN}` → 404 with error message
- [ ] Share link: Open share URL in browser → config displays, no "Expired" error
- [ ] PSK: Create device with PSK enabled → config_text includes `PresharedKey = <key>`
- [ ] PSK: Create device with PSK disabled → config_text has NO PresharedKey line
- [ ] PSK: Toggle PSK on existing device → updates correctly
- [ ] PSK: Server default disable PSK → new devices inherit
- [ ] PSK: Per-device toggle overrides server default
- [ ] Agent: Provision device with PSK → agent applies preshared_key via UAPI
- [ ] Agent: Provision device without PSK → agent does NOT emit preshared_key
- [ ] `npm run build` passes (dashboard-ui)
- [ ] `go build ./...` passes (server-core + device-agent)
### Must Have
- Share link URL mismatch fixed — recipient can download config
- Admin can disable PSK per-peer at creation/editing
- Server can set default "disable PSK" for all new peers on that server
- Device-agent receives and applies PresharedKey when present
- Device-agent handles missing/empty PresharedKey gracefully
### Must NOT Have (Guardrails)
- Do NOT refactor Axios `api` instance or `VITE_API_BASE_URL`
- Do NOT change server routing structure (`main.go` route definitions)
- Do NOT touch `shared/crypto/encryptor.go`
- Do NOT add PSK rotation, expiry, or custom/user-defined PSK values
- Do NOT add bulk tools for mass-updating existing devices
- Do NOT add new provisioning protocols or agent restart mechanisms
- Do NOT retroactively apply server default to existing devices
- Do NOT change how existing tunnels work mid-session
---
## Verification Strategy
> **ZERO HUMAN INTERVENTION** — ALL verification is agent-executed.
### Test Decision
- **Infrastructure exists**: Yes (Go tests + test DB + miniredis)
- **Automated tests**: Tests-after (add/update tests after implementation)
- **Framework**: Go `testing` + miniredis + httptest
- **Agent-Executed QA**: curl for API, fetch for share link, npm run build, go build
### QA Policy
Every task MUST include agent-executed QA scenarios.
- **API/Backend**: curl — specific HTTP methods, paths, request bodies, expected status codes + response fields
- **Frontend**: npm run build + grep for expected patterns
- **Agent**: grep for struct fields + build verification
- **Evidence**: Saved to `.sisyphus/evidence/task-{N}-{scenario-slug}.txt`
---
## Execution Strategy
### Parallel Execution Waves
```
Wave 1 (Foundation — 2 parallel tasks):
├── Task 1: Model migration (Device + WgServer fields) [quick]
├── Task 2: Share link fix (ShareConfig.vue) [quick]
Wave 2 (Backend — 4 parallel tasks, blocked by Task 1):
├── Task 3: peers.go — conditional PSK in CreatePeer + getDeviceConfig [quick]
├── Task 4: share.go — conditional PSK in share config [quick]
├── Task 5: provisioning.go — conditional PSK gen + payload [quick]
├── Task 6: devices.go — DisablePresharedKey in update handler [quick]
Wave 3 (Frontend + Agent — 4 parallel tasks):
├── Task 7: AddPeerModal.vue — PSK disable toggle [visual-engineering]
├── Task 8: DeviceDetail.vue — PSK disable toggle [visual-engineering]
├── Task 9: Servers.vue — default PSK disable setting [visual-engineering]
├── Task 10: Device-agent — PresharedKey struct + UAPI [quick]
Wave FINAL (Parallel reviews):
├── Task F1: Plan compliance audit (oracle)
├── Task F2: Code quality + build tests (unspecified-high)
├── Task F3: Real manual QA (unspecified-high)
├── Task F4: Scope fidelity check (deep)
```
---
## TODOs
- [x] 1. Database model migration — add DisablePresharedKey fields
**What to do**:
- In `apps/server-core/internal/models/models.go`:
1. Add to `Device` struct: `DisablePresharedKey bool \`json:"disable_preshared_key" gorm:"default:false"\``
2. Add to `WgServer` struct: `DefaultDisablePresharedKey bool \`json:"default_disable_preshared_key" gorm:"default:false"\``
- `main_dev.go` already runs GORM AutoMigrate with `-tags dev`. The `default:false` tag ensures existing rows get `false` (not NULL).
- **Note**: GORM `default:false` on bool works — existing rows will backfill correctly.
**Must NOT do**:
- Do NOT run `ALTER TABLE` manually — AutoMigrate handles this
- Do NOT touch `models_test.go` or `migrations.go`
**Recommended Agent Profile**:
- **Category**: `quick`
- Reason: 2 lines added, zero logic change, pure schema
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 1 (with Task 2)
- **Blocks**: Tasks 3-9, F1-F4
- **Blocked By**: None
**References**:
- `apps/server-core/internal/models/models.go:57-84` — Device struct (add field near line 80)
- `apps/server-core/internal/models/models.go:25-48` — WgServer struct (add field near line 45)
**Acceptance Criteria**:
- [ ] `go build ./...` passes
- [ ] Grep confirms new fields exist in model
**QA Scenarios**:
```
Scenario: Device model has DisablePresharedKey
Tool: Bash (grep)
Steps:
1. grep -n "DisablePresharedKey" apps/server-core/internal/models/models.go
Expected Result: Match found with gorm tag "default:false"
Evidence: .sisyphus/evidence/task-1-model-field.txt
Scenario: WgServer model has DefaultDisablePresharedKey
Tool: Bash (grep)
Steps:
1. grep -n "DefaultDisablePresharedKey" apps/server-core/internal/models/models.go
Expected Result: Match found with gorm tag "default:false"
Evidence: .sisyphus/evidence/task-1-server-default.txt
Scenario: Build passes
Tool: Bash
Steps:
1. cd apps/server-core && go build ./...
Expected Result: Exit 0
Evidence: .sisyphus/evidence/task-1-build.txt
```
**Commit**: YES (with Tasks 3-6)
- Message: `feat(model): add DisablePresharedKey flags to Device + WgServer`
---
- [x] 2. Fix share link URL mismatch in ShareConfig.vue
**What to do**:
- In `apps/dashboard-ui/src/views/ShareConfig.vue`:
1. Remove `import { api } from '../services/api'` (line 36)
2. Change `load()` function (lines 43-52) to use `fetch()` instead of `api.get()`:
```typescript
const load = async () => {
try {
const res = await fetch(`/share/${route.params.token}`)
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
const data = await res.json()
configText.value = data.config_text
} catch (err) {
error.value = true
} finally {
loading.value = false
}
}
```
- **Why**: The `api` Axios instance has `baseURL = VITE_API_BASE_URL` which includes `/api/v1` prefix. Share endpoint is at root `GET /share/:token`. Using `fetch()` hits the origin domain correctly.
**Must NOT do**:
- Do NOT import or use `api` (avoid JWT auth header on public route)
- Do NOT change router config or route definitions
- Do NOT modify server `main.go` routes
- Do NOT modify `api.ts` (Axios config)
**Recommended Agent Profile**:
- **Category**: `quick`
- Reason: Single file, ~15 lines change, straightforward
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 1 (with Task 1)
- **Blocks**: F1-F4
- **Blocked By**: None
**References**:
- `apps/dashboard-ui/src/views/ShareConfig.vue:33-52` — entire script section. Line 45 is the api.get call to replace.
- `apps/dashboard-ui/src/services/api.ts:4-5` — baseURL = VITE_API_BASE_URL (contains /api/v1). Confirms why api.get is wrong.
- `apps/server-core/main.go:268` — `r.GET("/share/:token", shareHandler.GetShare)` — root level, not under /api/v1.
**Acceptance Criteria**:
- [ ] `npm run build` passes
- [ ] Grep confirms no `api.get(` in ShareConfig.vue
- [ ] Grep confirms `fetch(` is used for share endpoint
**QA Scenarios**:
```
Scenario: ShareConfig no longer uses api.get
Tool: Bash (grep)
Steps:
1. grep -n "api\." apps/dashboard-ui/src/views/ShareConfig.vue
Expected Result: No matches (api not imported)
Evidence: .sisyphus/evidence/task-2-no-api.txt
Scenario: ShareConfig uses fetch instead
Tool: Bash (grep)
Steps:
1. grep -n "fetch(" apps/dashboard-ui/src/views/ShareConfig.vue
Expected Result: Match found in load() function
Evidence: .sisyphus/evidence/task-2-fetch.txt
Scenario: Build passes
Tool: Bash
Steps:
1. cd apps/dashboard-ui && npm run build
Expected Result: Exit 0
Evidence: .sisyphus/evidence/task-2-build.txt
```
**Commit**: YES (separate)
- Message: `fix(ui): use fetch() for share endpoint to fix URL mismatch /api/v1 prefix bug`
---
- [x] 3. Conditionally generate + output PresharedKey in peers.go
**What to do**:
Changes in `apps/server-core/api/peers.go`:
**3a. `CreatePeer()` — conditional PSK generation** (lines 76-80):
```go
// OLD:
psk, err := wgtypes.GenerateKey()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate preshared key"})
return
}
// NEW: Generate PSK only if not disabled
var pskStr string
if !req.DisablePresharedKey {
psk, err := wgtypes.GenerateKey()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate preshared key"})
return
}
pskStr = psk.String()
}
```
Also add `DisablePresharedKey` to `CreatePeerRequest` struct (line 30-34):
```go
type CreatePeerRequest struct {
Name string `json:"name" binding:"required"`
WgServerID string `json:"wg_server_id" binding:"required"`
AllowInternet bool `json:"allow_internet"`
DisablePresharedKey bool `json:"disable_preshared_key"`
}
```
And in the Device struct creation (line 94-107), change:
```go
// OLD:
PresharedKey: psk.String(),
// NEW:
DisablePresharedKey: req.DisablePresharedKey,
PresharedKey: pskStr,
```
**3b. `getDeviceConfig()` — conditional PSK in config text** (lines 226-246):
Change the configText format string to conditionally include PresharedKey:
```go
pskLine := fmt.Sprintf("PresharedKey = %s", device.PresharedKey)
if device.DisablePresharedKey || device.PresharedKey == "" {
pskLine = ""
}
```
Then change the format string:
```go
configText := fmt.Sprintf(`[Interface]
PrivateKey = %s
Address = %s/%s
DNS = %s%s
[Peer]
PublicKey = %s
%s
AllowedIPs = %s
Endpoint = %s%s`,
device.PrivateKey,
*device.InternalIP,
addrPrefix,
dns,
mtuLine,
wgServer.PublicKey,
pskLine,
allowedIPs,
wgServer.PublicEndpoint,
keepaliveLine,
)
```
Note: If `pskLine` is empty, the blank line with `%s` will produce `\n\n` — need to handle carefully. Better approach: build config text with a conditional block.
**Better approach**: Build config string with conditional parts:
```go
var pskBlock string
if device.PresharedKey != "" && !device.DisablePresharedKey {
pskBlock = fmt.Sprintf("PresharedKey = %s\n", device.PresharedKey)
}
configText := fmt.Sprintf(`[Interface]
PrivateKey = %s
Address = %s/%s
DNS = %s%s
[Peer]
PublicKey = %s
%sAllowedIPs = %s
Endpoint = %s%s`,
device.PrivateKey,
*device.InternalIP,
addrPrefix,
dns,
mtuLine,
wgServer.PublicKey,
pskBlock,
allowedIPs,
wgServer.PublicEndpoint,
keepaliveLine,
)
```
This way if pskBlock is empty, no blank line is inserted.
**3c. `UpdateConfig()` — handle PresharedKey validation** (lines 311-319):
Currently rejects PresharedKey changes. Keep this validation — users should use the toggle, not edit config manually.
**Must NOT do**:
- Do NOT touch `UpdateConfig()` validation logic
- Do NOT remove the PresharedKey change rejection (users should use toggle)
- Do NOT touch `wg_test.go`
**Parallelization**:
- **Can Run In Parallel**: YES (with Tasks 4, 5, 6)
- **Parallel Group**: Wave 2
- **Blocks**: Tasks 7-9, F1-F4
- **Blocked By**: Task 1
**References**:
- `apps/server-core/api/peers.go:30-34` — CreatePeerRequest struct
- `apps/server-core/api/peers.go:76-80` — PSK generation in CreatePeer
- `apps/server-core/api/peers.go:226-246` — getDeviceConfig config text generation
- `apps/server-core/api/peers.go:311-319` — UpdateConfig PresharedKey validation (keep as-is)
**Acceptance Criteria**:
- [ ] go build ./... passes
- [ ] CreatePeer with disable_preshared_key=false → PSK in response
- [ ] CreatePeer with disable_preshared_key=true → no PSK in response
- [ ] getDeviceConfig omits PresharedKey line when device has DisablePresharedKey=true
**QA Scenarios**:
```
Scenario: CreatePeerRequest has DisablePresharedKey field
Tool: Bash (grep)
Steps:
1. grep -n "DisablePresharedKey" apps/server-core/api/peers.go
Expected Result: 2+ matches (struct + usage)
Evidence: .sisyphus/evidence/task-3-request-struct.txt
Scenario: CreatePeer conditionally generates PSK
Tool: Bash (grep)
Steps:
1. grep -A5 "if !req.DisablePresharedKey" apps/server-core/api/peers.go
Expected Result: Shows GenerateKey() inside the conditional
Evidence: .sisyphus/evidence/task-3-conditional-gen.txt
Scenario: getDeviceConfig conditionally includes PSK
Tool: Bash (grep)
Steps:
1. grep -B2 -A3 "pskBlock\|PresharedKey =" apps/server-core/api/peers.go | head -20
Expected Result: Shows pskBlock built conditionally, used in format string
Evidence: .sisyphus/evidence/task-3-config-output.txt
Scenario: Build passes
Tool: Bash
Steps:
1. cd apps/server-core && go build ./...
Expected Result: Exit 0
Evidence: .sisyphus/evidence/task-3-build.txt
```
**Commit**: YES (with tasks 1, 4, 5, 6)
- Message: `feat(api): conditional PresharedKey generation and config output`
---
- [x] 4. Conditionally include PresharedKey in share config (share.go)
**What to do**:
- In `apps/server-core/api/share.go`, the `CreateShare()` function generates config text at lines 71-88.
- Replace the hardcoded `PresharedKey = %s` with conditional inclusion:
```go
var pskBlock string
if device.PresharedKey != "" && !device.DisablePresharedKey {
pskBlock = fmt.Sprintf("PresharedKey = %s\n", device.PresharedKey)
}
configText := fmt.Sprintf(`[Interface]
PrivateKey = %s
Address = %s/%s
DNS = 1.1.1.1
[Peer]
PublicKey = %s
%sAllowedIPs = %s
Endpoint = %s`,
device.PrivateKey,
*device.InternalIP,
addrPrefix,
wgServer.PublicKey,
pskBlock,
allowedIPs,
wgServer.PublicEndpoint,
)
```
**Must NOT do**:
- Do NOT change the existing config structure (Interface, Peer sections)
- Do NOT touch share_test.go
**Parallelization**:
- **Can Run In Parallel**: YES (with Tasks 3, 5, 6)
- **Parallel Group**: Wave 2
- **Blocks**: F1-F4
- **Blocked By**: Task 1
**References**:
- `apps/server-core/api/share.go:71-88` — current config generation (lines 78 = PresharedKey)
- `apps/server-core/api/peers.go:226-246` — same pattern to follow for conditional PSK block
**Acceptance Criteria**:
- [ ] go build ./... passes
- [ ] Share config omits PresharedKey when device has DisablePresharedKey=true
- [ ] Share config includes PresharedKey when device has DisablePresharedKey=false
**QA Scenarios**:
```
Scenario: Share config has conditional PSK
Tool: Bash (grep)
Steps:
1. grep -n "pskBlock\|DisablePresharedKey" apps/server-core/api/share.go
Expected Result: Matches showing conditional PSK logic
Evidence: .sisyphus/evidence/task-4-share-psk.txt
Scenario: Build passes
Tool: Bash
Steps:
1. cd apps/server-core && go build ./...
Expected Result: Exit 0
Evidence: .sisyphus/evidence/task-4-build.txt
```
**Commit**: YES (with tasks 1, 3, 5, 6)
- Message: `feat(api): conditional PresharedKey in share config`
---
- [x] 5. Conditionally generate + output PresharedKey in provisioning.go
**What to do**:
Changes in `apps/server-core/api/provisioning.go`:
**5a. Conditional PSK generation** (lines 105-110):
```go
// OLD:
psk, err := wgtypes.GenerateKey()
if err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate preshared key"})
return
}
...
device.PresharedKey = psk.String()
// NEW:
var pskStr string
if !device.DisablePresharedKey {
psk, err := wgtypes.GenerateKey()
if err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate preshared key"})
return
}
pskStr = psk.String()
}
...
device.PresharedKey = pskStr
```
**5b. Conditional PSK in ConfigPayload** (lines 138-145):
```go
payload := ConfigPayload{
PrivateKey: priv.String(),
InternalIP: *device.InternalIP,
ServerPub: wgServer.PublicKey,
Endpoint: wgServer.PublicEndpoint,
DNS: "1.1.1.1",
}
if pskStr != "" {
payload.PresharedKey = pskStr
}
```
**Note**: `ConfigPayload.PresharedKey` is a string — if not set, it serializes as `"preshared_key":""`. The device-agent already ignores unknown fields silently. This is fine.
**Must NOT do**:
- Do NOT touch the provisioning test file (tests need updating but out of scope for this task)
- Do NOT touch `ConfigPayload` struct (leave as-is, empty string = no PSK)
**Parallelization**:
- **Can Run In Parallel**: YES (with Tasks 3, 4, 6)
- **Parallel Group**: Wave 2
- **Blocks**: F1-F4
- **Blocked By**: Task 1
**References**:
- `apps/server-core/api/provisioning.go:37-44` — ConfigPayload struct (PresharedKey field already exists)
- `apps/server-core/api/provisioning.go:105-114` — PSK generation block
- `apps/server-core/api/provisioning.go:138-145` — ConfigPayload construction
**Acceptance Criteria**:
- [ ] go build ./... passes
- [ ] Provisioning with DisablePresharedKey=true → no PSK in ConfigPayload
- [ ] Provisioning with DisablePresharedKey=false → PSK in ConfigPayload
**QA Scenarios**:
```
Scenario: Provisioning conditionally generates PSK
Tool: Bash (grep)
Steps:
1. grep -B1 -A5 "if !device.DisablePresharedKey" apps/server-core/api/provisioning.go
Expected Result: Shows conditional block with GenerateKey()
Evidence: .sisyphus/evidence/task-5-conditional-gen.txt
Scenario: ConfigPayload conditionally includes PSK
Tool: Bash (grep)
Steps:
1. grep -A5 "if pskStr" apps/server-core/api/provisioning.go
Expected Result: Shows the conditional payload.PresharedKey assignment
Evidence: .sisyphus/evidence/task-5-payload.txt
Scenario: Build passes
Tool: Bash
Steps:
1. cd apps/server-core && go build ./...
Expected Result: Exit 0
Evidence: .sisyphus/evidence/task-5-build.txt
```
**Commit**: YES (with tasks 1, 3, 4, 6)
- Message: `feat(api): conditional PresharedKey in provisioning flow`
---
- [x] 6. Handle DisablePresharedKey in device Update handler (devices.go)
**What to do**:
Changes in `apps/server-core/api/devices.go`:
**6a. Add to UpdateDeviceRequest** (lines 166-174):
```go
type UpdateDeviceRequest struct {
Name string `json:"name"`
AllowInternet *bool `json:"allow_internet"`
EndpointAllowedIPs *string `json:"endpoint_allowed_ips"`
DNS *string `json:"dns"`
MTU *int `json:"mtu"`
PersistentKeepalive *int `json:"persistent_keepalive"`
Notes *string `json:"notes"`
DisablePresharedKey *bool `json:"disable_preshared_key"`
}
```
**6b. Handle in Update handler** (between lines 192-213):
```go
if req.DisablePresharedKey != nil {
updates["disable_preshared_key"] = *req.DisablePresharedKey
// If re-enabling PSK and no PSK exists, generate one
if !*req.DisablePresharedKey && device.PresharedKey == "" {
psk, err := wgtypes.GenerateKey()
if err == nil {
updates["preshared_key"] = psk.String()
}
}
// If disabling PSK, clear the stored key
if *req.DisablePresharedKey {
updates["preshared_key"] = ""
}
}
```
**Must NOT do**:
- Do NOT touch `Create()` — that's the peer creation path (handled by peers.go)
- Do NOT touch `List()`, `Get()`, `Delete()` handlers
- Do NOT generate PSK outside the re-enable scenario
**Parallelization**:
- **Can Run In Parallel**: YES (with Tasks 3, 4, 5)
- **Parallel Group**: Wave 2
- **Blocks**: Tasks 7-9, F1-F4
- **Blocked By**: Task 1
**References**:
- `apps/server-core/api/devices.go:166-174` — UpdateDeviceRequest struct
- `apps/server-core/api/devices.go:176-235` — Update handler (lines 192-213 are the updates section)
**Acceptance Criteria**:
- [ ] go build ./... passes
- [ ] PUT /devices/:id with disable_preshared_key=true → device.PresharedKey cleared
- [ ] PUT /devices/:id with disable_preshared_key=false on device with PSK → no change to PSK
- [ ] PUT /devices/:id with disable_preshared_key=false on device without PSK → new PSK generated
**QA Scenarios**:
```
Scenario: UpdateDeviceRequest has DisablePresharedKey
Tool: Bash (grep)
Steps:
1. grep -n "DisablePresharedKey" apps/server-core/api/devices.go
Expected Result: 2+ matches (struct + handler)
Evidence: .sisyphus/evidence/task-6-request-field.txt
Scenario: Update handler handles PSK toggle
Tool: Bash (grep)
Steps:
1. grep -B1 -A6 "if req.DisablePresharedKey" apps/server-core/api/devices.go
Expected Result: Shows the conditional block with PSK generation/clearing
Evidence: .sisyphus/evidence/task-6-toggle-logic.txt
Scenario: Build passes
Tool: Bash
Steps:
1. cd apps/server-core && go build ./...
Expected Result: Exit 0
Evidence: .sisyphus/evidence/task-6-build.txt
```
**Commit**: YES (with tasks 1, 3, 4, 5)
- Message: `feat(api): handle DisablePresharedKey in device update handler`
---
- [x] 7. Add PSK disable toggle to AddPeerModal.vue
**What to do**:
In `apps/dashboard-ui/src/components/AddPeerModal.vue`:
**7a. Add reactive state**:
```typescript
const disablePresharedKey = ref(false)
```
**7b. Add checkbox to template** (after the AllowInternet field):
```vue
<div class="flex items-center space-x-3 mt-3">
<input type="checkbox" id="disable-psk" v-model="disablePresharedKey"
class="w-4 h-4 rounded border-white/20 bg-white/5 text-cyan-600 focus:ring-cyan-500" />
<label for="disable-psk" class="text-sm text-gray-300">
Disable PresharedKey
<span class="text-xs text-gray-500 ml-1">(reduces connection security)</span>
</label>
</div>
```
**7c. Include in API request** (in the submit handler):
Pass `disable_preshared_key: disablePresharedKey.value` in the createPeer payload.
**Must NOT do**:
- Do NOT remove the existing AllowInternet toggle
- Do NOT add custom PSK input (only yes/no toggle)
- Do NOT change the existing API response handling
**Parallelization**:
- **Can Run In Parallel**: YES (with Tasks 8, 9, 10)
- **Parallel Group**: Wave 3
- **Blocks**: F1-F4
- **Blocked By**: Tasks 1, 3
**References**:
- `apps/dashboard-ui/src/components/AddPeerModal.vue` — full component (read first, add field + state + API passthrough)
- `apps/dashboard-ui/src/views/DeviceDetail.vue` — similar toggle pattern for reference
**Acceptance Criteria**:
- [ ] npm run build passes
- [ ] Toggle exists in template
- [ ] Toggle is passed in API request
**QA Scenarios**:
```
Scenario: Toggle exists in template
Tool: Bash (grep)
Steps:
1. grep -n "disable-psk\|DisablePresharedKey\|disable_preshared_key" apps/dashboard-ui/src/components/AddPeerModal.vue
Expected Result: 2+ matches (template + script)
Evidence: .sisyphus/evidence/task-7-toggle-exists.txt
Scenario: Build passes
Tool: Bash
Steps:
1. cd apps/dashboard-ui && npm run build
Expected Result: Exit 0
Evidence: .sisyphus/evidence/task-7-build.txt
```
**Commit**: YES (with tasks 8, 9)
- Message: `feat(ui): add PresharedKey disable toggle to peer creation form`
---
- [x] 8. Add PSK disable toggle to DeviceDetail.vue
**What to do**:
In `apps/dashboard-ui/src/views/DeviceDetail.vue`:
**8a. Add reactive state**:
```typescript
const disablePresharedKey = ref(false)
```
**8b. Load current state** from device data in `onMounted` or `watch`:
```typescript
disablePresharedKey.value = device.disable_preshared_key || false
```
**8c. Add toggle to the device edit form**:
```vue
<div class="flex items-center space-x-3 mt-3">
<input type="checkbox" id="device-disable-psk" v-model="disablePresharedKey"
class="w-4 h-4 rounded border-white/20 bg-white/5 text-cyan-600 focus:ring-cyan-500" />
<label for="device-disable-psk" class="text-sm text-gray-300">
Disable PresharedKey
<span class="text-xs text-gray-500 ml-1">(reduces connection security)</span>
</label>
</div>
```
**8d. Include in update payload**:
Pass `disable_preshared_key: disablePresharedKey.value` in the device update request.
**Must NOT do**:
- Do NOT change the existing edit form layout or other fields
- Do NOT add custom PSK input
**Parallelization**:
- **Can Run In Parallel**: YES (with Tasks 7, 9, 10)
- **Parallel Group**: Wave 3
- **Blocks**: F1-F4
- **Blocked By**: Tasks 1, 6
**References**:
- `apps/dashboard-ui/src/views/DeviceDetail.vue` — read expected edit form section
- `apps/server-core/api/devices.go:166-174` — UpdateDeviceRequest struct (for field name)
**Acceptance Criteria**:
- [ ] npm run build passes
- [ ] Toggle in device edit form, bound to device.disable_preshared_key
- [ ] Toggle sent in update API request
**QA Scenarios**:
```
Scenario: Toggle exists in DeviceDetail
Tool: Bash (grep)
Steps:
1. grep -n "disable_preshared_key\|DisablePresharedKey" apps/dashboard-ui/src/views/DeviceDetail.vue
Expected Result: 2+ matches (template + script)
Evidence: .sisyphus/evidence/task-8-toggle-exists.txt
Scenario: Build passes
Tool: Bash
Steps:
1. cd apps/dashboard-ui && npm run build
Expected Result: Exit 0
Evidence: .sisyphus/evidence/task-8-build.txt
```
**Commit**: YES (with tasks 7, 9)
- Message: `feat(ui): add PresharedKey disable toggle to device detail edit form`
---
- [x] 9. Add default PSK disable setting to Servers.vue
**What to do**:
In `apps/dashboard-ui/src/views/Servers.vue`:
**9a. Add reactive state** in the server edit form:
```typescript
const defaultDisablePresharedKey = ref(false)
```
**9b. Load from server data**:
```typescript
defaultDisablePresharedKey.value = editingServer.default_disable_preshared_key || false
```
**9c. Add toggle to server edit form** (near peer defaults section):
```vue
<div class="flex items-center space-x-3 mt-3">
<input type="checkbox" id="server-default-disable-psk" v-model="defaultDisablePresharedKey"
class="w-4 h-4 rounded border-white/20 bg-white/5 text-cyan-600 focus:ring-cyan-500" />
<label for="server-default-disable-psk" class="text-sm text-gray-300">
Disable PresharedKey for new peers by default
<span class="text-xs text-gray-500 ml-1">(can be overridden per-peer)</span>
</label>
</div>
```
**9d. Include in update payload**:
Pass `default_disable_preshared_key: defaultDisablePresharedKey.value` in the server update request.
**Must NOT do**:
- Do NOT change existing peer default fields (DNS, MTU, Keepalive, AllowedIPs)
- Do NOT add any other default settings
**Parallelization**:
- **Can Run In Parallel**: YES (with Tasks 7, 8, 10)
- **Parallel Group**: Wave 3
- **Blocks**: F1-F4
- **Blocked By**: Tasks 1, 3
**References**:
- `apps/dashboard-ui/src/views/Servers.vue` — read the server edit form (peer defaults section)
- `apps/server-core/internal/models/models.go:45` — WgServer.DefaultDisablePresharedKey field
**Acceptance Criteria**:
- [ ] npm run build passes
- [ ] Toggle exists in server edit form
- [ ] Toggle sent in server update request
**QA Scenarios**:
```
Scenario: Default toggle exists in Servers.vue
Tool: Bash (grep)
Steps:
1. grep -n "default_disable_preshared_key\|DefaultDisablePresharedKey" apps/dashboard-ui/src/views/Servers.vue
Expected Result: 2+ matches (template + script)
Evidence: .sisyphus/evidence/task-9-toggle-exists.txt
Scenario: Build passes
Tool: Bash
Steps:
1. cd apps/dashboard-ui && npm run build
Result: Exit 0
Evidence: .sisyphus/evidence/task-9-build.txt
```
**Commit**: YES (with tasks 7, 8)
- Message: `feat(ui): add default PresharedKey disable setting to server edit form`
---
- [x] 10. Fix device-agent PresharedKey gap (provisioning.go + wireguard.go)
**What to do**:
Changes in `apps/device-agent/`:
**10a. Add PresharedKey to WireGuardConfig struct** in `apps/device-agent/internal/client/provisioning.go`:
```go
type WireGuardConfig struct {
PrivateKey string `json:"private_key"`
PresharedKey string `json:"preshared_key"`
InternalIP string `json:"internal_ip"`
ServerPub string `json:"server_pub"`
Endpoint string `json:"endpoint"`
DNS string `json:"dns"`
}
```
**10b. Update ConvertToUAPI()** in `apps/device-agent/internal/tunnel/wireguard.go`:
Change from:
```go
return fmt.Sprintf(`private_key=%s
public_key=%s
endpoint=%s
allowed_ip=%s`, ...)
```
To:
```go
pskLine := ""
if presharedKey != "" {
pskLine = fmt.Sprintf("preshared_key=%s\n", presharedKey)
}
return fmt.Sprintf(`private_key=%s
public_key=%s
%sendpoint=%s
allowed_ip=%s`, privateKey, publicKey, pskLine, endpoint, allowedIP)
```
**10c. Update function signature** and callers:
- Change `ConvertToUAPI` signature to accept `presharedKey string`
- In `main.go` where it's called, pass `cfg.PresharedKey`
**Must NOT do**:
- Do NOT touch `shared/crypto/encryptor.go` (known debt, keep duplicated)
- Do NOT change the provisioning function flow (decrypt, parse, apply — stay the same)
**Parallelization**:
- **Can Run In Parallel**: YES (with Tasks 7, 8, 9)
- **Parallel Group**: Wave 3
- **Blocks**: F1-F4
- **Blocked By**: Task 1 (conceptual dependency — agent reads Device model changes but doesn't block on compile)
**References**:
- `apps/device-agent/internal/client/provisioning.go:28-34` — WireGuardConfig struct (add PresharedKey)
- `apps/device-agent/internal/tunnel/wireguard.go:83-97` — ConvertToUAPI function
- `apps/device-agent/main.go:38` — caller of ConvertToUAPI
- `apps/server-core/api/provisioning.go:138-145` — ConfigPayload struct (already has PresharedKey, confirmed)
**Acceptance Criteria**:
- [ ] go build ./... passes (device-agent)
- [ ] WireGuardConfig has PresharedKey field
- [ ] ConvertToUAPI conditionally emits preshared_key when non-empty
- [ ] ConvertToUAPI omits preshared_key when empty
**QA Scenarios**:
```
Scenario: WireGuardConfig has PresharedKey field
Tool: Bash (grep)
Steps:
1. grep -n "PresharedKey\|preshared_key" apps/device-agent/internal/client/provisioning.go
Expected Result: Shows PresharedKey string field with json tag
Evidence: .sisyphus/evidence/task-10-struct-field.txt
Scenario: ConvertToUAPI accepts presharedKey param
Tool: Bash (grep)
Steps:
1. grep -n "func ConvertToUAPI" apps/device-agent/internal/tunnel/wireguard.go
Expected Result: Shows function signature with presharedKey string param
Evidence: .sisyphus/evidence/task-10-uapi-sig.txt
Scenario: ConvertToUAPI conditionally emits preshared_key
Tool: Bash (grep)
Steps:
1. grep -B1 -A3 "if presharedKey" apps/device-agent/internal/tunnel/wireguard.go
Expected Result: Shows conditional pskLine building
Evidence: .sisyphus/evidence/task-10-uapi-conditional.txt
Scenario: Build passes
Tool: Bash
Steps:
1. cd apps/device-agent && go build ./...
Expected Result: Exit 0
Evidence: .sisyphus/evidence/task-10-build.txt
```
**Commit**: YES (separate, agent is independent submodule)
- Message: `feat(agent): add PresharedKey support to WireGuard config and UAPI`
---
## Final Verification Wave
- [x] F1. **Plan Compliance Audit** — `oracle`
Read the plan end-to-end. For each Must Have: verify implementation exists. For each Must NOT Have: search codebase for forbidden patterns (refactored Axios, crypto/encryptor.go changes, etc.). 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 + Build Tests** — `unspecified-high`
Run `go build ./...` on ALL 3 submodules (server-core, dashboard-ui, device-agent). Run `npm run build` on dashboard-ui. Check for: unused imports, commented-out code, AI slop (excessive comments, over-abstraction).
Output: `Build [PASS/FAIL] | Lint [PASS/FAIL] | Files [N clean/N issues] | VERDICT`
- [x] F3. **Real Manual QA** — `unspecified-high`
Execute these end-to-end scenarios:
1. **Share link fix**: Call POST /api/v1/devices/:id/share → get token → curl GET /share/TOKEN → verify 200 with config_text
2. **PSK creation**: POST /api/v1/peers with disable_preshared_key=false → response has PresharedKey. POST with true → no PresharedKey
3. **PSK config**: GET /devices/:id/config for both PSK states → verify correct format
4. **PSK toggle**: PUT /devices/:id with disable_preshared_key=true → GET device → verify field changed
5. **Server default**: Update server setting → create peer on that server → verify inheritance
6. **Share config PSK**: Create share for device with PSK disabled → verify share config omits PSK
7. **Agent build**: go build ./... in device-agent → verify WireGuardConfig has PresharedKey field
Save evidence to `.sisyphus/evidence/final-qa/`.
Output: `Scenarios [N/N pass] | Integration [N/N] | VERDICT`
- [x] F4. **Scope Fidelity Check** — `deep`
Read the actual diffs for all changed files. Verify 1:1 with spec. No missing, no scope creep. Check Must NOT do compliance. Detect cross-task contamination.
Output: `Tasks [N/N compliant] | Contamination [CLEAN/N issues] | VERDICT`
---
## Commit Strategy
- **Task 2** (standalone): `fix(ui): use fetch() for share endpoint to fix /api/v1 prefix bug`
- **Task 1 + 3-6**: `feat: conditional PresharedKey across model, creation, config, share, provisioning, device update`
- **Tasks 7-9**: `feat(ui): add PresharedKey disable toggles to AddPeerModal, DeviceDetail, Servers`
- **Task 10** (standalone agent): `feat(agent): add PresharedKey support to WireGuard config and UAPI`
- **Parent repo**: `feat: submodule refs for share link fix + PresharedKey disable feature`
---
## Success Criteria
### Verification Commands
```bash
cd apps/server-core && go build ./...
cd apps/dashboard-ui && npm run build
cd apps/device-agent && go build ./...
curl -v http://localhost:8080/share/{VALID_TOKEN}
curl -v http://localhost:8080/api/v1/peers -X POST -d '{"name":"test","wg_server_id":"...","disable_preshared_key":true}'
```
### Final Checklist
- [ ] All "Must Have" present
- [ ] All "Must NOT Have" absent
- [ ] All builds pass
- [ ] Share link: valid token returns 200 with config
- [ ] Share link: expired token returns 404 with error
- [ ] PSK disabled: no PresharedKey in any generated config
- [ ] PSK enabled (default): PresharedKey present in all generated configs
- [ ] Agent: builds with PresharedKey field in WireGuardConfig
- [ ] Agent: ConvertToUAPI conditionally emits preshared_key