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

550 lines
16 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 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,
},
})
}