feat: 新增管理员踢下线 VPN 客户端功能
- vpn/service.go: ClientInfo 增加 user_id 字段,新增 KickUser 方法 用读锁收集目标连接后逐个关闭 WebSocket,返回断开数量 - vpn/tunnel.go: info() 填充 UserID - handler/vpn.go: 新增 KickUserClient handler,校验用户存在后调用 KickUser - router.go: 注册 DELETE /api/admin/vpn/clients/:id - VpnView.vue: 在线客户端表格新增操作列与踢下线按钮,handleKick 确认后调用 API 并刷新 - zh.ts/en.ts: 新增 kick/confirmKick/kickFailed 国际化 key
This commit is contained in:
@@ -406,3 +406,26 @@ func DeleteVpnReservation(c *gin.Context) {
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
|
||||
}
|
||||
|
||||
func KickUserClient(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
var user model.User
|
||||
if err := db.DB.First(&user, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
if vpn.VPN == nil || !vpn.VPN.Running() {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "VPN 服务未运行", "kicked": 0})
|
||||
return
|
||||
}
|
||||
|
||||
n := vpn.VPN.KickUser(uint(id))
|
||||
c.JSON(http.StatusOK, gin.H{"message": "已断开用户连接", "kicked": n})
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ func Setup(r *gin.Engine) {
|
||||
admin.GET("/vpn/reservations", handler.ListVpnReservations)
|
||||
admin.POST("/vpn/reservations", handler.CreateVpnReservation)
|
||||
admin.DELETE("/vpn/reservations/:id", handler.DeleteVpnReservation)
|
||||
admin.DELETE("/vpn/clients/:id", handler.KickUserClient)
|
||||
}
|
||||
|
||||
distDir := http.Dir("./dist")
|
||||
|
||||
@@ -374,10 +374,26 @@ func (s *VpnService) ClientList() []ClientInfo {
|
||||
}
|
||||
|
||||
type ClientInfo struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
IP string `json:"ip"`
|
||||
IP6 string `json:"ip6,omitempty"`
|
||||
ConnectedAt string `json:"connected_at"`
|
||||
}
|
||||
|
||||
func (s *VpnService) KickUser(userID uint) int {
|
||||
s.mu.RLock()
|
||||
var toClose []*tunnelConn
|
||||
for c := range s.clients {
|
||||
if c.user.ID == userID {
|
||||
toClose = append(toClose, c)
|
||||
}
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
for _, c := range toClose {
|
||||
c.close()
|
||||
}
|
||||
return len(toClose)
|
||||
}
|
||||
|
||||
var VPN *VpnService
|
||||
|
||||
@@ -79,6 +79,7 @@ func (c *tunnelConn) close() {
|
||||
|
||||
func (c *tunnelConn) info() ClientInfo {
|
||||
ci := ClientInfo{
|
||||
UserID: c.user.ID,
|
||||
Username: c.user.Username,
|
||||
IP: c.assignedIP.String(),
|
||||
ConnectedAt: c.connectedAt.Format("2006-01-02 15:04:05"),
|
||||
|
||||
Reference in New Issue
Block a user