Files
zkcoi e9ca2f7d70 fix: rename manager module to git.zkcoi.com/zkcoi/Meshray-Manager
- go.mod module path matches repo zkcoi/Meshray-Manager (case-distinct from zkcoi/Meshray/core)
- rewrite internal imports meshray/{internal,web,pkg} -> Meshray-Manager/... (core refs kept)
- sync README.md / install.sh repo URLs; add CHANGELOG entry
2026-07-15 16:25:46 +08:00

546 lines
18 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package api
import (
"context"
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"git.zkcoi.com/zkcoi/Meshray-Manager/internal/api/handler"
"git.zkcoi.com/zkcoi/Meshray-Manager/internal/api/middleware"
"git.zkcoi.com/zkcoi/Meshray-Manager/internal/config"
"git.zkcoi.com/zkcoi/Meshray-Manager/internal/ctr"
corehandler "git.zkcoi.com/zkcoi/Meshray-Manager/internal/handler"
"git.zkcoi.com/zkcoi/Meshray-Manager/internal/model"
"git.zkcoi.com/zkcoi/Meshray-Manager/internal/service"
"git.zkcoi.com/zkcoi/Meshray-Manager/internal/store/sqlite"
"git.zkcoi.com/zkcoi/Meshray-Manager/web"
"github.com/gin-gonic/gin"
"github.com/shirou/gopsutil/v4/cpu"
"go.uber.org/zap"
"gorm.io/gorm"
)
// Server API 服务器
type Server struct {
engine *gin.Engine
config *config.Config
logger *zap.Logger
store *sqlite.Store
ctrClient *ctr.Ctr // 全局 Ctr 实例
}
// NewServer 创建 API 服务器
func NewServer(cfg *config.Config, logger *zap.Logger, dbStore *sqlite.Store) (*Server, error) {
// 设置 Gin 模式
switch cfg.Server.Mode {
case "debug":
gin.SetMode(gin.DebugMode)
case "release":
gin.SetMode(gin.ReleaseMode)
default:
gin.SetMode(gin.TestMode)
}
engine := gin.New()
server := &Server{
engine: engine,
config: cfg,
logger: logger,
store: dbStore,
}
// 首次启动初始化管理员账户(已存在则跳过,仅新建时返回随机密码)
userService := service.NewUserService(dbStore)
if uname, pwd, ierr := userService.InitializeAdmin(); ierr == nil {
if pwd != "" {
server.logger.Warn("已创建默认管理员账户,请尽快登录并修改密码",
zap.String("username", uname), zap.String("password", pwd))
}
} else {
server.logger.Error("初始化管理员账户失败", zap.Error(ierr))
}
// 注册中间件
server.registerMiddleware()
// 注册路由
if err := server.registerRoutes(); err != nil {
return nil, err
}
return server, nil
}
// registerMiddleware 注册全局中间件
func (s *Server) registerMiddleware() {
// 恢复中间件
s.engine.Use(gin.Recovery())
// 请求日志中间件
s.engine.Use(middleware.RequestLogger(s.logger))
// CORS 中间件(开发环境)
if s.config.Server.Mode != "release" {
s.engine.Use(middleware.CORS())
}
}
// registerRoutes 注册所有路由
func (s *Server) registerRoutes() error {
// 健康检查
s.engine.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
// 静态文件服务(前端)
// 使用 embed 的静态文件
var staticFS fs.FS
var useEmbed bool
// 直接使用 web 包中的 WebAssets,剥离外层的 static 目录
if embedFS, err := fs.Sub(web.WebAssets, "static"); err == nil {
// 检查是否有 index.html
if _, statErr := fs.Stat(embedFS, "index.html"); statErr == nil {
staticFS = embedFS
useEmbed = true
s.logger.Info("使用内嵌的静态文件")
} else {
s.logger.Warn("embed 中找不到 index.html", zap.Error(statErr))
}
} else {
s.logger.Warn("embed 文件系统创建失败", zap.Error(err))
}
// 如果 embed 没有内容,使用外部目录
if !useEmbed && s.config.Server.StaticPath != "" {
staticFS = os.DirFS(s.config.Server.StaticPath)
s.logger.Info("使用外部静态文件目录", zap.String("path", s.config.Server.StaticPath))
}
if staticFS != nil {
// ✅ 注册根路径处理
s.engine.GET("/", func(c *gin.Context) {
file, err := staticFS.Open("index.html")
if err == nil {
defer file.Close()
content, _ := io.ReadAll(file)
c.Data(200, "text/html; charset=utf-8", content)
return
}
c.String(500, "Failed to load index.html")
})
// ✅ 简化:只注册 NoRoute 处理 SPA 路由
s.engine.NoRoute(func(c *gin.Context) {
path := c.Request.URL.Path
// 如果是 API 请求,返回 404
if strings.HasPrefix(path, "/api/") {
c.JSON(404, gin.H{"error": "API not found"})
return
}
// 尝试直接提供文件
// staticFS 已通过 fs.Sub 剥离 "static" 前缀,需先去掉路径中的 "static/"
filePath := strings.TrimPrefix(path, "/")
filePath = strings.TrimPrefix(filePath, "static/")
if filePath == "" {
filePath = "index.html"
}
// 读取并返回文件内容
file, err := staticFS.Open(filePath)
if err == nil {
defer file.Close()
content, _ := io.ReadAll(file)
c.Data(200, getContentType(filePath), content)
return
}
// 其他请求都返回 index.htmlSPA 路由支持)
file, _ = staticFS.Open("index.html")
if file != nil {
defer file.Close()
content, _ := io.ReadAll(file)
c.Data(200, "text/html; charset=utf-8", content)
}
})
} else {
s.logger.Warn("未配置静态文件路径,前端将不可用")
}
// API v1 路由组
v1 := s.engine.Group("/api/v1")
{
// 公开接口(无需鉴权)
public := v1.Group("")
{
public.POST("/auth/login", middleware.LoginHandler(s.config.JWT.Secret, s.logger, s.store))
public.POST("/auth/refresh", middleware.RefreshTokenHandler(s.config.JWT.Secret, s.logger))
}
// ✅ 提前初始化 ctrClient(在路由注册前)
var err error
s.ctrClient, err = ctr.NewCtr("default", 1, &ctr.CtrConfig{}, s.logger)
if err != nil {
s.logger.Error("初始化 meshray-ctr 失败", zap.Error(err))
return fmt.Errorf("初始化 ctr 失败:%w", err)
}
// 需要鉴权的接口
protected := v1.Group("")
protected.Use(middleware.JWTAuth(s.config.JWT.Secret))
{
// 初始化 Service 层(注入 ctr 客户端)
networkService := service.NewNetworkService(s.store, s.ctrClient, s.logger)
deviceService := service.NewDeviceService(s.store, s.ctrClient)
userService := service.NewUserService(s.store)
policyService := service.NewPolicyService(s.store)
settingsService := service.NewSettingsService(s.store, s.logger)
pendingJoinService := service.NewPendingJoinService(s.store)
// 初始化 MeshSeedService(需要 Ed25519 签名密钥)
signingKey, err := s.loadSigningKey()
if err != nil {
s.logger.Error("加载签名密钥失败", zap.Error(err))
return fmt.Errorf("加载签名密钥失败:%w", err)
}
meshSeedService := service.NewMeshSeedService(s.store, s.logger, signingKey, "node-1")
// 初始化 SystemConfigService(系统配置服务)
systemConfigService := service.NewSystemConfigService(s.store, s.logger)
// 初始化 ExternalService 相关 Handler
serviceService := service.NewServiceService(s.store)
serviceHandler := handler.NewServiceHandler(serviceService, s.logger)
// 初始化 SystemConfigHandler(系统配置处理器)
systemConfigHandler := handler.NewSystemConfigHandler(systemConfigService, s.logger)
// 初始化 PendingJoinHandler(待审核处理器)
pendingJoinHandler := handler.NewPendingJoinHandler(pendingJoinService, s.store, s.logger)
// 初始化 handlers(使用 Service 层)
networkHandler := handler.NewNetworkHandler(networkService, meshSeedService, s.store, s.logger)
deviceHandler := handler.NewDeviceHandler(deviceService, s.logger)
adminHandler := handler.NewAdminHandler(userService, s.logger)
dashboardHandler := handler.NewDashboardHandler(s.store, s.logger)
policyHandler := handler.NewPolicyHandler(policyService, s.logger)
settingsHandler := handler.NewSettingsHandler(settingsService, s.logger)
wsHandler := handler.NewWSHandler(s.ctrClient, s.store, s.logger)
ddnsService, err := service.NewDDNSService(s.store.DB())
if err != nil {
s.logger.Error("初始化 DDNSService 失败", zap.Error(err))
return fmt.Errorf("初始化 DDNSService 失败:%w", err)
}
// 启动后台自动同步协程
go ddnsService.StartAutoSync(context.Background())
ddnsHandler := handler.NewDDNSHandler(ddnsService)
// ✅ 新增:DDNS Usage Handler
ddnsUsageHandler := handler.NewDDNSUsageHandler(s.store.DB(), s.logger)
// ✅ 新增:IP 检测 API
ddnsDetectHandler := handler.NewIPDetectHandler()
// 管理员管理
protected.GET("/admin/profile", adminHandler.GetProfile)
protected.PUT("/admin/profile", adminHandler.UpdateProfile)
protected.POST("/admin/change-password", adminHandler.ChangePassword)
// ✅ 系统备份恢复
backupHandler := corehandler.NewBackupHandler(s.store.DB(), s.logger)
protected.POST("/system/backup", backupHandler.CreateBackup)
protected.GET("/system/backups", backupHandler.ListBackups)
protected.POST("/system/restore", backupHandler.RestoreBackup)
protected.DELETE("/system/backup", backupHandler.DeleteBackup)
protected.GET("/system/backup/download", backupHandler.DownloadBackup)
// ✅ 系统更新检查
updateHandler := corehandler.NewUpdateHandler(s.config.App.Version)
protected.GET("/system/update/check", func(c *gin.Context) {
resp, err := updateHandler.CheckUpdate()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "检查更新失败",
})
return
}
c.JSON(http.StatusOK, gin.H{
"data": resp,
})
})
// ✅ WebSocket 实时通知推送
notificationSvc := service.NewNotificationService(s.store.DB(), s.logger)
notificationHandler := corehandler.NewNotificationHandler(notificationSvc, s.logger, s.store.DB())
protected.GET("/notifications", notificationHandler.GetNotifications)
protected.GET("/notifications/unread-count", notificationHandler.GetUnreadCount)
protected.POST("/notifications/:id/read", notificationHandler.MarkAsRead)
protected.POST("/notifications/read-all", notificationHandler.MarkAllAsRead)
protected.DELETE("/notifications/:id", notificationHandler.DeleteNotification)
protected.POST("/notifications/test", notificationHandler.TestSendNotification)
// DDNS 配置
protected.GET("/ddns/config", ddnsHandler.GetDDNSConfig)
protected.PUT("/ddns/config", ddnsHandler.UpdateDDNSConfig)
protected.POST("/ddns/test", ddnsHandler.TestDDNSConnectivity)
protected.POST("/ddns/sync", ddnsHandler.SyncDDNS)
// ✅ IP 检测 API(用于前端自动填充)
protected.GET("/ddns/detect-ip", ddnsDetectHandler.DetectIP)
// ✅ DDNS 统计 API(用于 Dashboard 监控)
ddnsStatsHandler := handler.NewDDNSStatsHandler(s.store.DB())
protected.GET("/ddns/stats", ddnsStatsHandler.GetDDNSStats)
// ✅ DDNS Usage 管理
protected.POST("/ddns/usages", ddnsUsageHandler.CreateUsage)
protected.GET("/ddns/usages/available", ddnsUsageHandler.GetAvailableUsages)
protected.GET("/ddns/check-prefix", ddnsUsageHandler.CheckPrefixOccupied)
// Dashboard
protected.GET("/dashboard/stats", dashboardHandler.GetStats)
protected.GET("/dashboard/logs", dashboardHandler.GetRecentLogs)
protected.GET("/dashboard/system-info", dashboardHandler.GetSystemInfo)
protected.GET("/dashboard/link-distribution", dashboardHandler.GetLinkDistribution)
protected.POST("/dashboard/logs/clear", dashboardHandler.ClearLogs) // 新增:清理日志
// 系统设置
protected.GET("/settings", settingsHandler.GetSettings)
protected.PUT("/settings", settingsHandler.UpdateSettings)
// 系统配置(WG 模式等)
protected.GET("/system-config/wg-mode", systemConfigHandler.GetWGMode)
protected.PUT("/system-config/wg-mode", systemConfigHandler.SetWGMode)
// 系统运维(P1/P2 功能)- 使用 SystemConfigHandler 统一管理
protected.POST("/system/change-password", systemConfigHandler.ChangePassword) // P1: 修改密码
protected.POST("/system/restart-core", systemConfigHandler.RestartCore) // P1: 重启核心
// 组网管理
protected.GET("/networks", networkHandler.ListNetworks)
protected.POST("/networks", networkHandler.CreateNetwork)
protected.POST("/networks/preview", networkHandler.PreviewMeshSeed)
protected.POST("/networks/join", networkHandler.JoinNetwork)
protected.GET("/networks/:id", networkHandler.GetNetwork)
protected.PUT("/networks/:id", networkHandler.UpdateNetwork)
protected.DELETE("/networks/:id", networkHandler.DeleteNetwork)
protected.POST("/networks/:id/start", networkHandler.StartNetwork)
protected.POST("/networks/:id/stop", networkHandler.StopNetwork)
protected.POST("/networks/:id/switch-mode", networkHandler.SwitchMode)
protected.POST("/networks/:id/meshseed", networkHandler.GenerateMeshSeed)
// 设备管理(使用独立前缀避免路由冲突)
protected.GET("/devices", deviceHandler.ListDevices)
protected.POST("/devices", deviceHandler.CreateDevice)
protected.GET("/devices/:id", deviceHandler.GetDevice)
protected.PUT("/devices/:id", deviceHandler.UpdateDevice)
protected.DELETE("/devices/:id", deviceHandler.DeleteDevice)
protected.GET("/devices/:id/config", deviceHandler.GenerateDeviceConfig)
// 策略管理
protected.GET("/policies", policyHandler.ListPolicies)
protected.POST("/policies", policyHandler.CreatePolicy)
protected.GET("/policies/:id", policyHandler.GetPolicy)
protected.PUT("/policies/:id", policyHandler.UpdatePolicy)
protected.DELETE("/policies/:id", policyHandler.DeletePolicy)
// ExternalService 管理(用户视角)
protected.GET("/services", serviceHandler.ListServices)
protected.POST("/services", serviceHandler.CreateService)
protected.GET("/services/:id", serviceHandler.GetService)
protected.PUT("/services/:id", serviceHandler.UpdateService)
protected.DELETE("/services/:id", serviceHandler.DeleteService)
protected.POST("/services/:id/test", serviceHandler.TestServiceConnectivity)
protected.GET("/services/schema", serviceHandler.GetServiceSchema)
protected.GET("/monitor/metrics", s.handleMetrics)
// 注册 WebSocket 通道
protected.GET("/ws", wsHandler.ServeWS)
// 待审核管理
protected.GET("/pending-joins", pendingJoinHandler.ListPendingJoins)
protected.POST("/pending-joins/:id/approve", pendingJoinHandler.ApproveJoin)
protected.POST("/pending-joins/:id/reject", pendingJoinHandler.RejectJoin)
protected.GET("/pending-joins/count", pendingJoinHandler.CountPending)
protected.POST("/pending-joins/cleanup", pendingJoinHandler.DeleteExpired)
}
}
return nil
}
// handleMetrics 监控指标 API
func (s *Server) handleMetrics(c *gin.Context) {
// 获取系统信息
var memStats runtime.MemStats
runtime.ReadMemStats(&memStats)
// 获取 CPU 使用率(简化版本)
cpuPercent := 0.0
if cpus, err := cpu.Percent(time.Second, false); err == nil && len(cpus) > 0 {
cpuPercent = cpus[0]
}
// 网络统计(从数据库)
var deviceCount, networkCount, onlineCount int64
s.store.DB().Model(&model.Device{}).Count(&deviceCount)
s.store.DB().Model(&model.Network{}).Count(&networkCount)
s.store.DB().Model(&model.Device{}).Where("status = ?", "online").Count(&onlineCount)
// 返回 Prometheus 格式或 JSON 格式
accept := c.GetHeader("Accept")
if strings.Contains(accept, "text/plain") {
// Prometheus 格式
metrics := fmt.Sprintf(`# HELP meshray_memory_alloc_bytes 当前内存使用量
# TYPE meshray_memory_alloc_bytes gauge
meshray_memory_alloc_bytes %d
# HELP meshray_cpu_usage_percent CPU 使用率
# TYPE meshray_cpu_usage_percent gauge
meshray_cpu_usage_percent %.2f
# HELP meshray_device_total 设备总数
# TYPE meshray_device_total gauge
meshray_device_total %d
# HELP meshray_device_online 在线设备数
# TYPE meshray_device_online gauge
meshray_device_online %d
# HELP meshray_network_total 网络总数
# TYPE meshray_network_total gauge
meshray_network_total %d
`,
memStats.Alloc,
cpuPercent,
deviceCount,
onlineCount,
networkCount)
c.Header("Content-Type", "text/plain; version=0.0.4")
c.String(200, metrics)
} else {
// JSON 格式(前端使用)
c.JSON(200, gin.H{
"data": gin.H{
"memory": gin.H{
"alloc_bytes": memStats.Alloc,
"alloc_mb": float64(memStats.Alloc) / 1024 / 1024,
"sys_bytes": memStats.Sys,
"num_gc": memStats.NumGC,
},
"cpu": gin.H{
"usage_percent": cpuPercent,
},
"devices": gin.H{
"total": deviceCount,
"online": onlineCount,
"offline": deviceCount - onlineCount,
},
"networks": gin.H{
"total": networkCount,
},
"timestamp": time.Now().Unix(),
},
})
}
}
// getContentType 根据文件扩展名返回 Content-Type
func getContentType(filePath string) string {
ext := strings.ToLower(filepath.Ext(filePath))
switch ext {
case ".html":
return "text/html; charset=utf-8"
case ".css":
return "text/css; charset=utf-8"
case ".js":
return "application/javascript; charset=utf-8"
case ".json":
return "application/json; charset=utf-8"
case ".png":
return "image/png"
case ".jpg", ".jpeg":
return "image/jpeg"
case ".gif":
return "image/gif"
case ".svg":
return "image/svg+xml"
case ".ico":
return "image/x-icon"
default:
return "application/octet-stream"
}
}
// Run 启动 API 服务器
func (s *Server) Run() error {
addr := fmt.Sprintf(":%d", s.config.Server.Port)
s.logger.Info("Starting MeshRay", zap.String("address", addr))
// 打印访问地址
fmt.Println("")
fmt.Printf("🌐 MeshRay 启动成功!\n")
fmt.Printf("📍 访问地址:http://localhost:%d\n", s.config.Server.Port)
fmt.Printf("💡 提示:请在浏览器中打开上述地址访问管理面板\n")
fmt.Println("")
return s.engine.Run(addr)
}
// loadSigningKey 加载或生成 Ed25519 签名密钥
func (s *Server) loadSigningKey() (ed25519.PrivateKey, error) {
var key model.SecurityKey
err := s.store.DB().Where("name = ?", "meshseed_signing").First(&key).Error
if err == nil {
// 从数据库加载已有密钥
keyBytes, decodeErr := base64.StdEncoding.DecodeString(key.Value)
if decodeErr != nil {
return nil, fmt.Errorf("解码密钥失败:%w", decodeErr)
}
return ed25519.PrivateKey(keyBytes), nil
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("查询密钥失败:%w", err)
}
// 密钥不存在,生成新密钥并保存
_, newKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, fmt.Errorf("生成密钥失败:%w", err)
}
keyBytes := []byte(newKey)
err = s.store.DB().Create(&model.SecurityKey{
Name: "meshseed_signing",
Value: base64.StdEncoding.EncodeToString(keyBytes),
Algorithm: "ed25519",
Purpose: "MeshSeed 数字签名",
}).Error
if err != nil {
return nil, fmt.Errorf("保存密钥失败:%w", err)
}
s.logger.Info("已生成新的签名密钥")
return newKey, nil
}