0d84b07f68
CI / docker (push) Successful in 1m58s
Go + Vue 管理台:Agent 隧道、域名反代、IP 安全、访客验证与监控大盘。 Gitea Actions CI 多架构镜像;纯 Go SQLite、无 CGO Docker 构建。 Co-authored-by: Cursor <cursoragent@cursor.com>
175 lines
5.0 KiB
Go
175 lines
5.0 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/wormhole/wormhole/internal/auth"
|
|
"github.com/wormhole/wormhole/internal/store"
|
|
)
|
|
|
|
func (s *Server) dashboardRankings(c *gin.Context) {
|
|
rankType := c.DefaultQuery("type", "tunnels")
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "50"))
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if pageSize < 1 {
|
|
pageSize = 50
|
|
}
|
|
if pageSize > 500 {
|
|
pageSize = 500
|
|
}
|
|
offset := (page - 1) * pageSize
|
|
q := c.Query("q")
|
|
|
|
switch rankType {
|
|
case "tunnels":
|
|
items, total, err := s.store.ListTunnelsByFlow(pageSize, offset)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"items": items, "total": total})
|
|
case "hosts":
|
|
items, total, err := s.store.ListHostsByFlow(pageSize, offset)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"items": items, "total": total})
|
|
case "ips":
|
|
items, total, err := s.store.ListAllRequestIPs(pageSize, offset, q)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"items": requestIPViews(s.store, items), "total": total})
|
|
default:
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid type"})
|
|
}
|
|
}
|
|
|
|
func (s *Server) listAPIKeys(c *gin.Context) {
|
|
claims := getClaims(c)
|
|
keys, err := s.store.ListAPIKeys(claims.UserID, auth.IsAdmin(claims.Role))
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, keys)
|
|
}
|
|
|
|
func (s *Server) createAPIKey(c *gin.Context) {
|
|
claims := getClaims(c)
|
|
var req struct {
|
|
Name string `json:"name"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
|
|
return
|
|
}
|
|
key, plain, err := s.store.CreateAPIKey(req.Name, claims.UserID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"key": key,
|
|
"api_key": plain,
|
|
"note": "请妥善保存 API Key,仅显示一次",
|
|
})
|
|
}
|
|
|
|
func (s *Server) deleteAPIKey(c *gin.Context) {
|
|
claims := getClaims(c)
|
|
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
|
if err := s.store.DeleteAPIKey(uint(id), claims.UserID, auth.IsAdmin(claims.Role)); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
}
|
|
|
|
func (s *Server) pauseTunnel(c *gin.Context) {
|
|
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
|
claims := getClaims(c)
|
|
existing, err := s.store.GetTunnelByID(uint(id))
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
|
return
|
|
}
|
|
if err := store.ClientOwnerCheck(s.store, existing.ClientID, claims.UserID, auth.IsAdmin(claims.Role)); err != nil {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
_ = s.proxy.StopTunnel(uint(id))
|
|
existing.Status = false
|
|
existing.RunStatus = false
|
|
_ = s.store.UpdateTunnel(existing)
|
|
s.cache.UpsertTunnel(existing)
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
}
|
|
|
|
func (s *Server) resumeTunnel(c *gin.Context) {
|
|
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
|
claims := getClaims(c)
|
|
existing, err := s.store.GetTunnelByID(uint(id))
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
|
return
|
|
}
|
|
if err := store.ClientOwnerCheck(s.store, existing.ClientID, claims.UserID, auth.IsAdmin(claims.Role)); err != nil {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
existing.Status = true
|
|
_ = s.store.UpdateTunnel(existing)
|
|
s.cache.UpsertTunnel(existing)
|
|
_ = s.proxy.StartTunnel(uint(id))
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
}
|
|
|
|
func (s *Server) pauseHost(c *gin.Context) {
|
|
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
|
claims := getClaims(c)
|
|
existing, err := s.store.GetHostByID(uint(id))
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
|
return
|
|
}
|
|
if err := store.ClientOwnerCheck(s.store, existing.ClientID, claims.UserID, auth.IsAdmin(claims.Role)); err != nil {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
existing.Status = false
|
|
_ = s.store.UpdateHost(existing)
|
|
s.cache.UpsertHost(existing)
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
}
|
|
|
|
func (s *Server) resumeHost(c *gin.Context) {
|
|
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
|
claims := getClaims(c)
|
|
existing, err := s.store.GetHostByID(uint(id))
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
|
return
|
|
}
|
|
if err := store.ClientOwnerCheck(s.store, existing.ClientID, claims.UserID, auth.IsAdmin(claims.Role)); err != nil {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
existing.Status = true
|
|
_ = s.store.UpdateHost(existing)
|
|
s.cache.UpsertHost(existing)
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
}
|
|
|
|
func (s *Server) openapiDoc(c *gin.Context) {
|
|
c.Header("Content-Type", "application/json")
|
|
c.String(http.StatusOK, openAPISpec)
|
|
}
|