fix htmx refactor route mismatches and remaining TODO items

- wireguard: POST /peers with JSON encoding (was /add-peer)
- rules: delete by rule_id in URL path (was JSON body); pass rule objects with id from server; add hx-disable to initial render
- nat: port forward delete uses URL path params to match blueprint
- nat: masquerade toggle uses native hx-post/hx-vals (was inline fetch)
- app.js renderers updated to use URL path deletes for rules and forwards
- remove TODO.md
This commit is contained in:
2026-05-17 01:15:52 +00:00
parent 0e7090a2cb
commit 37039351be
26 changed files with 1737 additions and 848 deletions
+277 -204
View File
@@ -34,12 +34,79 @@ Error responses carry one of the following HTTP status codes:
| `404` | Not found — the requested resource does not exist |
| `500` | Internal server error — unexpected failure in the backend |
### Route Patterns
Resource identification uses **path parameters** whenever possible. Exceptions occur only when the identifier is inherently long (e.g., a rich rule string), in which case the body carries the identifier.
---
## Firewall API
Endpoints prefixed with `/api/firewall/...`. Interact with firewalld for zone management, rich rules, NAT, and masquerade.
### Declarative Config
The firewall supports a two-step declarative workflow: save config to `config/firewall/config.json`, then apply it to live firewalld. The config tracks `rich_rules` and `forward_ports` with auto-generated `id` fields.
#### Get Config
```
GET /api/firewall/config
```
Return the current declarative firewall config.
**Response:** `data` contains the config object with a `zones` mapping.
#### Save Config
```
POST /api/firewall/config
```
Replace the declarative config. Returns pending changes summary.
**Request Body:** Request body must contain `zones`.
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `config_saved` | `boolean` | Always `true` |
| `pending` | `[object, ...]` | List of pending changes |
| `needs_apply` | `boolean` | Whether changes need to be applied |
| `unmanaged_zones` | `object` | Zones active on system but not in config |
#### Apply Config
```
POST /api/firewall/config/apply
```
Apply the declarative config to live firewalld. Applies targets, services, interfaces, masquerade, rich rules, and forward ports.
**Response:** `data` contains `applied_zones` list and backup path.
#### Check Pending Changes
```
GET /api/firewall/config/pending
```
Compare declarative config against live firewalld state. Returns diff for interfaces, services, targets, masquerade, rich rules, and forward ports.
**Response:** Same structure as POST /config response.
#### Partial Update Config
```
PATCH /api/firewall/config
```
Deep-merge the provided fields into the existing config.
**Response:** `data` is `null` on success.
### Zone Management
#### List All Zones
@@ -76,8 +143,8 @@ Return detailed configuration for a single zone.
| `services` | `[string, ...]` | Services allowed through the zone |
| `ports` | `[string, ...]` | Explicit port rules (format: `"443/tcp"`) |
| `masquerade` | `boolean` | Whether masquerade (NAT) is enabled |
| `forward_ports` | `[{port: number, proto: string, toaddr: string, toport: number}, ...]` | Port forward rules |
| `rich_rules` | `[string, ...]` | Rich rule definitions |
| `forward_ports` | `[{port, proto, toaddr, toport}, ...]` | Port forward rules |
| `rich_rules` | `[{rule, id}, ...]` | Rich rule definitions with IDs |
Returns HTTP `404` if the zone does not exist.
@@ -135,7 +202,7 @@ Replace all interfaces assigned to the zone with the provided list.
| Field | Type | Description |
|-------|------|-------------|
| `zone` | `string` | Zone name |
| `interfaces` | `[string, ...]` | List of interface names now assigned to the zone |
| `interfaces` | `[string, ...]` | List of interface names now assigned |
---
@@ -158,9 +225,9 @@ Replace all services allowed in the zone with the provided list.
| Field | Type | Description |
|-------|------|-------------|
| `zone` | `string` | Zone name |
| `services` | `[string, ...]` | List of services now allowed in the zone |
| `services` | `[string, ...]` | List of services now allowed |
### Firewall Rules
### Rich Rules
#### Add Rich Rule
@@ -168,7 +235,7 @@ Replace all services allowed in the zone with the provided list.
POST /api/firewall/rich-rules
```
Add a firewalld rich rule to a zone.
Add a firewalld rich rule to a zone. The rule is persisted to the declarative config with an auto-generated `id`.
**Request Body:**
@@ -182,6 +249,7 @@ Add a firewalld rich rule to a zone.
| Field | Type | Description |
|-------|------|-------------|
| `zone` | `string` | Zone name |
| `id` | `string` | 8-character unique ID |
| `rule` | `string` | Full rich rule string |
---
@@ -189,24 +257,19 @@ Add a firewalld rich rule to a zone.
#### Remove Rich Rule
```
DELETE /api/firewall/rich-rules
DELETE /api/firewall/rich-rules/<zone>/<id>
```
Remove an existing rich rule from a zone. The `rule` string must match exactly.
**Request Body:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `zone` | `string` | Yes | Zone the rule belongs to |
| `rule` | `string` | Yes | Exact rich rule string to remove |
Remove a rich rule by zone and auto-generated ID. (The rule string itself is too long for a URL path.)
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `zone` | `string` | Zone name |
| `rule` | `string` | Exact rich rule string that was removed |
| `id` | `string` | ID of the removed rule |
Returns HTTP `404` if the rule ID is not found.
---
@@ -216,15 +279,64 @@ Remove an existing rich rule from a zone. The `rule` string must match exactly.
GET /api/firewall/rich-rules/<zone>
```
Return all rich rules for the specified zone.
Return all rich rules for the specified zone, each with an `id` and `rule` string.
**Response:**
| Field | Type | Description |
|-------|------|-------------|
| `data` | `[string, ...]` | Rich rule strings |
| `data` | `[{id, rule}, ...]` | Rich rules with IDs |
### NAT
### Port Forwarding
#### Add Port Forward
```
POST /api/firewall/forward-port
```
Add a port forwarding rule to a zone. The rule is persisted to the declarative config with an auto-generated `id`.
**Request Body:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `zone` | `string` | Yes | Zone to add the rule to |
| `port` | `number` | Yes | External port |
| `proto` | `string` | Yes | Protocol (`"tcp"` or `"udp"`) |
| `toaddr` | `string` | No | Internal destination address |
| `toport` | `number` | No | Internal destination port |
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `zone` | `string` | Zone name |
| `id` | `string` | 8-character unique ID |
| `port` | `number` | External port |
| `proto` | `string` | Protocol (`"tcp"` or `"udp"`) |
---
#### Remove Port Forward
```
DELETE /api/firewall/forward-port/<zone>/<port>/<proto>
```
Remove a port forwarding rule. Zone, port, and protocol are all path parameters.
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `zone` | `string` | Zone name |
| `port` | `number` | External port |
| `proto` | `string` | Protocol (`"tcp"` or `"udp"`) |
Returns HTTP `404` if the forward port is not found.
### Masquerade (NAT)
#### Enable / Disable Masquerade
@@ -246,63 +358,7 @@ Toggle masquerade (source NAT) for a zone.
| Field | Type | Description |
|-------|------|-------------|
| `zone` | `string` | Zone name |
| `masquerade` | `boolean` | Whether masquerade is now enabled for the zone |
---
#### Add Port Forward
```
POST /api/firewall/forward-port
```
Add a port forwarding rule to a zone.
**Request Body:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `zone` | `string` | Yes | Zone to add the rule to |
| `port` | `number` | Yes | External port |
| `proto` | `string` | Yes | Protocol (`"tcp"` or `"udp"`) |
| `toaddr` | `string` | No | Internal destination address |
| `toport` | `number` | No | Internal destination port |
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `zone` | `string` | Zone name |
| `port` | `number` | External port |
| `proto` | `string` | Protocol (`"tcp"` or `"udp"`) |
---
#### Remove Port Forward
```
DELETE /api/firewall/forward-port
```
Remove a port forwarding rule. The body must match the original rule exactly.
**Request Body:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `zone` | `string` | Yes | Zone the rule belongs to |
| `port` | `number` | Yes | External port |
| `proto` | `string` | Yes | Protocol (`"tcp"` or `"udp"`) |
| `toaddr` | `string` | No | Internal destination address |
| `toport` | `number` | No | Internal destination port |
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `zone` | `string` | Zone name |
| `port` | `number` | External port |
| `proto` | `string` | Protocol (`"tcp"` or `"udp"`) |
| `masquerade` | `boolean` | Whether masquerade is now enabled |
### Info
@@ -406,24 +462,74 @@ Write the in-memory configuration to `/etc/dnsmasq.d/vacuum-wall.conf` and reloa
**Response:** `data` is `null` on success.
### Leases
### Status
#### Get Live Leases
#### Get Service Status
```
GET /api/dhcp/leases
GET /api/dhcp/status
```
Return the current DHCP lease table from dnsmasq.
Return the current service status, config summary, and active lease count.
**Response:**
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `data` | `[object, ...]` | Array of lease objects |
| `service_active` | `boolean` | Whether dnsmasq is running |
| `config_file_exists` | `boolean` | Whether config file exists on disk |
| `config_in_sync` | `boolean` | Whether disk config matches expected |
| `dhcp_ranges` | `number` | Number of DHCP ranges |
| `static_leases` | `number` | Number of static leases |
| `custom_dns_records` | `number` | Number of custom DNS records |
| `upstreams` | `[string, ...]` | Upstream DNS servers |
| `domain` | `string` | Local DNS domain |
| `active_leases` | `number` | Number of active leases |
| `leases` | `[object, ...]` | Active lease objects |
### DHCP Ranges
#### Add Range
```
POST /api/dhcp/ranges
```
Add or replace the DHCP range for a given interface.
**Request Body:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `interface` | `string` | No | Interface name (empty = all interfaces) |
| `start` | `string` | Yes | Start of IP range |
| `end` | `string` | Yes | End of IP range |
| `lease_time` | `string` | No | Lease duration; defaults to `"12h"` |
**Response:** `data` is `null` on success.
---
#### Remove Range
```
DELETE /api/dhcp/ranges
```
Remove a DHCP range. Body contains identifying fields.
**Request Body:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `interface` | `string` | Yes | Interface name |
| `start` | `string` | Yes | Start of IP range |
| `end` | `string` | Yes | End of IP range |
**Response:** `data` is `null` on success.
### Static Leases
#### Add Static Lease
```
@@ -446,28 +552,38 @@ Add a static (reserved) DHCP lease.
|-------|------|-------------|
| `mac` | `string` | MAC address |
| `ip` | `string` | Reserved IP address |
| `hostname` | `string` | Hostname for the reservation |
| `hostname` | `string` | Hostname |
---
#### Remove Static Lease
```
DELETE /api/dhcp/static-lease?mac=aa:bb:cc:dd:ee:ff
DELETE /api/dhcp/static-lease/<mac>
```
Remove a previously configured static lease.
**Query Parameters:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `mac` | `string` | Yes | MAC address of the lease to remove |
Remove a static lease by MAC address.
**Response:** `data` is `null` on success.
Returns HTTP `404` if no matching lease is found.
### Live Leases
#### Get Live Leases
```
GET /api/dhcp/leases
```
Return the current DHCP lease table from dnsmasq.
**Response:**
| Field | Type | Description |
|-------|------|-------------|
| `data` | `[object, ...]` | Array of lease objects |
### DNS Records
#### Add DNS Record
@@ -484,6 +600,7 @@ Add a custom DNS A record served by dnsmasq.
|-------|------|----------|-------------|
| `name` | `string` | Yes | Fully qualified domain name |
| `address` | `string` | Yes | IP address to resolve to |
| `hostname` | `string` | No | Short hostname |
**Response (`data`):**
@@ -498,16 +615,10 @@ Add a custom DNS A record served by dnsmasq.
#### Remove DNS Record
```
DELETE /api/dhcp/dns-record?name=nas.lan
DELETE /api/dhcp/dns-record/<name>
```
Remove a custom DNS record.
**Query Parameters:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | `string` | Yes | Fully qualified domain name to remove |
Remove a custom DNS record by domain name.
**Response:** `data` is `null` on success.
@@ -553,6 +664,8 @@ Add a new reverse proxy domain.
| `backend_host` | `string` | Yes | Backend server IP or hostname |
| `backend_port` | `number` | Yes | Backend server port |
| `backend_proto` | `string` | No | Backend protocol (`"http"` or `"https"`); defaults to `"http"` |
| `cert` | `string` | No | Certificate domain |
| `extra_headers` | `object` | No | Extra proxy headers |
**Response (`data`):**
@@ -572,14 +685,7 @@ GET /api/proxy/domains/<domain>
Return the configuration for a single proxy domain.
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `domain` | `string` | Domain name |
| `backend_host` | `string` | Backend server address |
| `backend_port` | `number` | Backend server port |
| `backend_proto` | `string` | Backend protocol |
**Response (`data`):** Domain name plus backend configuration fields.
Returns HTTP `404` if the domain is not configured.
@@ -591,15 +697,9 @@ Returns HTTP `404` if the domain is not configured.
PUT /api/proxy/domains/<domain>
```
Update one or more fields of an existing domain entry. Only the fields present in the body are modified.
Update one or more fields of an existing domain entry. Only fields present in the body are modified.
**Request Body:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `backend_host` | `string` | No | Backend server IP or hostname |
| `backend_port` | `number` | No | Backend server port |
| `backend_proto` | `string` | No | Backend protocol |
**Request Body:** Any subset of (`backend_host`, `backend_port`, `backend_proto`, `cert`, `extra_headers`).
**Response (`data`):**
@@ -649,15 +749,17 @@ Returns HTTP `500` if nginx config generation fails or the reload fails.
POST /api/proxy/test
```
Run `nginx -t` against the generated configuration without reloading. Useful for validating changes before applying.
Run `nginx -t` against the generated configuration without reloading.
**Response:**
**Response (valid):**
| Field | Type | Description |
|-------|------|-------------|
| `data.valid` | `boolean` | Whether the configuration syntax is valid |
| `data.valid` | `boolean` | Always `true` |
| `data.output` | `string` | Raw nginx test output |
**Error (invalid):** HTTP `400` with standard `{"ok": false, "error": "<nginx output>"}` response.
### Management
#### Configure Management WebUI Proxy
@@ -675,13 +777,11 @@ Configure the nginx proxy block for the management WebUI itself, including optio
| `domain` | `string` | Yes | Management domain (e.g., `"myhost.local"`) |
| `flask_host` | `string` | No | Flask app bind host; defaults to `"127.0.0.1"` |
| `flask_port` | `number` | No | Flask app bind port; defaults to `9090` |
| `auth_user` | `string` | No | Username for basic auth. An `.htpasswd` entry is created when this field is present. |
| `auth_pass` | `string` | No | Password for basic auth. Used together with `auth_user`. |
| `auth_user` | `string` | No | Username for basic auth |
| `auth_pass` | `string` | No | Password for basic auth |
**Response:** `data` is `null` on success.
If `auth_user` and `auth_pass` are provided, the endpoint creates or updates the corresponding `.htpasswd` file entry.
---
## Certificate API
@@ -704,15 +804,7 @@ Return all managed certificates with metadata.
|-------|------|-------------|
| `data` | `[object, ...]` | Array of certificate objects |
Each certificate object:
| Field | Type | Description |
|-------|------|-------------|
| `domain` | `string` | Domain the certificate covers |
| `expires_at` | `string` | Expiration date (ISO 8601) |
| `days_until_expiry` | `number` | Remaining days until expiration |
| `cert_path` | `string` | Path to the certificate file |
| `key_path` | `string` | Path to the private key file |
Each certificate object contains `domain`, `expires_at`, `days_until_expiry`, `cert_path`, `key_path`.
---
@@ -724,15 +816,7 @@ GET /api/certs/<domain>
Return details for a single certificate.
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `domain` | `string` | Domain |
| `expires_at` | `string` | Expiration date (ISO 8601) |
| `days_until_expiry` | `number` | Remaining days |
| `cert_path` | `string` | Certificate file path |
| `key_path` | `string` | Private key file path |
**Response (`data`):** Fields: `domain`, `expires_at`, `days_until_expiry`, `cert_path`, `key_path`.
Returns HTTP `404` if no certificate is found for the domain.
@@ -755,7 +839,7 @@ Request a new certificate for a domain.
**Response:** `data` is `null` on success.
Returns HTTP `400` if the domain is missing or the request is malformed. Returns HTTP `500` if the ACME challenge or certificate issuance fails.
Returns HTTP `400` if the domain is missing. Returns HTTP `500` if issuance fails.
---
@@ -765,7 +849,7 @@ Returns HTTP `400` if the domain is missing or the request is malformed. Returns
POST /api/certs/<domain>/renew
```
Force-renew an existing certificate, regardless of its current expiry status.
Force-renew an existing certificate.
**Response:** `data` is `null` on success.
@@ -793,7 +877,7 @@ Returns HTTP `404` if the certificate is not found.
POST /api/certs/email
```
Set or update the ACME account contact email (used by the CA for expiration and security notices).
Set or update the ACME account contact email.
**Request Body:**
@@ -801,11 +885,7 @@ Set or update the ACME account contact email (used by the CA for expiration and
|-------|------|----------|-------------|
| `email` | `string` | Yes | Contact email address |
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `email` | `string` | Contact email address |
**Response (`data`):** Returns the set `email` field.
---
@@ -821,13 +901,13 @@ Endpoints prefixed with `/api/wireguard/...`. Manage the WireGuard VPN server, p
GET /api/wireguard/config
```
Return the current WireGuard server configuration. The `private_key` field is stripped from the response.
Return the current WireGuard server configuration. The `private_key` field is stripped.
**Response:**
| Field | Type | Description |
|-------|------|-------------|
| `data` | `object` | Full WireGuard configuration dictionary (`private_key` omitted) |
| `data` | `object` | WireGuard config (`private_key` omitted) |
---
@@ -845,11 +925,7 @@ Replace the entire WireGuard configuration. The `private_key` field is stripped
|-------|------|----------|-------------|
| *(entire body)* | `object` | Yes | Complete WireGuard configuration object |
**Response:**
| Field | Type | Description |
|-------|------|-------------|
| `data` | `object` | Updated configuration (`private_key` omitted) |
**Response:** `data` contains the updated configuration (`private_key` omitted).
### Tunnel Control
@@ -859,11 +935,21 @@ Replace the entire WireGuard configuration. The `private_key` field is stripped
POST /api/wireguard/apply
```
Write the current configuration to `wg0.conf` on disk and bring the WireGuard tunnel up.
Write the current configuration to `wg0.conf` and bring the tunnel up.
**Response:** `data` is `null` on success.
Returns HTTP `500` if config write or interface bring-up fails.
---
#### Start Tunnel
```
POST /api/wireguard/up
```
Alias for `/api/wireguard/apply` — write config and bring the tunnel up.
**Response:** `data` is `null` on success.
---
@@ -885,15 +971,15 @@ Bring down the WireGuard tunnel interface (`wg0`).
GET /api/wireguard/status
```
Return live tunnel state, including interface metrics and per-peer connection statistics.
Return live tunnel state with interface metrics and per-peer connection statistics.
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `up` | `boolean` | Whether the tunnel interface is up |
| `interface` | `object` | Interface info (listen port, public key, etc.) |
| `peers` | `[object, ...]` | Per-peer connection stats (handshake time, transfer bytes, endpoint, etc.) |
| `interface` | `object` | Interface info (listen port, public key) |
| `peers` | `[object, ...]` | Per-peer stats (handshake, bytes, endpoint) |
---
@@ -903,19 +989,35 @@ Return live tunnel state, including interface metrics and per-peer connection st
POST /api/wireguard/initialize
```
Perform first-time setup: generate a server key pair, write an initial configuration, and prepare for peer enrollment. This endpoint is idempotent — calling it multiple times has no additional effect.
First-time setup: generate server key pair, write initial config. Idempotent.
**Response:** `data` is `null` on success.
### Peer Management
#### List Peers
```
GET /api/wireguard/peers
```
Return all configured peers. Private keys are stripped.
**Response:**
| Field | Type | Description |
|-------|------|-------------|
| `data` | `[object, ...]` | Peer objects (private keys omitted) |
---
#### Add Peer
```
POST /api/wireguard/add-peer
POST /api/wireguard/peers
```
Add a new WireGuard peer. A key pair is auto-generated for the peer. The response includes peer details with the private key stripped.
Add a new WireGuard peer. A key pair is auto-generated. Private key stripped from response.
**Request Body:**
@@ -924,33 +1026,20 @@ Add a new WireGuard peer. A key pair is auto-generated for the peer. The respons
| `name` | `string` | Yes | Peer identifier name |
| `endpoint` | `string` | No | Allowed endpoint address (`"ip:port"`) |
| `allowed_ips` | `[string, ...]` | No | Allowed IPs; defaults to `["0.0.0.0/0"]` |
| `persistent_keepalive` | `number` | No | Persistent keepalive interval in seconds |
| `persistent_keepalive` | `number` | No | Persistent keepalive interval (seconds) |
| `preshared_key` | `string` | No | Preshared key |
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `name` | `string` | Peer name |
| `public_key` | `string` | Peer's public key |
| `allowed_ips` | `[string, ...]` | Allowed IPs |
| `endpoint` | `string` | Allowed endpoint |
| `persistent_keepalive` | `number` | Keepalive interval |
**Response (`data`):** Peer object with `name`, `public_key`, `allowed_ips`, etc. (no `private_key`).
---
#### Remove Peer
```
DELETE /api/wireguard/remove-peer?name=alice
DELETE /api/wireguard/peers/<name>
```
Remove a configured peer.
**Query Parameters:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | `string` | Yes | Peer name to remove |
Remove a configured peer by name.
**Response (`data`):**
@@ -962,35 +1051,19 @@ Returns HTTP `404` if the peer is not found.
---
#### List Peers
```
GET /api/wireguard/peers
```
Return all configured peers. Private keys are stripped from the response.
**Response:**
| Field | Type | Description |
|-------|------|-------------|
| `data` | `[object, ...]` | Array of peer objects (private keys omitted) |
---
#### Peer Connection Status
```
GET /api/wireguard/peer-status
```
Return live per-peer connection status from `wg show`, including last handshake time, transfer bytes, and current endpoint.
Return live per-peer connection status from `wg show`.
**Response:**
| Field | Type | Description |
|-------|------|-------------|
| `data` | `[object, ...]` | Array of live peer status objects |
| `data` | `[object, ...]` | Live peer status (handshake time, bytes, endpoint) |
### Client Configuration
@@ -1000,21 +1073,21 @@ Return live per-peer connection status from `wg show`, including last handshake
POST /api/wireguard/generate-client
```
Generate a complete WireGuard client configuration file for provisioning a device. The returned config includes the peer's private key for the client to use.
Generate a complete WireGuard client configuration file. The returned config includes the peer's private key for provisioning.
**Request Body:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | `string` | Yes | Peer name to generate config for |
| `server_endpoint` | `string` | Yes | Server public address (`"ip:port"`) for the client's `[Peer]` section |
| `server_endpoint` | `string` | Yes | Server public address (`"ip:port"`) |
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `config` | `string` | Complete WireGuard client config text (`[Interface]` + `[Peer]` block) |
| `config` | `string` | Complete client config text (`[Interface]` + `[Peer]`) |
The client config includes the generated private key so the client can be provisioned directly. Note that this is the only endpoint that returns a WireGuard private key — all other endpoints strip private keys from responses.
This is the only endpoint that returns a WireGuard private key. All other endpoints strip private keys from responses.
Returns HTTP `404` if the peer is not found.
Returns HTTP `404` if the peer is not found.