merge: 管理端最小后台 + TOTP 双因素 [tsk_SCMtcGF4F434]
以 main 现有 cmd/server 设计为准(保留 probe + /v1 路由),叠加分支新增:internal/admin/ 自包含后台(session/2FA/IP 白名单/审计/模板)、internal/totp/ + cmd/adminctl/(TOTP + 管理员创建 CLI)、cmd/server/main.go 的 startAdminIfConfigured()(opt-in:ADMIN_SECRET_KEY+DB_DSN 才起)。统一 getenvDefault 命名;go.mod 取 HEAD + go mod tidy(x/term 提为直接依赖);admins 迁移与 main 既有 000008_provision 撞号,重排为 000009_admins。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -9,11 +9,13 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/wangjia/pangolin/server/internal/redisutil"
|
||||
"github.com/wangjia/pangolin/server/internal/scheduler/probe"
|
||||
|
||||
"github.com/wangjia/pangolin/server/api/gen"
|
||||
"github.com/wangjia/pangolin/server/internal/admin"
|
||||
"github.com/wangjia/pangolin/server/internal/db"
|
||||
"github.com/wangjia/pangolin/server/internal/httpapi"
|
||||
"github.com/wangjia/pangolin/server/internal/redisutil"
|
||||
"github.com/wangjia/pangolin/server/internal/scheduler/probe"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -28,6 +30,10 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Admin backend (separate internal listener). Started only when explicitly
|
||||
// configured so the public API can run on its own. See internal/admin.
|
||||
startAdminIfConfigured()
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(middleware.Recoverer)
|
||||
@@ -77,6 +83,44 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// startAdminIfConfigured launches the admin listener in a background goroutine
|
||||
// when ADMIN_SECRET_KEY and DB_DSN are present. The admin port binds an
|
||||
// internal address only (enforced by admin.FromEnv) and is fronted by an IP
|
||||
// allowlist + two-factor login.
|
||||
func startAdminIfConfigured() {
|
||||
if os.Getenv("ADMIN_SECRET_KEY") == "" || os.Getenv("DB_DSN") == "" {
|
||||
log.Printf("admin backend disabled (set DB_DSN and ADMIN_SECRET_KEY to enable)")
|
||||
return
|
||||
}
|
||||
cfg, err := admin.FromEnv()
|
||||
if err != nil {
|
||||
log.Fatalf("admin config: %v", err)
|
||||
}
|
||||
database, err := db.Open(os.Getenv("DB_DSN"))
|
||||
if err != nil {
|
||||
log.Fatalf("admin db: %v", err)
|
||||
}
|
||||
rdb, err := redisutil.New(
|
||||
getenvDefault("REDIS_ADDR", "127.0.0.1:6379"),
|
||||
os.Getenv("REDIS_PASSWORD"), 0)
|
||||
if err != nil {
|
||||
log.Fatalf("admin redis: %v", err)
|
||||
}
|
||||
|
||||
svc := admin.BuildServices(database, rdb, 5, cfg.LoginLockDuration)
|
||||
handler, err := admin.NewHandler(cfg, database, rdb, svc, log.Default())
|
||||
if err != nil {
|
||||
log.Fatalf("admin handler: %v", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Printf("admin backend listening on %s (internal only)", cfg.Listen)
|
||||
if err := http.ListenAndServe(cfg.Listen, handler); err != nil {
|
||||
log.Fatalf("admin server error: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func getenvDefault(key, def string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
|
||||
Reference in New Issue
Block a user