- 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
546 lines
18 KiB
Go
546 lines
18 KiB
Go
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.html(SPA 路由支持)
|
||
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
|
||
}
|