package httpapi import ( "context" "crypto/ed25519" "crypto/rand" "encoding/hex" "errors" "net/http" "strings" "time" "evobgp/internal/jobs" "evobgp/internal/maintenance" "evobgp/internal/pgmonitor" "evobgp/internal/runtimelogs" "evobgp/internal/store" "github.com/jackc/pgx/v5/pgxpool" ) // Server implements EvoBGP control-plane HTTP API. type Server struct { store store.Backend pgPool *pgxpool.Pool pgMonitor *pgmonitor.Service maintConfig *maintenance.ConfigProvider maintStats *maintenance.DBStatsProvider jobs *jobs.Registry bundlePriv ed25519.PrivateKey keyResolver *apiKeyResolver firewallResolver *firewallTokenResolver bundleSeedHex string corsOrigins []string cdnHTTP *http.Client runtimeLogs *runtimelogs.Service runtimeLogsPolicyTenant string mux *http.ServeMux // Portal / dual-auth (JWT) configuration. jwtSecret string authIssuer string authPortalURL string portalTenantID string authRequired bool } // Options configures the API server. type Options struct { APIKeys string // DatabaseURL enables PostgreSQL-backed store (migrations applied on connect). DatabaseURL string InsecureDev bool SeedDemo bool BundleSeedHex string CORSAllowedOrigins string // RuntimeLogsPolicyTenant overrides tenant for auto-cleanup scheduler settings (optional). RuntimeLogsPolicyTenant string // Portal / dual-auth (JWT) — leave empty to disable JWT path. JWTSecret string // AUTH_JWT_SECRET / EVOBGP_AUTH_JWT_SECRET (HS256 shared secret) AuthIssuer string // AUTH_ISSUER (expected iss claim; default https://auth.shnt.top) AuthPortalURL string // AUTH_PORTAL_URL (returned by /v1/auth/config for the UI) PortalTenantID string // fallback when JWT has no bgp_tenant_id / tenants.bgp AuthRequired bool // AUTH_REQUIRED / EVOBGP_AUTH_REQUIRED (surfaced via /v1/auth/config) } // New constructs Server and wiring for async jobs. func New(opts Options) (*Server, error) { backend, reg, pool, err := BootstrapWorkers(context.Background(), opts) if err != nil { return nil, err } var priv ed25519.PrivateKey if strings.TrimSpace(opts.BundleSeedHex) != "" { seed, err := hex.DecodeString(strings.TrimSpace(opts.BundleSeedHex)) if err != nil { return nil, err } if len(seed) != ed25519.SeedSize { return nil, errors.New("httpapi: BundleSeedHex must decode to 32 bytes") } priv = ed25519.NewKeyFromSeed(seed) } else { _, priv, _ = ed25519.GenerateKey(rand.Reader) } resolver, err := newAPIKeyResolver(opts.APIKeys, backend) if err != nil { return nil, err } fwResolver, err := newFirewallTokenResolver(backend) if err != nil { return nil, err } var pgMon *pgmonitor.Service var maintCfg *maintenance.ConfigProvider var maintStats *maintenance.DBStatsProvider if pool != nil { pgMon = pgmonitor.NewService(pool) maintCfg = maintenance.NewConfigProvider(backend) _ = maintCfg.Reload(context.Background()) maintStats = maintenance.NewDBStatsProvider(pgMon) } s := &Server{ store: backend, pgPool: pool, pgMonitor: pgMon, maintConfig: maintCfg, maintStats: maintStats, jobs: reg, bundlePriv: priv, keyResolver: resolver, firewallResolver: fwResolver, bundleSeedHex: strings.TrimSpace(opts.BundleSeedHex), corsOrigins: parseCORSOrigins(opts.CORSAllowedOrigins), cdnHTTP: NewCDNHTTPClient(), runtimeLogs: runtimelogs.NewService(runtimelogs.ConfigFromEnv()), runtimeLogsPolicyTenant: strings.TrimSpace(opts.RuntimeLogsPolicyTenant), jwtSecret: strings.TrimSpace(opts.JWTSecret), authIssuer: strings.TrimSpace(opts.AuthIssuer), authPortalURL: strings.TrimSpace(opts.AuthPortalURL), portalTenantID: strings.TrimSpace(opts.PortalTenantID), authRequired: opts.AuthRequired, } if s.authIssuer == "" { s.authIssuer = "https://auth.shnt.top" } s.mux = http.NewServeMux() s.registerRoutes() return s, nil } // Close releases database resources. func (s *Server) Close() { if s.pgPool != nil { s.pgPool.Close() } } // Store exposes the backing store (for operators / tests). func (s *Server) Store() store.Backend { return s.store } // Jobs exposes the in-process async job registry (for scheduler / evobgp-all). func (s *Server) Jobs() *jobs.Registry { return s.jobs } // StartBackground starts PostgreSQL monitoring and maintenance schedulers until ctx is cancelled. func (s *Server) StartBackground(ctx context.Context) { if s != nil && s.pgPool != nil { pgmonitor.StartScheduler(ctx, s.pgPool) } if s != nil && s.maintConfig != nil && s.jobs != nil { maintenance.StartScheduler(ctx, s.maintConfig, func(policyID string, dryRun bool, idem string) { key := idem _, _, _ = s.jobs.Enqueue("", jobs.KindMaintenancePolicyRun, &key, nil, map[string]any{ "policy_id": policyID, "dry_run": dryRun, "trigger": "scheduler", }) }, 30*time.Second) } if s != nil && s.runtimeLogs != nil && s.store != nil { runtimelogs.StartAutoCleanupScheduler(ctx, runtimelogs.SchedulerDeps{ Service: s.runtimeLogs, Store: s.store, PolicyTenant: s.runtimeLogsPolicyTenant, }, 30*time.Second) } }