feat: 修复菜单路由+角色权限展示+操作审计+WebSSH终端审计
1. 菜单路由修复: /roles /users /logs-audit /terminal-audit 等系统管理子菜单 通过 useLocation 同步 URL 路径到 Tab 选中状态 2. WebSSH错误提示优化: Go error → 友好中文提示 i/o timeout → 连接超时(防火墙或网络不通) connection refused → 端口未开放/SSH服务未启动 cannot find session key → 密码或密钥认证失败 连接提示增加端口号显示 3. 角色权限展示: 31项权限种子数据 (资产管理/云资源/K8s/发布/监控/AIOps/系统管理) 4个角色权限分配: admin(31) ops(27) developer(16) viewer(3) 前端角色卡片显示权限名称而非原始code 4. 操作审计修复: 删除/创建/更新服务器均记录审计日志 批量删除逐条记录审计 5. WebSSH终端审计 (JumpServer模式): - 新增 TerminalSession 模型记录会话(用户/服务器/时间/状态) - webshell.go 录制终端输出(1MB上限) - 终端审计页面: 会话列表+状态筛选+批量删除 - 操作回放: xterm.js 重放录制的终端输出 - WS连接增加JWT鉴权+token传递
This commit is contained in:
1 parent
4797059be2
commit
bdc7a507c0
17 files changed
+985
-34
No files matched your search
@@ -20,6 +20,27 @@ func NewServerHandler(db *gorm.DB) *ServerHandler {
|
||||
return &ServerHandler{db: db}
|
||||
}
|
||||
|
||||
// recordServerAudit 记录服务器操作审计日志 // Logan.Li
|
||||
func (h *ServerHandler) recordServerAudit(c *gin.Context, action, resource string, resourceID string, status int, errMsg string) {
|
||||
userID, _ := c.Get("userID")
|
||||
username, _ := c.Get("username")
|
||||
uid, _ := userID.(uint)
|
||||
uname, _ := username.(string)
|
||||
|
||||
h.db.Create(&models.AuditLog{
|
||||
UserID: &uid,
|
||||
Username: uname,
|
||||
IP: c.ClientIP(),
|
||||
UserAgent: c.Request.UserAgent(),
|
||||
Module: "server",
|
||||
Action: action,
|
||||
Resource: resource,
|
||||
ResourceID: resourceID,
|
||||
Status: status,
|
||||
Error: errMsg,
|
||||
})
|
||||
}
|
||||
|
||||
// ListServers 获取服务器列表
|
||||
// @Summary 获取服务器列表
|
||||
// @Description 获取所有服务器列表
|
||||
@@ -170,6 +191,8 @@ func (h *ServerHandler) CreateServer(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
h.recordServerAudit(c, "create", server.Hostname+" ("+server.IP+")", strconv.Itoa(int(server.ID)), 1, "")
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
Code: 200,
|
||||
Message: "创建成功",
|
||||
@@ -268,6 +291,7 @@ func (h *ServerHandler) UpdateServer(c *gin.Context) {
|
||||
}
|
||||
|
||||
h.db.Preload("Region").Preload("Cloud").First(&server, id)
|
||||
h.recordServerAudit(c, "update", server.Hostname+" ("+server.IP+")", strconv.Itoa(int(server.ID)), 1, "")
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
Code: 200,
|
||||
@@ -288,6 +312,15 @@ func (h *ServerHandler) UpdateServer(c *gin.Context) {
|
||||
func (h *ServerHandler) DeleteServer(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var server models.Server
|
||||
if err := h.db.First(&server, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
Code: 404,
|
||||
Message: "服务器不存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.Delete(&models.Server{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
Code: 500,
|
||||
@@ -296,6 +329,8 @@ func (h *ServerHandler) DeleteServer(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
h.recordServerAudit(c, "delete", server.Hostname+" ("+server.IP+")", strconv.Itoa(int(server.ID)), 1, "")
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
Code: 200,
|
||||
Message: "删除成功",
|
||||
|
||||
@@ -3,6 +3,7 @@ package handlers
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"ezops/internal/models"
|
||||
@@ -106,6 +107,10 @@ func (h *ServerHandler) BatchDeleteServers(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 查询待删除服务器用于审计日志 // Logan.Li
|
||||
var servers []models.Server
|
||||
h.db.Where("id IN ?", req.IDs).Find(&servers)
|
||||
|
||||
if err := h.db.Delete(&models.Server{}, req.IDs).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
Code: 500,
|
||||
@@ -114,6 +119,11 @@ func (h *ServerHandler) BatchDeleteServers(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 记录批量删除审计日志
|
||||
for _, s := range servers {
|
||||
h.recordServerAudit(c, "delete", s.Hostname+" ("+s.IP+")", strconv.Itoa(int(s.ID)), 1, "")
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
Code: 200,
|
||||
Message: "删除成功",
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"ezops/internal/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// TerminalSessionHandler 终端会话审计处理器 // Logan.Li
|
||||
type TerminalSessionHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewTerminalSessionHandler 创建终端会话审计处理器
|
||||
func NewTerminalSessionHandler(db *gorm.DB) *TerminalSessionHandler {
|
||||
return &TerminalSessionHandler{db: db}
|
||||
}
|
||||
|
||||
// ListTerminalSessions 终端会话列表
|
||||
// GET /api/v1/terminal-sessions?server_id=&username=&status=&page=&page_size=
|
||||
func (h *TerminalSessionHandler) ListTerminalSessions(c *gin.Context) {
|
||||
var req models.TerminalSessionQuery
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
if req.Page <= 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize <= 0 {
|
||||
req.PageSize = 20
|
||||
}
|
||||
|
||||
query := h.db.Model(&models.TerminalSession{})
|
||||
if req.ServerID > 0 {
|
||||
query = query.Where("server_id = ?", req.ServerID)
|
||||
}
|
||||
if req.Username != "" {
|
||||
query = query.Where("username LIKE ?", "%"+req.Username+"%")
|
||||
}
|
||||
if req.Status != "" {
|
||||
query = query.Where("status = ?", req.Status)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "查询失败"})
|
||||
return
|
||||
}
|
||||
|
||||
var sessions []models.TerminalSession
|
||||
if err := query.Order("id DESC").
|
||||
Offset((req.Page - 1) * req.PageSize).
|
||||
Limit(req.PageSize).
|
||||
Find(&sessions).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "查询失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ListResponse{
|
||||
List: sessions,
|
||||
Total: total,
|
||||
}})
|
||||
}
|
||||
|
||||
// GetTerminalSession 终端会话详情
|
||||
// GET /api/v1/terminal-sessions/:id
|
||||
func (h *TerminalSessionHandler) GetTerminalSession(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的会话ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var session models.TerminalSession
|
||||
if err := h.db.First(&session, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "会话不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "查询失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: session})
|
||||
}
|
||||
|
||||
// GetTerminalRecord 终端会话录制数据(用于回放)
|
||||
// GET /api/v1/terminal-sessions/:id/record
|
||||
func (h *TerminalSessionHandler) GetTerminalRecord(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的会话ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var session models.TerminalSession
|
||||
if err := h.db.First(&session, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "会话不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "查询失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: session.RecordData})
|
||||
}
|
||||
|
||||
// DeleteTerminalSession 删除终端会话记录
|
||||
// DELETE /api/v1/terminal-sessions/:id
|
||||
func (h *TerminalSessionHandler) DeleteTerminalSession(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的会话ID"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.Delete(&models.TerminalSession{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"})
|
||||
}
|
||||
|
||||
// BatchDeleteTerminalSessions 批量删除终端会话记录
|
||||
// POST /api/v1/terminal-sessions/batch-delete
|
||||
func (h *TerminalSessionHandler) BatchDeleteTerminalSessions(c *gin.Context) {
|
||||
var req models.IDsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的请求参数"})
|
||||
return
|
||||
}
|
||||
|
||||
result := h.db.Where("id IN ?", req.IDs).Delete(&models.TerminalSession{})
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功", Data: gin.H{"deleted": result.RowsAffected}})
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -43,7 +44,44 @@ func NewWebShellHandler(db *gorm.DB) *WebShellHandler {
|
||||
return &WebShellHandler{db: db}
|
||||
}
|
||||
|
||||
// Terminal WebSocket终端连接 — 真实SSH // Logan.Li
|
||||
// formatSSHError 将 Go SSH 错误转换为友好的中文提示 // Logan.Li
|
||||
func formatSSHError(err error) string {
|
||||
errStr := err.Error()
|
||||
switch {
|
||||
case contains(errStr, "i/o timeout"):
|
||||
return "连接超时(目标主机可能在防火墙后或网络不通)"
|
||||
case contains(errStr, "connection refused"):
|
||||
return "连接被拒绝(SSH 服务未运行或端口错误)"
|
||||
case contains(errStr, "no route to host"):
|
||||
return "无法路由到目标主机(IP 地址不可达)"
|
||||
case contains(errStr, "authentication failed") || contains(errStr, "unable to authenticate"):
|
||||
return "认证失败(用户名或密码错误)"
|
||||
case contains(errStr, "handshake"):
|
||||
return "SSH 握手失败(目标可能不是 SSH 服务)"
|
||||
case contains(errStr, "dns") || contains(errStr, "no such host"):
|
||||
return "域名解析失败(IP 地址无效)"
|
||||
case contains(errStr, "ssh: must specify"):
|
||||
return "认证方式错误(未配置密码或密钥)"
|
||||
default:
|
||||
return errStr
|
||||
}
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
|
||||
(len(s) > 0 && len(substr) > 0 && indexOf(s, substr) >= 0))
|
||||
}
|
||||
|
||||
func indexOf(s, substr string) int {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// Terminal WebSocket终端连接 — 真实SSH,带终端会话录制 // Logan.Li
|
||||
func (h *WebShellHandler) Terminal(c *gin.Context) {
|
||||
serverID := c.Query("id")
|
||||
if serverID == "" {
|
||||
@@ -65,9 +103,51 @@ func (h *WebShellHandler) Terminal(c *gin.Context) {
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 创建终端会话记录(终端审计) // Logan.Li
|
||||
var userIDPtr *uint
|
||||
if uid, exists := c.Get("userID"); exists {
|
||||
if uidVal, ok := uid.(uint); ok {
|
||||
userIDPtr = &uidVal
|
||||
}
|
||||
}
|
||||
username, _ := c.Get("username")
|
||||
usernameStr, _ := username.(string)
|
||||
|
||||
termSession := models.TerminalSession{
|
||||
UserID: userIDPtr,
|
||||
Username: usernameStr,
|
||||
ServerID: server.ID,
|
||||
ServerName: server.Hostname,
|
||||
ServerIP: server.IP,
|
||||
ServerPort: server.Port,
|
||||
ClientIP: c.ClientIP(),
|
||||
Status: "active",
|
||||
StartTime: time.Now(),
|
||||
}
|
||||
if err := h.db.Create(&termSession).Error; err != nil {
|
||||
log.Printf("[WebShell] 创建会话记录失败: %v", err)
|
||||
}
|
||||
|
||||
// 终端录制缓冲(限制 1MB 避免内存问题) // Logan.Li
|
||||
const maxRecordSize = 1024 * 1024
|
||||
var recordBuf strings.Builder
|
||||
|
||||
// finalizeSession 结束会话记录(统一收尾)
|
||||
finalizeSession := func(status string) {
|
||||
endTime := time.Now()
|
||||
duration := int(endTime.Sub(termSession.StartTime).Seconds())
|
||||
h.db.Model(&termSession).Updates(map[string]interface{}{
|
||||
"status": status,
|
||||
"duration": duration,
|
||||
"end_time": &endTime,
|
||||
"record_data": recordBuf.String(),
|
||||
"record_size": recordBuf.Len(),
|
||||
})
|
||||
}
|
||||
|
||||
conn.WriteJSON(WSMessage{
|
||||
Status: 0,
|
||||
Data: "\r\n\x1b[36m正在连接 " + server.Hostname + " (" + server.IP + ")...\x1b[0m\r\n",
|
||||
Data: "\r\n\x1b[36m正在连接 " + server.Hostname + " (" + server.IP + ":" + strconv.Itoa(server.Port) + ")...\x1b[0m\r\n",
|
||||
})
|
||||
|
||||
// 构建 SSH 配置
|
||||
@@ -96,10 +176,16 @@ func (h *WebShellHandler) Terminal(c *gin.Context) {
|
||||
sshClient, err := ssh.Dial("tcp", addr, sshConfig)
|
||||
if err != nil {
|
||||
log.Printf("[WebShell] SSH连接失败 %s: %v", addr, err)
|
||||
conn.WriteJSON(WSMessage{
|
||||
Status: 0,
|
||||
Data: "\r\n\x1b[31mSSH连接失败: " + err.Error() + "\x1b[0m\r\n\x1b[33m提示: 请检查服务器IP/端口/密码是否正确,目标主机是否在线\x1b[0m\r\n",
|
||||
})
|
||||
friendlyErr := formatSSHError(err)
|
||||
errMsg := "\r\n\x1b[31m连接失败: " + friendlyErr + "\x1b[0m\r\n" +
|
||||
"\x1b[33m──────────────────────────\x1b[0m\r\n" +
|
||||
"\x1b[33m目标:\x1b[0m " + server.Hostname + " (" + server.IP + ":" + strconv.Itoa(server.Port) + ")\r\n" +
|
||||
"\x1b[33m账号:\x1b[0m " + server.Username + "\r\n" +
|
||||
"\x1b[33m排查:\x1b[0m ① 检查 IP/端口是否正确 ② 确认 SSH 服务运行中 ③ 验证账号密码\r\n" +
|
||||
"\x1b[33m──────────────────────────\x1b[0m\r\n"
|
||||
conn.WriteJSON(WSMessage{Status: 0, Data: errMsg})
|
||||
recordBuf.WriteString(errMsg)
|
||||
finalizeSession("failed")
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
@@ -112,6 +198,7 @@ func (h *WebShellHandler) Terminal(c *gin.Context) {
|
||||
if err != nil {
|
||||
log.Printf("[WebShell] 创建session失败: %v", err)
|
||||
conn.WriteJSON(WSMessage{Status: 0, Data: "\r\n\x1b[31m创建SSH会话失败\x1b[0m\r\n"})
|
||||
finalizeSession("failed")
|
||||
return
|
||||
}
|
||||
defer session.Close()
|
||||
@@ -125,45 +212,62 @@ func (h *WebShellHandler) Terminal(c *gin.Context) {
|
||||
if err := session.RequestPty("xterm-256color", 24, 80, modes); err != nil {
|
||||
log.Printf("[WebShell] PTY请求失败: %v", err)
|
||||
conn.WriteJSON(WSMessage{Status: 0, Data: "\r\n\x1b[31mPTY请求失败\x1b[0m\r\n"})
|
||||
finalizeSession("failed")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取管道
|
||||
stdinPipe, err := session.StdinPipe()
|
||||
if err != nil {
|
||||
finalizeSession("failed")
|
||||
return
|
||||
}
|
||||
stdoutPipe, err := session.StdoutPipe()
|
||||
if err != nil {
|
||||
finalizeSession("failed")
|
||||
return
|
||||
}
|
||||
|
||||
// 启动 shell
|
||||
if err := session.Shell(); err != nil {
|
||||
log.Printf("[WebShell] Shell启动失败: %v", err)
|
||||
finalizeSession("failed")
|
||||
return
|
||||
}
|
||||
|
||||
// 发送欢迎消息
|
||||
welcome := "\r\n\x1b[32mEZOps WebShell\x1b[0m — 已连接 " + server.Hostname + " (" + server.IP + ")\r\n\r\n"
|
||||
conn.WriteJSON(WSMessage{
|
||||
Status: 0,
|
||||
Data: "\r\n\x1b[32mEZOps WebShell\x1b[0m — 已连接 " + server.Hostname + " (" + server.IP + ")\r\n\r\n",
|
||||
Data: welcome,
|
||||
})
|
||||
recordBuf.WriteString(welcome)
|
||||
|
||||
done := make(chan struct{})
|
||||
|
||||
// goroutine: SSH stdout -> WebSocket
|
||||
// goroutine: SSH stdout -> WebSocket(同时录制输出) // Logan.Li
|
||||
go func() {
|
||||
defer close(done)
|
||||
buf := make([]byte, 8192)
|
||||
for {
|
||||
n, err := stdoutPipe.Read(buf)
|
||||
if err != nil {
|
||||
conn.WriteJSON(WSMessage{Status: 0, Data: "\r\n\x1b[31m[远程连接已断开]\x1b[0m\r\n"})
|
||||
discMsg := "\r\n\x1b[31m[远程连接已断开]\x1b[0m\r\n"
|
||||
conn.WriteJSON(WSMessage{Status: 0, Data: discMsg})
|
||||
recordBuf.WriteString(discMsg)
|
||||
break
|
||||
}
|
||||
if n > 0 {
|
||||
conn.WriteJSON(WSMessage{Status: 0, Data: string(buf[:n])})
|
||||
// 录制终端输出(受 1MB 上限保护)
|
||||
if recordBuf.Len() < maxRecordSize {
|
||||
remain := maxRecordSize - recordBuf.Len()
|
||||
if n > remain {
|
||||
recordBuf.Write(buf[:remain])
|
||||
} else {
|
||||
recordBuf.Write(buf[:n])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -195,5 +299,6 @@ func (h *WebShellHandler) Terminal(c *gin.Context) {
|
||||
// 等待 stdout goroutine 结束
|
||||
<-done
|
||||
stdinPipe.Close()
|
||||
finalizeSession("closed")
|
||||
log.Printf("[WebShell] 断开 %s (%s)", server.Hostname, addr)
|
||||
}
|
||||
@@ -39,9 +39,9 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) {
|
||||
})
|
||||
})
|
||||
|
||||
// WebSocket终端 (token via query param)
|
||||
// WebSocket终端 (JWT 鉴权,支持 ?token= 查询参数;终端会话录制) // Logan.Li
|
||||
webShellHandler := handlers.NewWebShellHandler(db)
|
||||
r.GET("/ws/terminal", webShellHandler.Terminal)
|
||||
r.GET("/ws/terminal", middleware.JWTAuth(&cfg.JWT), webShellHandler.Terminal)
|
||||
|
||||
// 公开路由
|
||||
authHandler := handlers.NewUserHandler(db, cfg)
|
||||
@@ -371,6 +371,17 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) {
|
||||
audit.POST("/logs/clear", auditHandler.ClearAuditLogs)
|
||||
}
|
||||
|
||||
// 终端会话审计路由(WebSSH 录制回放) // Logan.Li
|
||||
terminalSessionHandler := handlers.NewTerminalSessionHandler(db)
|
||||
ts := authorized.Group("/terminal-sessions")
|
||||
{
|
||||
ts.GET("", terminalSessionHandler.ListTerminalSessions)
|
||||
ts.POST("/batch-delete", terminalSessionHandler.BatchDeleteTerminalSessions)
|
||||
ts.GET("/:id", terminalSessionHandler.GetTerminalSession)
|
||||
ts.GET("/:id/record", terminalSessionHandler.GetTerminalRecord)
|
||||
ts.DELETE("/:id", terminalSessionHandler.DeleteTerminalSession)
|
||||
}
|
||||
|
||||
// Agent 上报路由(无需 JWT)
|
||||
agentHandler := handlers.NewAgentHandler(db)
|
||||
taskHandler := handlers.NewAgentTaskHandler(db)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// TerminalSession SSH终端会话记录(JumpServer 式终端审计) // Logan.Li
|
||||
type TerminalSession struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
UserID *uint `json:"user_id"`
|
||||
Username string `gorm:"size:64" json:"username"`
|
||||
ServerID uint `json:"server_id"`
|
||||
ServerName string `gorm:"size:128" json:"server_name"`
|
||||
ServerIP string `gorm:"size:64" json:"server_ip"`
|
||||
ServerPort int `json:"server_port"`
|
||||
ClientIP string `gorm:"size:64" json:"client_ip"`
|
||||
Status string `gorm:"size:32" json:"status"` // active, closed, failed
|
||||
Duration int `json:"duration"` // seconds
|
||||
RecordData string `gorm:"type:text" json:"-"` // 终端录制原始输出(不直接返回 JSON)
|
||||
RecordSize int `json:"record_size"` // 录制字节数
|
||||
StartTime time.Time `json:"start_time"`
|
||||
EndTime *time.Time `json:"end_time"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// TableName 表名
|
||||
func (TerminalSession) TableName() string {
|
||||
return "terminal_sessions"
|
||||
}
|
||||
|
||||
// TerminalSessionQuery 终端会话查询
|
||||
type TerminalSessionQuery struct {
|
||||
ServerID uint `form:"server_id"`
|
||||
Username string `form:"username"`
|
||||
Status string `form:"status"`
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"page_size,default=20"`
|
||||
}
|
||||
Reference in new issue
Block a user