Files
EvoBGP/internal/httpapi/speakers.go
T
DenozordecandCursor 4d83b8d673
CI / changes (push) Successful in 6s
CI / commitlint (push) Skipped
CI / openapi (push) Successful in 27s
CI / web (push) Successful in 51s
CI / go (push) Successful in 2m19s
CI / bird2 (push) Successful in 13s
CI / release (push) Successful in 4m24s
feat(auth): integrate portal JWT for enhanced authentication and authorization
Added support for portal JWT authentication, enabling single sign-on (SSO) capabilities. Updated the application to handle JWT claims for user permissions and roles, enhancing security and access control. Refactored relevant components and API routes to accommodate the new authentication flow, ensuring a seamless user experience. Updated documentation to reflect the new authentication requirements and configurations.

Co-authored-by: Cursor <[email protected]>
2026-07-18 23:23:52 +07:00

144 lines
3.7 KiB
Go

package httpapi
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"net/http"
"strings"
"time"
"evobgp/internal/nodedispatch"
"evobgp/internal/store"
)
func speakerJSONFromStore(st store.Backend, sp *store.Speaker) map[string]any {
if sp == nil {
return map[string]any{}
}
meta := store.ParseSpeakerMeta(sp.MetaJSON)
m := map[string]any{
"id": sp.ID,
"role": sp.Role,
"endpoint": sp.Endpoint,
}
if sp.LastAppliedRevisionID != nil {
m["last_applied_revision_id"] = *sp.LastAppliedRevisionID
} else {
m["last_applied_revision_id"] = nil
}
if st != nil {
if rid, at, err := st.LatestPublishedRevision(sp.ID); err == nil && rid != "" {
m["published_revision_id"] = rid
m["published_at"] = at.UTC().Format(time.RFC3339Nano)
} else {
m["published_revision_id"] = nil
m["published_at"] = nil
}
}
if strings.TrimSpace(sp.MetaJSON) != "" && sp.MetaJSON != "{}" {
var raw map[string]any
if json.Unmarshal([]byte(sp.MetaJSON), &raw) == nil {
delete(raw, "agent_secret")
if len(raw) > 0 {
m["meta_json"] = raw
}
}
}
if meta.AgentDomain != "" {
m["agent_domain"] = meta.AgentDomain
}
if meta.NodeIPv4 != "" {
m["node_ipv4"] = meta.NodeIPv4
}
if meta.BirdBgpSourceIPv4 != "" {
m["bird_bgp_source_ipv4"] = meta.BirdBgpSourceIPv4
}
if meta.LastDispatchAt != "" {
m["last_dispatch_at"] = meta.LastDispatchAt
}
if meta.LastDispatchError != "" {
m["last_dispatch_error"] = meta.LastDispatchError
}
if meta.LastDispatchStatus != "" {
m["dispatch_status"] = meta.LastDispatchStatus
}
if meta.SyncStatus != "" {
m["sync_status"] = meta.SyncStatus
}
return m
}
func (s *Server) handleBundleSigningPublicKey(w http.ResponseWriter, r *http.Request) {
a, ok := authFromContext(r.Context())
if !ok || !s.requirePerm(w, a, "bgp:network:read") {
return
}
writeJSON(w, http.StatusOK, map[string]any{
"public_key_base64": s.BundlePublicKeyBase64(),
})
}
// normalizeSpeakerCreate fills meta defaults and validates replica fields.
func normalizeSpeakerCreate(in *store.Speaker) error {
if in == nil {
return store.ErrInvalidInput
}
meta := store.ParseSpeakerMeta(in.MetaJSON)
if meta.AgentSecret == "" {
b := make([]byte, 24)
if _, err := rand.Read(b); err != nil {
return err
}
meta.AgentSecret = hex.EncodeToString(b)
}
if meta.AgentPort == 0 {
meta.AgentPort = 8443
}
if meta.NodeIPv4 == "" {
meta.NodeIPv4 = store.IPv4FromEndpoint(in.Endpoint)
}
if meta.BirdBgpSourceIPv4 == "" && meta.NodeIPv4 != "" {
meta.BirdBgpSourceIPv4 = meta.NodeIPv4
}
if meta.BirdBgpSourceIPv4 != "" && !store.ValidIPv4(meta.BirdBgpSourceIPv4) {
return store.ErrInvalidInput
}
if meta.AgentDomain == "" && in.Endpoint != "" {
ep := strings.TrimSpace(in.Endpoint)
if strings.HasPrefix(ep, "https://") {
u := strings.TrimPrefix(ep, "https://")
if idx := strings.Index(u, "/"); idx >= 0 {
u = u[:idx]
}
if idx := strings.Index(u, ":"); idx >= 0 {
u = u[:idx]
}
if u != "" && !store.ValidIPv4(u) {
meta.AgentDomain = u
}
}
}
in.MetaJSON = store.SpeakerMetaJSON(meta)
return nil
}
func (s *Server) recordSpeakerDispatch(tenantID string, sp *store.Speaker, res nodedispatch.Result) {
if s == nil || s.store == nil || sp == nil {
return
}
patch := store.SpeakerMeta{
LastDispatchAt: time.Now().UTC().Format(time.RFC3339Nano),
LastDispatchStatus: res.Status,
}
if res.Error != "" {
patch.LastDispatchError = res.Error
patch.SyncStatus = "error"
} else if res.Status == "ok" {
patch.LastDispatchError = ""
patch.SyncStatus = "synced"
}
meta := store.MergeSpeakerMetaJSON(sp.MetaJSON, patch)
_, _ = s.store.UpdateSpeaker(tenantID, sp.ID, &store.SpeakerPatch{MetaJSON: &meta})
}