- 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
550 lines
16 KiB
Go
550 lines
16 KiB
Go
package handler
|
||
|
||
import (
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"git.zkcoi.com/zkcoi/Meshray-Manager/internal/api/dto"
|
||
"git.zkcoi.com/zkcoi/Meshray-Manager/internal/model"
|
||
"git.zkcoi.com/zkcoi/Meshray-Manager/internal/service"
|
||
sqlite "git.zkcoi.com/zkcoi/Meshray-Manager/internal/store/sqlite"
|
||
"github.com/gin-gonic/gin"
|
||
"go.uber.org/zap"
|
||
)
|
||
|
||
// NetworkHandler 网络管理 Handler
|
||
type NetworkHandler struct {
|
||
networkService *service.NetworkService
|
||
meshSeedService *service.MeshSeedService // 新增:MeshSeed 服务
|
||
store *sqlite.Store // 新增:用于存储 PendingJoin
|
||
logger *zap.Logger
|
||
}
|
||
|
||
// NewNetworkHandler 创建 Network Handler
|
||
func NewNetworkHandler(networkService *service.NetworkService, meshSeedService *service.MeshSeedService, store *sqlite.Store, logger *zap.Logger) *NetworkHandler {
|
||
return &NetworkHandler{
|
||
networkService: networkService,
|
||
meshSeedService: meshSeedService,
|
||
store: store,
|
||
logger: logger,
|
||
}
|
||
}
|
||
|
||
// CreateNetworkResponse 创建网络响应(包含完整配置信息)
|
||
type CreateNetworkResponse struct {
|
||
*model.Network
|
||
STUNServers []model.Service `json:"stun_servers"` // STUN 服务器列表
|
||
TURNServers []model.Service `json:"turn_servers"` // TURN 服务器列表
|
||
DDNSConfig *DDNSConfigInfo `json:"ddns_config,omitempty"` // DDNS 配置信息(如果启用)
|
||
}
|
||
|
||
// DDNSConfigInfo DDNS 配置信息
|
||
type DDNSConfigInfo struct {
|
||
Provider string `json:"provider"` // 服务商
|
||
Domain string `json:"domain"` // 域名
|
||
RecordType string `json:"record_type"` // 记录类型
|
||
Prefix string `json:"prefix"` // 前缀
|
||
}
|
||
|
||
// CreateNetwork 创建网络
|
||
// @Summary 创建新的 WireGuard 网络
|
||
// @Tags networks
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param network body model.Network true "网络配置"
|
||
// @Success 200 {object} CreateNetworkResponse
|
||
// @Router /api/v1/networks [post]
|
||
func (h *NetworkHandler) CreateNetwork(c *gin.Context) {
|
||
var req model.Network
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
|
||
return
|
||
}
|
||
|
||
// 使用 Service 层创建网络
|
||
network, err := h.networkService.CreateNetwork(&req)
|
||
if err != nil {
|
||
h.logger.Error("创建网络失败", zap.Error(err))
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
|
||
// 查询关联的 STUN/TURN 服务器
|
||
var stunServers []model.Service
|
||
h.store.DB().Where("type = 'STUN' AND enabled = true").Find(&stunServers)
|
||
|
||
var turnServers []model.Service
|
||
h.store.DB().Where("type = 'TURN' AND enabled = true").Find(&turnServers)
|
||
|
||
// 构建响应
|
||
resp := &CreateNetworkResponse{
|
||
Network: network,
|
||
STUNServers: stunServers,
|
||
TURNServers: turnServers,
|
||
}
|
||
|
||
// 如果启用了 DDNS,查询 DDNS 配置
|
||
if network.DDNSEnabled && network.DDNSServiceID != "" {
|
||
var ddnsService model.Service
|
||
if err := h.store.DB().First(&ddnsService, network.DDNSServiceID).Error; err == nil {
|
||
resp.DDNSConfig = &DDNSConfigInfo{
|
||
Provider: ddnsService.Provider,
|
||
Domain: ddnsService.Domain,
|
||
RecordType: ddnsService.RecordType,
|
||
Prefix: network.DDNSPrefix,
|
||
}
|
||
}
|
||
}
|
||
|
||
h.logger.Info("网络创建成功",
|
||
zap.String("name", network.Name),
|
||
zap.Uint64("id", network.ID),
|
||
zap.Int("stun_count", len(stunServers)),
|
||
zap.Int("turn_count", len(turnServers)))
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"message": "网络创建成功",
|
||
"data": resp,
|
||
})
|
||
}
|
||
|
||
// GetNetwork 获取网络详情
|
||
// @Summary 获取网络详细信息
|
||
// @Tags networks
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param id path string true "网络 ID"
|
||
// @Success 200 {object} model.Network
|
||
// @Router /api/v1/networks/:id [get]
|
||
func (h *NetworkHandler) GetNetwork(c *gin.Context) {
|
||
idStr := c.Param("id")
|
||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||
if err != nil {
|
||
h.logger.Error("解析网络 ID 失败", zap.Error(err))
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的网络 ID"})
|
||
return
|
||
}
|
||
|
||
// 使用 Service 层获取网络
|
||
network, err := h.networkService.GetNetwork(id)
|
||
if err != nil {
|
||
h.logger.Error("获取网络失败", zap.Error(err))
|
||
c.JSON(http.StatusNotFound, gin.H{"error": "网络不存在"})
|
||
return
|
||
}
|
||
|
||
// 使用 DTO 转换
|
||
resp := dto.ToNetworkResponse(network)
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"data": resp,
|
||
})
|
||
}
|
||
|
||
// ListNetworks 获取网络列表
|
||
// @Summary 获取所有网络列表
|
||
// @Tags networks
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Success 200 {array} model.Network
|
||
// @Router /api/v1/networks [get]
|
||
// ListNetworks 获取网络列表
|
||
func (h *NetworkHandler) ListNetworks(c *gin.Context) {
|
||
networks, err := h.networkService.ListNetworks()
|
||
if err != nil {
|
||
h.logger.Error("查询网络列表失败", zap.Error(err))
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询失败"})
|
||
return
|
||
}
|
||
|
||
// 使用 DTO 批量转换
|
||
respList := dto.ToNetworkResponseList(networks)
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"data": respList,
|
||
})
|
||
}
|
||
|
||
// DeleteNetwork 删除网络
|
||
// @Summary 删除指定网络
|
||
// @Tags networks
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param id path string true "网络 ID"
|
||
// @Success 200
|
||
// @Router /api/v1/networks/:id [delete]
|
||
func (h *NetworkHandler) DeleteNetwork(c *gin.Context) {
|
||
idStr := c.Param("id")
|
||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||
if err != nil {
|
||
h.logger.Error("解析网络 ID 失败", zap.Error(err))
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的网络 ID"})
|
||
return
|
||
}
|
||
|
||
// 获取是否强制级联删除
|
||
force := c.Query("force") == "true"
|
||
|
||
// 使用 Service 层删除网络
|
||
err = h.networkService.DeleteNetwork(id, force)
|
||
if err != nil {
|
||
h.logger.Error("删除网络失败", zap.Error(err))
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
|
||
h.logger.Info("网络已删除", zap.Uint64("id", id))
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"message": "删除成功",
|
||
})
|
||
}
|
||
|
||
// UpdateNetwork 更新网络
|
||
// @Summary 更新网络信息
|
||
// @Tags networks
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param id path string true "网络 ID"
|
||
// @Param network body model.Network true "网络配置"
|
||
// @Success 200 {object} model.Network
|
||
// @Router /api/v1/networks/:id [put]
|
||
func (h *NetworkHandler) UpdateNetwork(c *gin.Context) {
|
||
idStr := c.Param("id")
|
||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||
if err != nil {
|
||
h.logger.Error("解析网络 ID 失败", zap.Error(err))
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的网络 ID"})
|
||
return
|
||
}
|
||
|
||
var req model.Network
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
|
||
return
|
||
}
|
||
|
||
// 构建更新字段
|
||
updates := make(map[string]interface{})
|
||
if req.Name != "" {
|
||
updates["name"] = req.Name
|
||
}
|
||
if req.SubnetIPv4 != "" {
|
||
updates["subnetIPv4"] = req.SubnetIPv4
|
||
}
|
||
if req.SubnetIPv6 != "" {
|
||
updates["subnetIPv6"] = req.SubnetIPv6
|
||
}
|
||
if req.Mode != "" {
|
||
updates["mode"] = req.Mode
|
||
}
|
||
updates["dhcpEnabled"] = req.DHCPEnabled
|
||
|
||
// 使用 Service 层更新
|
||
network, err := h.networkService.UpdateNetwork(id, updates)
|
||
if err != nil {
|
||
h.logger.Error("更新网络失败", zap.Error(err))
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
|
||
h.logger.Info("网络更新成功", zap.Uint64("id", id))
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"message": "更新成功",
|
||
"data": network,
|
||
})
|
||
}
|
||
|
||
// StartNetwork 启动网络(创建 WG 设备)
|
||
// @Summary 启动网络
|
||
// @Tags networks
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param id path string true "网络 ID"
|
||
// @Success 200
|
||
// @Router /api/v1/networks/:id/start [post]
|
||
func (h *NetworkHandler) StartNetwork(c *gin.Context) {
|
||
idStr := c.Param("id")
|
||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||
if err != nil {
|
||
h.logger.Error("解析网络 ID 失败", zap.Error(err))
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的网络 ID"})
|
||
return
|
||
}
|
||
|
||
// 使用 Service 层启动网络
|
||
err = h.networkService.StartNetwork(id)
|
||
if err != nil {
|
||
h.logger.Error("启动网络失败", zap.Error(err))
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
|
||
h.logger.Info("网络启动成功", zap.Uint64("id", id))
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"message": "启动成功",
|
||
})
|
||
}
|
||
|
||
// StopNetwork 停止网络
|
||
// @Summary 停止网络
|
||
// @Tags networks
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param id path string true "网络 ID"
|
||
// @Success 200
|
||
// @Router /api/v1/networks/:id/stop [post]
|
||
func (h *NetworkHandler) StopNetwork(c *gin.Context) {
|
||
idStr := c.Param("id")
|
||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||
if err != nil {
|
||
h.logger.Error("解析网络 ID 失败", zap.Error(err))
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的网络 ID"})
|
||
return
|
||
}
|
||
|
||
// 使用 Service 层停止网络
|
||
err = h.networkService.StopNetwork(id)
|
||
if err != nil {
|
||
h.logger.Error("停止网络失败", zap.Error(err))
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
|
||
h.logger.Info("网络已停止", zap.Uint64("id", id))
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"message": "停止成功",
|
||
})
|
||
}
|
||
|
||
// SwitchMode 切换组网模式
|
||
// @Summary 切换组网模式
|
||
// @Tags networks
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param id path string true "网络 ID"
|
||
// @Param mode body object{mesh_mode string} true "组网模式 (native|enhanced)"
|
||
// @Success 200
|
||
// @Router /api/v1/networks/:id/switch-mode [post]
|
||
func (h *NetworkHandler) SwitchMode(c *gin.Context) {
|
||
idStr := c.Param("id")
|
||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||
if err != nil {
|
||
h.logger.Error("解析网络 ID 失败", zap.Error(err))
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的网络 ID"})
|
||
return
|
||
}
|
||
|
||
var req struct {
|
||
MeshMode string `json:"mesh_mode"`
|
||
}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
|
||
return
|
||
}
|
||
|
||
// 使用 Service 层切换模式
|
||
err = h.networkService.SwitchMode(id, req.MeshMode)
|
||
if err != nil {
|
||
h.logger.Error("切换模式失败", zap.Error(err))
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
|
||
h.logger.Info("模式切换成功", zap.Uint64("id", id), zap.String("mesh_mode", req.MeshMode))
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"message": "切换成功",
|
||
})
|
||
}
|
||
|
||
// GenerateMeshSeed 生成 MeshSeed
|
||
// @Summary 生成 MeshSeed
|
||
// @Tags networks
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param id path string true "网络 ID"
|
||
// @Param params body object true "生成参数"
|
||
// @Success 200
|
||
// @Router /api/v1/networks/:id/meshseed [post]
|
||
func (h *NetworkHandler) GenerateMeshSeed(c *gin.Context) {
|
||
idStr := c.Param("id")
|
||
networkID, err := strconv.ParseUint(idStr, 10, 64)
|
||
if err != nil {
|
||
h.logger.Error("解析网络 ID 失败", zap.Error(err))
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的网络 ID"})
|
||
return
|
||
}
|
||
|
||
var req struct {
|
||
ExpiresInHours int `json:"expires_in_hours"`
|
||
MaxUses int `json:"max_uses"`
|
||
DDNSEnabled bool `json:"ddns_enabled"`
|
||
}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
|
||
return
|
||
}
|
||
|
||
// 默认值
|
||
if req.ExpiresInHours <= 0 {
|
||
req.ExpiresInHours = 24 // 默认 24 小时
|
||
}
|
||
if req.MaxUses <= 0 {
|
||
req.MaxUses = 10 // 默认 10 次
|
||
}
|
||
|
||
// 计算过期时间
|
||
expiresAt := time.Now().Add(time.Duration(req.ExpiresInHours) * time.Hour)
|
||
|
||
// 调用 MeshSeedService 生成真实的 MeshSeed
|
||
meshSeed, err := h.meshSeedService.GenerateMeshSeed(networkID, req.MaxUses, expiresAt, req.DDNSEnabled)
|
||
if err != nil {
|
||
h.logger.Error("生成 MeshSeed 失败", zap.Error(err))
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
|
||
// 返回完整的 MeshSeed URL(包含 JoinToken 和 Signature)
|
||
seedString := "meshray://" + meshSeed.JoinToken + "." + meshSeed.Signature
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"data": gin.H{
|
||
"seed_string": seedString,
|
||
"expires_at": meshSeed.ExpiresAt.Format(time.RFC3339),
|
||
"max_uses": meshSeed.MaxUses,
|
||
"ddns_enabled": meshSeed.DDNSEnabled,
|
||
"used_count": meshSeed.UsedCount,
|
||
"revoked": meshSeed.Revoked,
|
||
"issued_at": time.Now().Format(time.RFC3339),
|
||
"remaining_uses": meshSeed.MaxUses - meshSeed.UsedCount,
|
||
},
|
||
})
|
||
}
|
||
|
||
// PreviewMeshSeed 预览 MeshSeed 信息
|
||
// @Summary 预览 MeshSeed
|
||
// @Tags networks
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param req body object{seed string} true "MeshSeed 字符串"
|
||
// @Success 200
|
||
// @Router /api/v1/networks/preview [post]
|
||
func (h *NetworkHandler) PreviewMeshSeed(c *gin.Context) {
|
||
var req struct {
|
||
Seed string `json:"seed"`
|
||
}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
|
||
return
|
||
}
|
||
|
||
// 解析 seed_string
|
||
// 格式: meshray://<joinToken>.<signature>
|
||
const prefix = "meshray://"
|
||
if len(req.Seed) <= len(prefix) || string(req.Seed[:len(prefix)]) != prefix {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "MeshSeed 格式不正确"})
|
||
return
|
||
}
|
||
|
||
seedContent := req.Seed[len(prefix):]
|
||
parts := strings.SplitN(seedContent, ".", 2)
|
||
if len(parts) != 2 {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "MeshSeed 内容不完整"})
|
||
return
|
||
}
|
||
joinToken, signature := parts[0], parts[1]
|
||
|
||
meshSeed, err := h.meshSeedService.VerifyMeshSeed(joinToken, signature)
|
||
if err != nil {
|
||
h.logger.Error("MeshSeed 验证失败", zap.Error(err))
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
|
||
// 查询网络名称和信息
|
||
network, err := h.networkService.GetNetworkByID(meshSeed.NetworkID)
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取网络信息失败"})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"data": gin.H{
|
||
"name": network.Name,
|
||
"subnet_ipv4": network.SubnetIPv4,
|
||
"mesh_mode": network.Mode,
|
||
// 在该应用逻辑中,如果是基于 MeshSeed 加入的设备默认进入待审核状态
|
||
"require_approval": true,
|
||
},
|
||
})
|
||
}
|
||
|
||
// JoinNetwork 加入网络
|
||
// @Summary 加入网络
|
||
// @Tags networks
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param req body object{seed string, device_name string, message string} true "请求参数"
|
||
// @Success 200
|
||
// @Router /api/v1/networks/join [post]
|
||
func (h *NetworkHandler) JoinNetwork(c *gin.Context) {
|
||
var req struct {
|
||
Seed string `json:"seed"`
|
||
DeviceName string `json:"device_name"`
|
||
Message string `json:"message"`
|
||
}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
|
||
return
|
||
}
|
||
|
||
const prefix = "meshray://"
|
||
if len(req.Seed) <= len(prefix) || string(req.Seed[:len(prefix)]) != prefix {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "MeshSeed 格式不正确"})
|
||
return
|
||
}
|
||
|
||
seedContent := req.Seed[len(prefix):]
|
||
parts := strings.SplitN(seedContent, ".", 2)
|
||
if len(parts) != 2 {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "MeshSeed 内容不完整"})
|
||
return
|
||
}
|
||
joinToken, signature := parts[0], parts[1]
|
||
|
||
meshSeed, err := h.meshSeedService.VerifyMeshSeed(joinToken, signature)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
|
||
// 记录一次使用次数
|
||
if err := h.meshSeedService.IncrementUseCount(meshSeed.SeedID); err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "处理失败"})
|
||
return
|
||
}
|
||
|
||
// 在目前的 handler 这里,直接通过 store 存储记录
|
||
pendingJoin := &model.PendingJoin{
|
||
SeedID: meshSeed.SeedID,
|
||
DeviceName: req.DeviceName,
|
||
RequestIP: c.ClientIP(),
|
||
Status: "pending",
|
||
ExpireAt: time.Now().Add(72 * time.Hour), // 72小时过期
|
||
}
|
||
|
||
// 存入数据库
|
||
db := h.store.DB()
|
||
if db != nil {
|
||
db.Create(pendingJoin)
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"message": "申请已提交,等待管理员审核",
|
||
"data": gin.H{
|
||
"success": true,
|
||
"needApproval": true,
|
||
},
|
||
})
|
||
}
|