Files
Wormhole/internal/api/server.go
T
rose_cat707 0d84b07f68
CI / docker (push) Successful in 1m58s
feat: Wormhole 内网穿透与反向代理网关
Go + Vue 管理台:Agent 隧道、域名反代、IP 安全、访客验证与监控大盘。
Gitea Actions CI 多架构镜像;纯 Go SQLite、无 CGO Docker 构建。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 17:52:43 +08:00

922 lines
27 KiB
Go

package api
import (
"errors"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/wormhole/wormhole/internal/auth"
"github.com/wormhole/wormhole/internal/bridge"
"github.com/wormhole/wormhole/internal/cache"
"github.com/wormhole/wormhole/internal/config"
"github.com/wormhole/wormhole/internal/metrics"
"github.com/wormhole/wormhole/internal/proxy"
"github.com/wormhole/wormhole/internal/security"
"github.com/wormhole/wormhole/internal/store"
)
type Server struct {
cfg *config.Config
store *store.Store
cache *cache.Manager
auth *auth.Service
resAuth *auth.ResourceAuth
security *security.Service
bridge *bridge.Bridge
proxy *proxy.Manager
metrics *metrics.Collector
}
func NewServer(
cfg *config.Config,
st *store.Store,
c *cache.Manager,
authSvc *auth.Service,
resAuth *auth.ResourceAuth,
sec *security.Service,
b *bridge.Bridge,
pm *proxy.Manager,
mc *metrics.Collector,
) *Server {
return &Server{
cfg: cfg,
store: st,
cache: c,
auth: authSvc,
resAuth: resAuth,
security: sec,
bridge: b,
proxy: pm,
metrics: mc,
}
}
func (s *Server) Router() *gin.Engine {
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.Use(gin.Recovery(), gin.Logger())
api := r.Group("/api")
{
api.GET("/openapi.json", s.openapiDoc)
api.POST("/auth/login", s.login)
authGroup := api.Group("")
authGroup.Use(s.authMiddleware())
{
authGroup.GET("/auth/me", s.me)
authGroup.PUT("/auth/me/password", s.changeMyPassword)
authGroup.GET("/dashboard", s.dashboard)
authGroup.GET("/dashboard/rankings", s.dashboardRankings)
authGroup.GET("/api-keys", s.listAPIKeys)
authGroup.POST("/api-keys", s.createAPIKey)
authGroup.DELETE("/api-keys/:id", s.deleteAPIKey)
authGroup.GET("/clients", s.listClients)
authGroup.POST("/clients", s.createClient)
authGroup.PUT("/clients/:id", s.updateClient)
authGroup.DELETE("/clients/:id", s.deleteClient)
authGroup.GET("/tunnels", s.listTunnels)
authGroup.POST("/tunnels", s.createTunnel)
authGroup.PUT("/tunnels/:id", s.updateTunnel)
authGroup.DELETE("/tunnels/:id", s.deleteTunnel)
authGroup.POST("/tunnels/:id/start", s.startTunnel)
authGroup.POST("/tunnels/:id/stop", s.stopTunnel)
authGroup.POST("/tunnels/:id/pause", s.pauseTunnel)
authGroup.POST("/tunnels/:id/resume", s.resumeTunnel)
authGroup.GET("/tunnels/export", s.exportTunnels)
authGroup.POST("/tunnels/import", s.importTunnels)
authGroup.GET("/hosts", s.listHosts)
authGroup.POST("/hosts", s.createHost)
authGroup.PUT("/hosts/:id", s.updateHost)
authGroup.DELETE("/hosts/:id", s.deleteHost)
authGroup.POST("/hosts/:id/pause", s.pauseHost)
authGroup.POST("/hosts/:id/resume", s.resumeHost)
authGroup.GET("/hosts/export", s.exportHosts)
authGroup.POST("/hosts/import", s.importHosts)
authGroup.GET("/security/ip-rules", s.listIPRules)
authGroup.POST("/security/ip-rules", s.createIPRule)
authGroup.DELETE("/security/ip-rules/:id", s.deleteIPRule)
authGroup.GET("/security/request-ips", s.listRequestIPs)
authGroup.POST("/security/block-ip", s.blockIP)
authGroup.GET("/auth-policies", s.listAuthPolicies)
authGroup.POST("/auth-policies", s.createAuthPolicy)
authGroup.DELETE("/auth-policies/:id", s.deleteAuthPolicy)
authGroup.GET("/settings", s.getSettings)
authGroup.PUT("/settings", s.updateSettings)
authGroup.GET("/users", s.listUsers)
authGroup.GET("/users/:id/access-logs", s.listVisitorAccessLogs)
authGroup.POST("/users", s.createUser)
authGroup.PUT("/users/:id", s.updateUser)
authGroup.DELETE("/users/:id", s.deleteUser)
}
}
return r
}
func getClaims(c *gin.Context) *auth.Claims {
v, _ := c.Get("claims")
claims, _ := v.(*auth.Claims)
return claims
}
func (s *Server) login(c *gin.Context) {
var req struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
return
}
ip := auth.DirectRemoteIP(c.Request)
token, user, err := s.auth.Login(s.store, ip, req.Username, req.Password)
if err != nil {
var blocked *auth.LoginBlockedError
if errors.As(err, &blocked) {
secs := int(blocked.RetryAfter.Seconds())
if secs < 1 {
secs = 1
}
c.Header("Retry-After", strconv.Itoa(secs))
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "登录尝试次数过多,请稍后再试",
"retry_after": secs,
})
return
}
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"token": token,
"user": gin.H{
"id": user.ID,
"username": user.Username,
"role": user.Role,
},
})
}
func (s *Server) me(c *gin.Context) {
claims := getClaims(c)
c.JSON(http.StatusOK, gin.H{
"id": claims.UserID,
"username": claims.Username,
"role": claims.Role,
})
}
func (s *Server) changeMyPassword(c *gin.Context) {
claims := getClaims(c)
if claims.Role == "visitor" {
c.JSON(http.StatusForbidden, gin.H{"error": "访客账号无法修改管理密码"})
return
}
var req struct {
CurrentPassword string `json:"current_password"`
NewPassword string `json:"new_password"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
return
}
if req.CurrentPassword == "" || req.NewPassword == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "请输入当前密码和新密码"})
return
}
if len(req.NewPassword) < 4 {
c.JSON(http.StatusBadRequest, gin.H{"error": "新密码至少 4 位"})
return
}
user, err := s.store.GetUserByID(claims.UserID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
if !store.CheckPassword(user.PasswordHash, req.CurrentPassword) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "当前密码不正确"})
return
}
hash, err := store.HashPassword(req.NewPassword)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
user.PasswordHash = hash
if err := s.store.UpdateUser(user); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}
func (s *Server) dashboard(c *gin.Context) {
data, err := s.metrics.Dashboard()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if tops, ok := data["top_ips"].([]store.RequestIP); ok {
data["top_ips"] = requestIPViews(s.store, tops)
}
c.JSON(http.StatusOK, data)
}
func (s *Server) listClients(c *gin.Context) {
claims := getClaims(c)
clients, err := s.store.ListClients(claims.UserID, auth.IsAdmin(claims.Role))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
type clientView struct {
store.Client
Online bool `json:"online"`
}
out := make([]clientView, 0, len(clients))
for _, cl := range clients {
out = append(out, clientView{
Client: cl,
Online: s.bridge.IsOnline(cl.ID),
})
}
c.JSON(http.StatusOK, out)
}
func (s *Server) createClient(c *gin.Context) {
claims := getClaims(c)
var req store.Client
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
return
}
req.OwnerUserID = claims.UserID
if auth.IsAdmin(claims.Role) && req.OwnerUserID == 0 {
req.OwnerUserID = claims.UserID
}
if err := s.store.CreateClient(&req); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
s.cache.UpsertClient(&req)
c.JSON(http.StatusOK, req)
}
func (s *Server) updateClient(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
claims := getClaims(c)
if err := store.ClientOwnerCheck(s.store, uint(id), claims.UserID, auth.IsAdmin(claims.Role)); err != nil {
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return
}
var req store.Client
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
return
}
existing, err := s.store.GetClientByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
req.ID = existing.ID
req.VerifyKey = existing.VerifyKey
req.OwnerUserID = existing.OwnerUserID
if err := s.store.UpdateClient(&req); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
s.cache.UpsertClient(&req)
c.JSON(http.StatusOK, req)
}
func (s *Server) deleteClient(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
claims := getClaims(c)
if err := store.ClientOwnerCheck(s.store, uint(id), claims.UserID, auth.IsAdmin(claims.Role)); err != nil {
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return
}
if err := s.store.DeleteClient(uint(id)); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
s.cache.DeleteClient(uint(id))
c.JSON(http.StatusOK, gin.H{"ok": true})
}
func (s *Server) listTunnels(c *gin.Context) {
claims := getClaims(c)
clientID, _ := strconv.ParseUint(c.Query("client_id"), 10, 64)
p := pageParams(c)
tunnels, total, err := s.store.ListTunnelsPaged(uint(clientID), claims.UserID, auth.IsAdmin(claims.Role), p)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
items := make([]tunnelView, 0, len(tunnels))
for _, t := range tunnels {
view, err := s.enrichTunnelView(t)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
items = append(items, view)
}
c.JSON(http.StatusOK, gin.H{"items": items, "total": total})
}
func (s *Server) createTunnel(c *gin.Context) {
claims := getClaims(c)
var req tunnelRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
return
}
if err := store.ClientOwnerCheck(s.store, req.ClientID, claims.UserID, auth.IsAdmin(claims.Role)); err != nil {
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return
}
if req.Mode == "" {
req.Mode = "tcp"
}
req.ListenIP = "0.0.0.0"
if req.VisitorBindMode == "" {
req.VisitorBindMode = "all"
}
if req.VisitorTTLSeconds <= 0 {
req.VisitorTTLSeconds = 7200
}
if req.VisitorAuthEnabled && req.Mode == "udp" {
c.JSON(http.StatusBadRequest, gin.H{"error": "UDP 隧道不支持访客登录"})
return
}
inUse, err := s.store.TunnelPortInUse(req.ListenPort, 0)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if inUse {
c.JSON(http.StatusConflict, gin.H{"error": "监听端口已被占用"})
return
}
if err := s.store.CreateTunnel(&req.Tunnel); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err := s.saveResourceVisitors("tunnel", req.ID, req.VisitorAuthEnabled, req.VisitorBindMode, req.VisitorIDs); err != nil {
_ = s.store.DeleteTunnel(req.ID)
writeVisitorSaveError(c, err)
return
}
s.cache.UpsertTunnel(&req.Tunnel)
if req.Status {
_ = s.proxy.StartTunnel(req.ID)
}
view, _ := s.enrichTunnelView(req.Tunnel)
c.JSON(http.StatusOK, view)
}
func (s *Server) updateTunnel(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
}
var req tunnelRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
return
}
req.ID = existing.ID
req.ClientID = existing.ClientID
req.ListenIP = "0.0.0.0"
if req.VisitorBindMode == "" {
req.VisitorBindMode = "all"
}
if req.VisitorTTLSeconds <= 0 {
req.VisitorTTLSeconds = 7200
}
if req.VisitorAuthEnabled && req.Mode == "udp" {
c.JSON(http.StatusBadRequest, gin.H{"error": "UDP 隧道不支持访客登录"})
return
}
inUse, err := s.store.TunnelPortInUse(req.ListenPort, uint(id))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if inUse {
c.JSON(http.StatusConflict, gin.H{"error": "监听端口已被占用"})
return
}
if err := s.store.UpdateTunnel(&req.Tunnel); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err := s.saveResourceVisitors("tunnel", req.ID, req.VisitorAuthEnabled, req.VisitorBindMode, req.VisitorIDs); err != nil {
writeVisitorSaveError(c, err)
return
}
wasRunning := existing.RunStatus
s.cache.UpsertTunnel(&req.Tunnel)
if wasRunning || (req.Status && req.VisitorAuthEnabled != existing.VisitorAuthEnabled) {
_ = s.proxy.StopTunnel(uint(id))
if req.Status {
_ = s.proxy.StartTunnel(uint(id))
}
}
view, _ := s.enrichTunnelView(req.Tunnel)
c.JSON(http.StatusOK, view)
}
func (s *Server) deleteTunnel(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))
if err := s.store.DeleteTunnel(uint(id)); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
s.cache.DeleteTunnel(uint(id))
s.cache.DeleteResourceVisitors("tunnel", uint(id))
c.JSON(http.StatusOK, gin.H{"ok": true})
}
func (s *Server) startTunnel(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
}
if err := s.proxy.StartTunnel(uint(id)); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}
func (s *Server) stopTunnel(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
}
if err := s.proxy.StopTunnel(uint(id)); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}
func (s *Server) listHosts(c *gin.Context) {
claims := getClaims(c)
clientID, _ := strconv.ParseUint(c.Query("client_id"), 10, 64)
p := pageParams(c)
hosts, total, err := s.store.ListHostsPaged(uint(clientID), claims.UserID, auth.IsAdmin(claims.Role), p)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
items := make([]hostView, 0, len(hosts))
for _, h := range hosts {
view, err := s.enrichHostView(h)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
items = append(items, view)
}
c.JSON(http.StatusOK, gin.H{"items": items, "total": total})
}
func (s *Server) createHost(c *gin.Context) {
claims := getClaims(c)
var req hostRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
return
}
if err := store.ClientOwnerCheck(s.store, req.ClientID, claims.UserID, auth.IsAdmin(claims.Role)); err != nil {
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return
}
if req.Location == "" {
req.Location = "/"
}
if req.Scheme == "" {
req.Scheme = "all"
}
if req.VisitorBindMode == "" {
req.VisitorBindMode = "all"
}
if req.VisitorTTLSeconds <= 0 {
req.VisitorTTLSeconds = 7200
}
if err := s.store.HostRouteConflict(req.Host.Host, req.Location, 0); err != nil {
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
return
}
if err := s.store.CreateHost(&req.Host); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err := s.saveResourceVisitors("host", req.ID, req.VisitorAuthEnabled, req.VisitorBindMode, req.VisitorIDs); err != nil {
_ = s.store.DeleteHost(req.ID)
writeVisitorSaveError(c, err)
return
}
s.cache.UpsertHost(&req.Host)
view, _ := s.enrichHostView(req.Host)
c.JSON(http.StatusOK, view)
}
func (s *Server) updateHost(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
}
var req hostRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
return
}
req.ID = existing.ID
req.ClientID = existing.ClientID
if req.VisitorBindMode == "" {
req.VisitorBindMode = "all"
}
if req.VisitorTTLSeconds <= 0 {
req.VisitorTTLSeconds = 7200
}
if err := s.store.HostRouteConflict(req.Host.Host, req.Location, uint(id)); err != nil {
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
return
}
if err := s.store.UpdateHost(&req.Host); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err := s.saveResourceVisitors("host", req.ID, req.VisitorAuthEnabled, req.VisitorBindMode, req.VisitorIDs); err != nil {
writeVisitorSaveError(c, err)
return
}
s.cache.UpsertHost(&req.Host)
view, _ := s.enrichHostView(req.Host)
c.JSON(http.StatusOK, view)
}
func (s *Server) deleteHost(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
}
if err := s.store.DeleteHost(uint(id)); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
s.cache.DeleteHost(uint(id))
s.cache.DeleteResourceVisitors("host", uint(id))
c.JSON(http.StatusOK, gin.H{"ok": true})
}
func (s *Server) listIPRules(c *gin.Context) {
scope := c.Query("scope")
resourceID, _ := strconv.ParseUint(c.Query("resource_id"), 10, 64)
rules, err := s.store.ListIPRules(scope, uint(resourceID))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, rules)
}
func (s *Server) createIPRule(c *gin.Context) {
var req store.IPRule
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
return
}
if req.PatternKind == "" {
req.PatternKind = security.DetectPatternKind(req.Pattern)
}
if err := s.store.CreateIPRule(&req); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
_ = s.cache.ReloadIPRules(s.store)
c.JSON(http.StatusOK, req)
}
func (s *Server) deleteIPRule(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
if err := s.store.DeleteIPRule(uint(id)); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
_ = s.cache.ReloadIPRules(s.store)
c.JSON(http.StatusOK, gin.H{"ok": true})
}
func (s *Server) listRequestIPs(c *gin.Context) {
resourceType := c.Query("resource_type")
resourceID, _ := strconv.ParseUint(c.Query("resource_id"), 10, 64)
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50"))
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
q := c.Query("q")
if resourceType == "" && resourceID == 0 {
items, total, err := s.store.ListAllRequestIPs(limit, 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})
return
}
items, total, err := s.store.ListRequestIPs(resourceType, uint(resourceID), limit, offset)
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})
}
func (s *Server) blockIP(c *gin.Context) {
var req struct {
IP string `json:"ip"`
Scope string `json:"scope"`
ResourceType string `json:"resource_type"`
ResourceID uint `json:"resource_id"`
Remark string `json:"remark"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
return
}
scope := req.Scope
if scope == "" {
scope = req.ResourceType
}
if scope == "" {
scope = "global"
}
if err := s.security.BlockIP(req.IP, scope, req.ResourceID, req.Remark); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}
func (s *Server) listAuthPolicies(c *gin.Context) {
resourceType := c.Query("resource_type")
resourceID, _ := strconv.ParseUint(c.Query("resource_id"), 10, 64)
policies, err := s.store.ListAuthPolicies(resourceType, uint(resourceID))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, policies)
}
func (s *Server) createAuthPolicy(c *gin.Context) {
var req store.AuthPolicy
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
return
}
if err := s.resAuth.CreatePolicy(s.store, &req); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
_ = s.cache.ReloadAll(s.store)
c.JSON(http.StatusOK, req)
}
func (s *Server) deleteAuthPolicy(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
if err := s.store.DeleteAuthPolicy(uint(id)); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
_ = s.cache.ReloadAll(s.store)
c.JSON(http.StatusOK, gin.H{"ok": true})
}
func (s *Server) listUsers(c *gin.Context) {
claims := getClaims(c)
if !auth.IsAdmin(claims.Role) {
c.JSON(http.StatusForbidden, gin.H{"error": "admin only"})
return
}
users, err := s.store.ListVisitors()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, users)
}
func (s *Server) listVisitorAccessLogs(c *gin.Context) {
claims := getClaims(c)
if !auth.IsAdmin(claims.Role) {
c.JSON(http.StatusForbidden, gin.H{"error": "admin only"})
return
}
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
user, err := s.store.GetUserByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
if user.Role != "visitor" {
c.JSON(http.StatusBadRequest, gin.H{"error": "仅可查看访客访问记录"})
return
}
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "200"))
logs, err := s.store.ListVisitorAccessLogs(uint(id), limit)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
items := make([]gin.H, 0, len(logs))
for _, log := range logs {
items = append(items, gin.H{
"id": log.ID,
"user_id": log.UserID,
"resource_type": log.ResourceType,
"resource_id": log.ResourceID,
"resource_label": visitorResourceLabel(s.store, log.ResourceType, log.ResourceID),
"action": log.Action,
"method": log.Method,
"path": log.Path,
"ip": log.IP,
"user_agent": log.UserAgent,
"created_at": log.CreatedAt,
})
}
c.JSON(http.StatusOK, gin.H{"items": items, "total": len(items)})
}
func (s *Server) createUser(c *gin.Context) {
claims := getClaims(c)
if !auth.IsAdmin(claims.Role) {
c.JSON(http.StatusForbidden, gin.H{"error": "admin only"})
return
}
var req struct {
Username string `json:"username"`
Password string `json:"password"`
Role string `json:"role"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
return
}
hash, err := store.HashPassword(req.Password)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if req.Role == "" {
req.Role = "visitor"
}
if req.Role != "visitor" {
c.JSON(http.StatusBadRequest, gin.H{"error": "仅支持创建访客用户"})
return
}
user := store.User{
Username: req.Username,
PasswordHash: hash,
Role: req.Role,
Status: true,
}
if err := s.store.CreateUser(&user); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, user)
}
func (s *Server) updateUser(c *gin.Context) {
claims := getClaims(c)
if !auth.IsAdmin(claims.Role) {
c.JSON(http.StatusForbidden, gin.H{"error": "admin only"})
return
}
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
user, err := s.store.GetUserByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
if user.Role != "visitor" {
c.JSON(http.StatusBadRequest, gin.H{"error": "仅可编辑访客用户"})
return
}
var req struct {
Password string `json:"password"`
Role string `json:"role"`
Status *bool `json:"status"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
return
}
if req.Password != "" {
hash, err := store.HashPassword(req.Password)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
user.PasswordHash = hash
}
if req.Role != "" {
if req.Role != "visitor" {
c.JSON(http.StatusBadRequest, gin.H{"error": "仅支持访客用户"})
return
}
user.Role = req.Role
}
if req.Status != nil {
user.Status = *req.Status
}
if err := s.store.UpdateUser(user); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, user)
}
func (s *Server) deleteUser(c *gin.Context) {
claims := getClaims(c)
if !auth.IsAdmin(claims.Role) {
c.JSON(http.StatusForbidden, gin.H{"error": "admin only"})
return
}
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
user, err := s.store.GetUserByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
if user.Role != "visitor" {
c.JSON(http.StatusBadRequest, gin.H{"error": "仅可删除访客用户"})
return
}
if err := s.store.DeleteUser(uint(id), claims.UserID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}