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}) }