Initial commit

This commit is contained in:
2026-06-30 15:14:37 +08:00
commit 15dab96872
311 changed files with 95639 additions and 0 deletions
+549
View File
@@ -0,0 +1,549 @@
package handler
import (
"net/http"
"strconv"
"strings"
"time"
"git.zkcoi.com/zkcoi/meshray/internal/api/dto"
"git.zkcoi.com/zkcoi/meshray/internal/model"
"git.zkcoi.com/zkcoi/meshray/internal/service"
sqlite "git.zkcoi.com/zkcoi/meshray/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,
},
})
}