// Command admin starts the OpenGoods admin console (authenticated write API + // embedded SPA), served under a base path (default /ping). package main import ( "context" "crypto/rand" "log" "net/http" "os" "time" "github.com/jackc/pgx/v5/pgxpool" "golang.org/x/crypto/bcrypt" "github.com/baicai2026-baicai/goods/api/internal/adminhandler" "github.com/baicai2026-baicai/goods/api/internal/adminstore" "github.com/baicai2026-baicai/goods/api/internal/adminweb" "github.com/baicai2026-baicai/goods/api/internal/auth" "github.com/baicai2026-baicai/goods/api/internal/ratelimit" ) func getenv(key, fallback string) string { if v, ok := os.LookupEnv(key); ok && v != "" { return v } return fallback } func main() { addr := getenv("GOODS_ADMIN_ADDR", ":8080") dbURL := getenv("OPENGOODS_DATABASE_URL", "postgres://opengoods:opengoods@localhost:5432/opengoods?sslmode=disable") basePath := getenv("GOODS_ADMIN_BASE_PATH", "/ping") username := getenv("GOODS_ADMIN_USER", "admin") // Password: prefer a bcrypt hash; otherwise hash a plaintext password. var passwordHash []byte if h := os.Getenv("GOODS_ADMIN_PASSWORD_HASH"); h != "" { passwordHash = []byte(h) } else if p := os.Getenv("GOODS_ADMIN_PASSWORD"); p != "" { hashed, err := bcrypt.GenerateFromPassword([]byte(p), bcrypt.DefaultCost) if err != nil { log.Fatalf("failed to hash admin password: %v", err) } passwordHash = hashed } else { log.Fatal("set GOODS_ADMIN_PASSWORD or GOODS_ADMIN_PASSWORD_HASH") } secret := []byte(os.Getenv("GOODS_ADMIN_JWT_SECRET")) if len(secret) == 0 { secret = make([]byte, 32) if _, err := rand.Read(secret); err != nil { log.Fatalf("failed to generate jwt secret: %v", err) } log.Print("warning: GOODS_ADMIN_JWT_SECRET not set; using a random secret (tokens invalidate on restart)") } ctx := context.Background() pool, err := pgxpool.New(ctx, dbURL) if err != nil { log.Fatalf("failed to create db pool: %v", err) } defer pool.Close() pingCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() if err := pool.Ping(pingCtx); err != nil { log.Printf("warning: database not reachable at startup: %v", err) } authn := auth.New(username, passwordHash, secret, 12*time.Hour) usage := ratelimit.New(getenv("OPENGOODS_REDIS_URL", "redis://localhost:6379/0")) h := adminhandler.New(adminstore.New(pool), authn, basePath, adminweb.Dist()). WithUsage(usage) srv := &http.Server{ Addr: addr, Handler: h.Router(), ReadHeaderTimeout: 10 * time.Second, } log.Printf("OpenGoods admin console listening on %s (base path %s)", addr, basePath) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("server error: %v", err) } }