package admin import ( "database/sql" "log" "net/http" "time" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/redis/go-redis/v9" "github.com/wangjia/pangolin/server/internal/codes" "github.com/wangjia/pangolin/server/internal/provision" "github.com/wangjia/pangolin/server/internal/provision/providers" ) // NewRouter wires the full admin HTTP handler chain: // // IP allowlist → [ /login, /static ] (no session) // IP allowlist → session → [ everything else ] (authenticated) func NewRouter(h *Handlers, sessions *SessionStore, cfg *Config, sec *SecurityLog) http.Handler { ipAllow := func(next http.Handler) http.Handler { return NewIPAllow(cfg.AllowCIDRs, sec, next) } requireSession := func(next http.Handler) http.Handler { return NewSessionMiddleware(sessions, next) } r := chi.NewRouter() r.Use(middleware.Recoverer) r.Use(ipAllow) // Static assets (CSS/JS) — embedded; behind the allowlist but pre-auth. r.Handle("/static/*", http.FileServer(http.FS(assetsFS))) // Pre-auth login routes. r.Get("/login", h.LoginPage) r.Post("/login", h.LoginSubmit) // Authenticated routes. r.Group(func(pr chi.Router) { pr.Use(requireSession) pr.Get("/", h.Dashboard) pr.Post("/logout", h.Logout) pr.Get("/codes", h.CodesPage) pr.Post("/codes", h.CreateBatch) pr.Post("/codes/void", h.VoidBatch) pr.Get("/nodes", h.NodesPage) pr.Post("/nodes/op", h.NodeOp) pr.Get("/audit", h.AuditPage) }) return r } // BuildServices assembles the downstream services. Codes (#3) and lifecycle (#5) // are always real (DB-backed). Provisioning (#14) is real when the provision // service initializes; otherwise it falls back to the not-ready stub and the UI // greys out the replace control (Ready()==false). func BuildServices(db *sql.DB, rdb *redis.Client, failMax int, lockDur time.Duration) Services { codeStore := codes.NewStore(db) codeSvc := codes.NewService(codeStore, rdb, failMax, lockDur) svc := Services{ Codes: NewCodesAdapter(codeSvc, codeStore), Lifecycle: NewRealLifecycle(db), Provision: NewStubProvision(), } provSvc, err := provision.NewService(provision.Config{ Store: provision.NewMySQLStore(db), Adapters: providers.NewRegistry(), }) if err != nil { log.Printf("admin: provision service init failed (%v) — replace control disabled", err) } else { svc.Provision = NewRealProvision(provSvc) } return svc } // NewHandler builds the complete admin http.Handler from its dependencies. func NewHandler(cfg *Config, db *sql.DB, rdb *redis.Client, svc Services, logger *log.Logger) (http.Handler, error) { if logger == nil { logger = log.Default() } store := NewDBStore(db) sessions := NewSessionStore(rdb, cfg.SessionTTL) sec := NewSecurityLog(store, logger) auth := NewAuthenticator(store, sessions, rdb, cfg, sec) handlers, err := NewHandlers(cfg, store, sessions, auth, svc, sec, logger) if err != nil { return nil, err } return NewRouter(handlers, sessions, cfg, sec), nil }