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://. 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, }, }) }