// Package api wires the HTTP endpoints. Kept on net/http — no router lib // for a handful of endpoints, stdlib mux is plenty. // // Slice 2 adds sandboxes, templates, environments, routes, port // allocation, and a real agent-mediated auth path. The legacy // /api/deployments POST is kept for ad-hoc deploys (no sandbox) and a // new /api/sandboxes/{id}/services/{repo}/deploy adds the // sandbox-scoped variant that picks the right port and env. package api import ( "crypto/rand" "encoding/hex" "net/http" "strings" "sync" "time" "github.com/sdp/control-plane/internal/store" "github.com/sdp/control-plane/internal/ws" ) type Server struct { st *store.Store hub *ws.Hub agents *AgentRegistry sess *Sessions // gatewayRepo is the repo the control plane uses to validate // Bitbucket creds at login. Defaults to "api-gateway". gatewayRepo string // microVMIP / gatewayVMIP are the host IPs the dashboard shows // when a user picks a "local stand-in" for a microservice route. // Defaults match the company infra (172.18.136.92 / 172.18.139.186). microVMIP string gatewayVMIP string } type AgentRegistry struct { mu sync.RWMutex conns map[string]bool // nodeIDs currently connected over WS } func New(st *store.Store, hub *ws.Hub) *Server { return &Server{ st: st, hub: hub, agents: &AgentRegistry{conns: make(map[string]bool)}, sess: NewSessions(), gatewayRepo: "api-gateway", microVMIP: "172.18.136.92", gatewayVMIP: "172.18.139.186", } } func (s *Server) Routes() http.Handler { mux := http.NewServeMux() // auth mux.HandleFunc("/api/login", s.handleLogin) mux.HandleFunc("/api/logout", s.handleLogout) // repos & deploy mux.HandleFunc("/api/repos", s.handleListRepos) mux.HandleFunc("/api/repos/branches", s.handleListBranches) mux.HandleFunc("/api/agents", s.handleListAgents) mux.HandleFunc("/api/deployments", s.handleListDeployments) // GET list mux.HandleFunc("/api/deployments/new", s.handleCreateDeployment) // POST mux.HandleFunc("/api/deployments/stop", s.handleStopDeployment) // POST // environments mux.HandleFunc("/api/environments", s.handleEnvironments) // GET list, POST create mux.HandleFunc("/api/environments/", s.handleEnvironmentByID) // GET/PUT/DELETE /{id} // sandboxes mux.HandleFunc("/api/sandboxes", s.handleSandboxes) // GET list, POST create mux.HandleFunc("/api/sandboxes/", s.handleSandboxByID) // GET/PUT/DELETE /{id} mux.HandleFunc("/api/sandboxes/clone", s.handleCloneSandbox) // POST: clone from template // templates mux.HandleFunc("/api/templates", s.handleTemplates) // GET list, POST create mux.HandleFunc("/api/templates/", s.handleTemplateByID) // GET/PUT/DELETE /{id} // routes mux.HandleFunc("/api/routes", s.handleListRoutes) // GET: live routes from gateway agent's config.php mux.HandleFunc("/api/routes/push", s.handlePushRoutes) // POST: push routes for a sandbox // websocket mux.Handle("/ws/agent", s.hub.AgentWS(s.st, func(nodeID string) { s.agents.mu.Lock() s.agents.conns[nodeID] = true s.agents.mu.Unlock() }, func(nodeID string) { s.agents.mu.Lock() delete(s.agents.conns, nodeID) s.agents.mu.Unlock() }, )) mux.HandleFunc("/ws/deployments/", s.hub.DeploymentWS(s.st)) return s.withAuth(mux) } // withAuth checks the session cookie on /api/* (skipping login). /ws/* is // protected at the handler — we don't pass auth in headers easily on the WS // upgrade from the browser, so the dashboard sends ?token=... instead. func (s *Server) withAuth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !strings.HasPrefix(r.URL.Path, "/api/") || r.URL.Path == "/api/login" { next.ServeHTTP(w, r) return } c, err := r.Cookie("sdp_session") if err != nil || !s.sess.Valid(c.Value) { http.Error(w, "unauthorized", http.StatusUnauthorized) return } next.ServeHTTP(w, r) }) } // userFromContext returns the authenticated user from the session cookie. // The cookie is set on /api/login; on every other /api/* path we look it // up here so handlers can record who did what (audit trail). func (s *Server) userFromContext(r *http.Request) string { c, err := r.Cookie("sdp_session") if err != nil { return "" } u, _ := s.sess.User(c.Value) return u } func newID() string { b := make([]byte, 8) _, _ = rand.Read(b) return hex.EncodeToString(b) } func writeJSON(w http.ResponseWriter, status int, v interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = encodeJSON(w, v) } func writeErr(w http.ResponseWriter, status int, msg string) { writeJSON(w, status, map[string]string{"error": msg}) } // nowMs is a small helper so tests can stub it later. func nowMs() int64 { return time.Now().UnixMilli() }