initial: ZeroTier-like P2P mesh VPN server with multi-tenant Web UI

This commit is contained in:
xieyao
2026-06-17 03:50:23 +08:00
commit ee5f036325
74 changed files with 9517 additions and 0 deletions

62
internal/handler/admin.go Normal file
View File

@@ -0,0 +1,62 @@
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
"zeromesh/internal/database"
"zeromesh/internal/model"
"zeromesh/internal/service"
)
type AdminHandler struct {
authService *service.AuthService
db *database.DB
nodeService *service.NodeService
netService *service.NetworkService
}
func NewAdminHandler(authService *service.AuthService, db *database.DB, nodeService *service.NodeService, netService *service.NetworkService) *AdminHandler {
return &AdminHandler{
authService: authService,
db: db,
nodeService: nodeService,
netService: netService,
}
}
func (h *AdminHandler) Dashboard(c *gin.Context) {
uid := userID(c)
var authorizedMembers int64
if isAdmin(c) {
h.db.Model(&model.NetworkMember{}).Where("authorized = ?", true).Count(&authorizedMembers)
} else {
networks, _ := h.netService.ListNetworks(uid)
var ids []uint32
for _, n := range networks {
ids = append(ids, n.NetworkID)
}
if len(ids) > 0 {
h.db.Model(&model.NetworkMember{}).Where("authorized = ? AND network_id IN ?", true, ids).Count(&authorizedMembers)
}
}
nodes, _ := h.nodeService.ListNode(uid)
var online int64
for _, n := range nodes {
if n.Online {
online++
}
}
networks, _ := h.netService.ListNetworks(uid)
c.JSON(http.StatusOK, gin.H{
"authorized_members": authorizedMembers,
"nodes_total": len(nodes),
"nodes_online": online,
"networks_count": len(networks),
})
}

87
internal/handler/auth.go Normal file
View File

@@ -0,0 +1,87 @@
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
"zeromesh/internal/service"
)
type AuthHandler struct {
authService *service.AuthService
}
func NewAuthHandler(authService *service.AuthService) *AuthHandler {
return &AuthHandler{authService: authService}
}
type loginReq struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
type registerReq struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
func (h *AuthHandler) Login(c *gin.Context) {
var req loginReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
token, user, err := h.authService.Login(req.Username, req.Password)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"token": token,
"user": user,
})
}
func (h *AuthHandler) Register(c *gin.Context) {
var req registerReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user, token, err := h.authService.Register(req.Username, req.Password)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"token": token,
"user": user,
})
}
func (h *AuthHandler) InitAdmin(c *gin.Context) {
var req registerReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user, token, err := h.authService.InitAdmin(req.Username, req.Password)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"token": token,
"user": user,
})
}
func (h *AuthHandler) CheckAdmin(c *gin.Context) {
exists, err := h.authService.CheckAdmin()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"admin_exists": exists})
}

View File

@@ -0,0 +1,23 @@
package handler
import "github.com/gin-gonic/gin"
func userID(c *gin.Context) uint {
id, exists := c.Get("user_id")
if !exists {
return 0
}
uid, ok := id.(uint)
if !ok {
return 0
}
return uid
}
func isAdmin(c *gin.Context) bool {
role, exists := c.Get("role")
if !exists {
return false
}
return role.(string) == "admin"
}

75
internal/handler/log.go Normal file
View File

@@ -0,0 +1,75 @@
package handler
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"zeromesh/internal/service"
)
type LogHandler struct {
logService *service.LogService
}
func NewLogHandler(logService *service.LogService) *LogHandler {
return &LogHandler{logService: logService}
}
func (h *LogHandler) ListApiLogs(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
ip := c.Query("ip")
path := c.Query("path")
method := c.Query("method")
statusCode, _ := strconv.Atoi(c.Query("status_code"))
logs, total, err := h.logService.ListApiLogs(page, pageSize, ip, path, method, statusCode)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": logs, "total": total, "page": page, "page_size": pageSize})
}
func (h *LogHandler) ListErrorLogs(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
ip := c.Query("ip")
path := c.Query("path")
method := c.Query("method")
statusCode, _ := strconv.Atoi(c.Query("status_code"))
logs, total, err := h.logService.ListErrorLogs(page, pageSize, ip, path, method, statusCode)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": logs, "total": total, "page": page, "page_size": pageSize})
}
func (h *LogHandler) ListApiLogGrouped(c *gin.Context) {
groups, err := h.logService.ListApiLogGrouped()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": groups})
}
func (h *LogHandler) ClearApiLogs(c *gin.Context) {
if err := h.logService.ClearApiLogs(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "ok"})
}
func (h *LogHandler) ClearErrorLogs(c *gin.Context) {
if err := h.logService.ClearErrorLogs(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "ok"})
}

143
internal/handler/network.go Normal file
View File

@@ -0,0 +1,143 @@
package handler
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"zeromesh/internal/service"
)
type NetworkHandler struct {
networkService *service.NetworkService
}
func NewNetworkHandler(networkService *service.NetworkService) *NetworkHandler {
return &NetworkHandler{networkService: networkService}
}
type createNetworkReq struct {
Name string `json:"name" binding:"required"`
IPRange string `json:"ip_range"`
}
func (h *NetworkHandler) Create(c *gin.Context) {
var req createNetworkReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
uid := userID(c)
network, err := h.networkService.CreateNetwork(uid, req.Name, req.IPRange)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"network": network})
}
func (h *NetworkHandler) List(c *gin.Context) {
uid := userID(c)
if !isAdmin(c) {
networks, err := h.networkService.ListNetworks(uid)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"networks": networks})
return
}
networks, err := h.networkService.ListNetworks(0)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"networks": networks})
}
func (h *NetworkHandler) Get(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid network id"})
return
}
uid := userID(c)
network, err := h.networkService.GetNetwork(uint32(id), uid)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "network not found"})
return
}
c.JSON(http.StatusOK, gin.H{"network": network})
}
func (h *NetworkHandler) Delete(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid network id"})
return
}
uid := userID(c)
if err := h.networkService.DeleteNetwork(uint32(id), uid); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}
func (h *NetworkHandler) Members(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid network id"})
return
}
uid := userID(c)
members, err := h.networkService.ListMembers(uint32(id), uid)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"members": members})
}
type authorizeReq struct {
NodeID string `json:"node_id" binding:"required"`
}
func (h *NetworkHandler) Authorize(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid network id"})
return
}
var req authorizeReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
uid := userID(c)
if err := h.networkService.AuthorizeMember(uint32(id), req.NodeID, uid); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "member authorized"})
}
func (h *NetworkHandler) Deauthorize(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid network id"})
return
}
var req authorizeReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
uid := userID(c)
if err := h.networkService.DeauthorizeMember(uint32(id), req.NodeID, uid); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "member deauthorized"})
}

81
internal/handler/node.go Normal file
View File

@@ -0,0 +1,81 @@
package handler
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"zeromesh/internal/service"
)
type NodeHandler struct {
nodeService *service.NodeService
}
func NewNodeHandler(nodeService *service.NodeService) *NodeHandler {
return &NodeHandler{nodeService: nodeService}
}
type registerNodeReq struct {
NodeID string `json:"node_id" binding:"required"`
PublicKey string `json:"public_key" binding:"required"`
Name string `json:"name"`
IPAddress string `json:"ip_address"`
Port int `json:"port"`
Version string `json:"version"`
}
func (h *NodeHandler) Register(c *gin.Context) {
var req registerNodeReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
uid := userID(c)
node, err := h.nodeService.Register(req.NodeID, req.PublicKey, req.Name, req.IPAddress, req.Port, req.Version, uid)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"node": node})
}
func (h *NodeHandler) List(c *gin.Context) {
uid := userID(c)
if !isAdmin(c) {
nodes, err := h.nodeService.ListNode(uid)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"nodes": nodes})
return
}
nodes, err := h.nodeService.ListNode(0)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"nodes": nodes})
}
func (h *NodeHandler) ListOnline(c *gin.Context) {
uid := userID(c)
nodes, err := h.nodeService.ListOnline(uid)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"nodes": nodes})
}
func (h *NodeHandler) Heartbeat(c *gin.Context) {
nodeID := c.Param("node_id")
if err := h.nodeService.SetOnline(nodeID, true); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
time.Sleep(5 * time.Second)
c.JSON(http.StatusOK, gin.H{"status": "ok"})
}