package httpapi import ( "encoding/json" "net/http" "strings" "time" "evobgp/internal/birdfmt" "evobgp/internal/store" ) func (s *Server) registerPeerDiscoveryRoutes(m *http.ServeMux) { m.HandleFunc("GET /peers/discovered", s.handleListPeerDiscoveries) m.HandleFunc("POST /peers/discovered/{id}/approve", s.handleApprovePeerDiscovery) m.HandleFunc("POST /peers/discovered/{id}/reject", s.handleRejectPeerDiscovery) } func peerDiscoveryJSON(d *store.BGPPeerDiscovery) map[string]any { if d == nil { return nil } m := map[string]any{ "id": d.ID, "neighbor_id": d.NeighborID, "neighbor": d.Neighbor, "remote_asn": d.RemoteASN, "protocol_name": d.ProtocolName, "session_state": d.SessionState, "status": d.Status, "first_seen_at": d.FirstSeenAt.UTC().Format(time.RFC3339Nano), "last_seen_at": d.LastSeenAt.UTC().Format(time.RFC3339Nano), } if d.SpeakerID != "" { m["speaker_id"] = d.SpeakerID } else { m["speaker_id"] = nil } if d.ApprovedPeerID != "" { m["approved_peer_id"] = d.ApprovedPeerID } else { m["approved_peer_id"] = nil } return m } // syncPeerDiscoveriesFromLive upserts pending discoveries from dynamic BGP sessions. func (s *Server) syncPeerDiscoveriesFromLive(tenantID string, views []speakerBGPLive) { if s.store == nil || tenantID == "" { return } knownNeighbors := map[string]struct{}{} for _, p := range s.store.ListPeers(tenantID) { if p == nil { continue } if n, ok := store.NormalizePeerNeighborString(p.Neighbor); ok { knownNeighbors[n] = struct{}{} } } now := time.Now().UTC() for _, v := range views { for _, sess := range v.Sessions { if !birdfmt.IsDynamicDiscoverySession(sess.Name) { continue } neighbor, ok := store.NormalizePeerNeighborString(sess.Neighbor) if !ok { continue } if _, known := knownNeighbors[neighbor]; known { continue } _, _ = s.store.UpsertPeerDiscovery(tenantID, &store.PeerDiscoveryUpsert{ SpeakerID: v.SpeakerID, NeighborID: strings.TrimSpace(sess.NeighborID), Neighbor: neighbor, RemoteASN: sess.NeighborAS, ProtocolName: sess.Name, SessionState: sess.State, SeenAt: now, }) } } } func (s *Server) handleListPeerDiscoveries(w http.ResponseWriter, r *http.Request) { a, ok := authFromContext(r.Context()) if !ok { writeProblem(w, http.StatusUnauthorized, "Unauthorized", "missing auth") return } if !s.requirePerm(w, a, "bgp:network:read") { return } status := strings.TrimSpace(r.URL.Query().Get("status")) if status == "" { status = store.PeerDiscoveryPending } // Refresh live discovery when listing pending. if status == store.PeerDiscoveryPending || status == "all" { views := s.collectSpeakerBGPLive(r.Context(), a.TenantID, true) s.syncPeerDiscoveriesFromLive(a.TenantID, views) } listStatus := status if status == "all" { listStatus = "" } items, err := s.store.ListPeerDiscoveries(a.TenantID, listStatus) if err != nil { writeStoreErr(w, err) return } out := make([]map[string]any, 0, len(items)) for _, d := range items { out = append(out, peerDiscoveryJSON(d)) } writeJSON(w, http.StatusOK, map[string]any{"items": out}) } func (s *Server) handleApprovePeerDiscovery(w http.ResponseWriter, r *http.Request) { a, ok := authFromContext(r.Context()) if !ok || !s.requirePerm(w, a, "bgp:network:write") { return } var body struct { Name string `json:"name"` BGPSpeakerID *string `json:"bgp_speaker_id"` Enabled *bool `json:"enabled"` } _ = json.NewDecoder(r.Body).Decode(&body) peer, disc, err := s.store.ApprovePeerDiscovery(a.TenantID, r.PathValue("id"), &store.PeerDiscoveryApproveInput{ Name: body.Name, SpeakerID: body.BGPSpeakerID, Enabled: body.Enabled, }) if err != nil { writeStoreErr(w, err) return } s.enqueuePeerReconcile(a.TenantID, "peer_discovery_approve") s.recordCRUDAudit(r, a, "bgp.peer.discovery.approve", "Approved discovered peer "+peer.Neighbor, peer.ID, map[string]any{ "peer_id": peer.ID, "discovery_id": disc.ID, "neighbor": peer.Neighbor, "neighbor_id": disc.NeighborID, "remote_asn": peer.RemoteASN, }) writeJSON(w, http.StatusOK, map[string]any{ "peer": peerJSON(peer), "discovery": peerDiscoveryJSON(disc), }) } func (s *Server) handleRejectPeerDiscovery(w http.ResponseWriter, r *http.Request) { a, ok := authFromContext(r.Context()) if !ok || !s.requirePerm(w, a, "bgp:network:write") { return } disc, err := s.store.RejectPeerDiscovery(a.TenantID, r.PathValue("id")) if err != nil { writeStoreErr(w, err) return } s.recordCRUDAudit(r, a, "bgp.peer.discovery.reject", "Rejected discovered peer "+disc.Neighbor, disc.ID, map[string]any{ "discovery_id": disc.ID, "neighbor": disc.Neighbor, "neighbor_id": disc.NeighborID, }) writeJSON(w, http.StatusOK, peerDiscoveryJSON(disc)) }