hub: offsite multi-endpoint management UI (v0.47.0 part 5)
- /offsite lists ALL wg_endpoints rows as cards (id, address, pubkey, subnet, PBS addr, peers-in-subnet count) + add/edit/delete forms - store: ListWGEndpoints (id order) + DeleteWGEndpoint (plain delete; the peers-in-subnet guard lives in the handler where the refusal is built); SetWGEndpoint upsert reused, single-expected comment updated - guards: subnet edit refused 409 while peers sit in the current subnet; endpoint delete refused 409 while peers sit in its subnet; full form validation (CIDR, pbs ip in subnet, port 1-65535, pubkey, id charset) -> 400 - peer table gains an Endpoint column (first id-ordered subnet match; em dash when none); pubkey-change edit gets a type-to-confirm noting pull-based convergence - allocation/reconciler/desired-state STAY lowest-endpoint-id (page notes the deferral); GetWGEndpoint semantics untouched - tests: E1-E6 incl. guard red-proofs; store list/delete test Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vvz1NCu22p8dGkRCpeX9re
This commit is contained in:
@@ -45,7 +45,9 @@ type WGPeer struct {
|
||||
CreatedAt string // as stored (SQLite datetime text); display-only
|
||||
}
|
||||
|
||||
// SetWGEndpoint upserts the (single expected) endpoint record.
|
||||
// SetWGEndpoint upserts an endpoint record. Multiple endpoints are legal since v0.47.0
|
||||
// (the /offsite management UI); allocation + sync still use the LOWEST endpoint_id only
|
||||
// (GetWGEndpoint) — per-endpoint allocation is a future arc.
|
||||
func (s *Store) SetWGEndpoint(e *WGEndpoint) error {
|
||||
if e.EndpointID == "" {
|
||||
e.EndpointID = "ep0"
|
||||
@@ -75,6 +77,41 @@ func (s *Store) GetWGEndpoint() (*WGEndpoint, error) {
|
||||
return &e, nil
|
||||
}
|
||||
|
||||
// ListWGEndpoints returns every endpoint record ordered by endpoint_id (v0.47.0 — the
|
||||
// /offsite management UI lists them all; the lowest id stays THE allocation/sync endpoint).
|
||||
func (s *Store) ListWGEndpoints() ([]WGEndpoint, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT endpoint_id, dns_name, wg_port, server_pubkey, tunnel_subnet, pbs_tunnel_ip
|
||||
FROM wg_endpoints ORDER BY endpoint_id`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var eps []WGEndpoint
|
||||
for rows.Next() {
|
||||
var e WGEndpoint
|
||||
if err := rows.Scan(&e.EndpointID, &e.DNSName, &e.WGPort, &e.ServerPubkey, &e.TunnelSubnet, &e.PBSTunnelIP); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
eps = append(eps, e)
|
||||
}
|
||||
return eps, rows.Err()
|
||||
}
|
||||
|
||||
// DeleteWGEndpoint removes an endpoint record. Plain delete — the peers-in-subnet guard
|
||||
// lives in the web handler (where the refusal message is built); sql.ErrNoRows when the
|
||||
// endpoint is unknown.
|
||||
func (s *Store) DeleteWGEndpoint(endpointID string) error {
|
||||
res, err := s.db.Exec(`DELETE FROM wg_endpoints WHERE endpoint_id = ?`, endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n, _ := res.RowsAffected(); n == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddWGPeer registers a peer pubkey and allocates the lowest free host address in the tunnel
|
||||
// subnet — skipping the network address, the endpoint's own pbs_tunnel_ip, and (v4) the
|
||||
// broadcast. Idempotent: an already-registered pubkey returns its existing IP with existed=true
|
||||
|
||||
Reference in New Issue
Block a user