feat: 完善日志审计功能

- 实现文件系统日志(FilesystemLog)记录文件管理器操作
- 实现操作日志(OperationLog)记录用户操作行为
- 实现数据库SQL日志(DatabaseSQLLog)模型和API
- 实现SSH会话命令记录(SessionCommand)含命令输出和风险等级
- 添加IP提取服务支持X-Real-IP和X-Forwarded-For
- 添加日志自动清理功能
- 修复ProFormSwitch required验证问题
- 修复设置页面默认值问题
- 修复文件上传错误检测逻辑
- 修复资产树key前缀问题
- 添加VNC/RDP设置默认值
- 修复文件管理标题翻译
This commit is contained in:
2026-04-19 06:57:42 +08:00
parent a2a1613384
commit 1f7c491048
42 changed files with 1214 additions and 130 deletions
+7 -6
View File
@@ -30,7 +30,8 @@ func (api AccountApi) LoginEndpoint(c echo.Context) error {
} }
// 存储登录失败次数信息 // 存储登录失败次数信息
loginFailCountKey := c.RealIP() + loginAccount.Username clientIP := service.PropertyService.GetClientIP(c)
loginFailCountKey := clientIP + loginAccount.Username
v, ok := cache.LoginFailedKeyManager.Get(loginFailCountKey) v, ok := cache.LoginFailedKeyManager.Get(loginFailCountKey)
if !ok { if !ok {
v = 1 v = 1
@@ -49,7 +50,7 @@ func (api AccountApi) LoginEndpoint(c echo.Context) error {
count++ count++
cache.LoginFailedKeyManager.Set(loginFailCountKey, count, cache.LoginLockExpiration) cache.LoginFailedKeyManager.Set(loginFailCountKey, count, cache.LoginLockExpiration)
// 保存登录日志 // 保存登录日志
if err := service.UserService.SaveLoginLog(c.RealIP(), c.Request().UserAgent(), loginAccount.Username, false, loginAccount.Remember, "", "账号或密码不正确"); err != nil { if err := service.UserService.SaveLoginLog(clientIP, c.Request().UserAgent(), loginAccount.Username, false, loginAccount.Remember, "", "账号或密码不正确"); err != nil {
return err return err
} }
return FailWithData(c, -1, "您输入的账号或密码不正确", count) return FailWithData(c, -1, "您输入的账号或密码不正确", count)
@@ -63,7 +64,7 @@ func (api AccountApi) LoginEndpoint(c echo.Context) error {
count++ count++
cache.LoginFailedKeyManager.Set(loginFailCountKey, count, cache.LoginLockExpiration) cache.LoginFailedKeyManager.Set(loginFailCountKey, count, cache.LoginLockExpiration)
// 保存登录日志 // 保存登录日志
if err := service.UserService.SaveLoginLog(c.RealIP(), c.Request().UserAgent(), loginAccount.Username, false, loginAccount.Remember, "", "账号或密码不正确"); err != nil { if err := service.UserService.SaveLoginLog(clientIP, c.Request().UserAgent(), loginAccount.Username, false, loginAccount.Remember, "", "账号或密码不正确"); err != nil {
return err return err
} }
return FailWithData(c, -1, "您输入的账号或密码不正确", count) return FailWithData(c, -1, "您输入的账号或密码不正确", count)
@@ -78,7 +79,7 @@ func (api AccountApi) LoginEndpoint(c echo.Context) error {
count++ count++
cache.LoginFailedKeyManager.Set(loginFailCountKey, count, cache.LoginLockExpiration) cache.LoginFailedKeyManager.Set(loginFailCountKey, count, cache.LoginLockExpiration)
// 保存登录日志 // 保存登录日志
if err := service.UserService.SaveLoginLog(c.RealIP(), c.Request().UserAgent(), loginAccount.Username, false, loginAccount.Remember, "", "双因素认证授权码不正确"); err != nil { if err := service.UserService.SaveLoginLog(clientIP, c.Request().UserAgent(), loginAccount.Username, false, loginAccount.Remember, "", "双因素认证授权码不正确"); err != nil {
return err return err
} }
return FailWithData(c, -1, "您输入双因素认证授权码不正确", count) return FailWithData(c, -1, "您输入双因素认证授权码不正确", count)
@@ -86,12 +87,12 @@ func (api AccountApi) LoginEndpoint(c echo.Context) error {
} }
} }
token, err := api.LoginSuccess(loginAccount, user, c.RealIP()) token, err := api.LoginSuccess(loginAccount, user, clientIP)
if err != nil { if err != nil {
return err return err
} }
// 保存登录日志 // 保存登录日志
if err := service.UserService.SaveLoginLog(c.RealIP(), c.Request().UserAgent(), loginAccount.Username, true, loginAccount.Remember, token, ""); err != nil { if err := service.UserService.SaveLoginLog(clientIP, c.Request().UserAgent(), loginAccount.Username, true, loginAccount.Remember, token, ""); err != nil {
return err return err
} }
+23
View File
@@ -21,6 +21,21 @@ import (
type AssetApi struct{} type AssetApi struct{}
func recordOperationLog(c echo.Context, action, content, status, errorMessage string) {
account, _ := GetCurrentAccount(c)
clientIP := service.PropertyService.GetClientIP(c)
_ = service.OperationLogService.Record(context.TODO(), service.OperationLogParams{
AccountId: account.ID,
AccountName: account.Username,
Action: action,
Content: content,
IP: clientIP,
Status: status,
ErrorMessage: errorMessage,
UserAgent: c.Request().UserAgent(),
})
}
func (assetApi AssetApi) AssetCreateEndpoint(c echo.Context) error { func (assetApi AssetApi) AssetCreateEndpoint(c echo.Context) error {
m := maps.Map{} m := maps.Map{}
if err := c.Bind(&m); err != nil { if err := c.Bind(&m); err != nil {
@@ -30,10 +45,13 @@ func (assetApi AssetApi) AssetCreateEndpoint(c echo.Context) error {
account, _ := GetCurrentAccount(c) account, _ := GetCurrentAccount(c)
m["owner"] = account.ID m["owner"] = account.ID
assetName, _ := m["name"].(string)
if _, err := service.AssetService.Create(context.TODO(), m); err != nil { if _, err := service.AssetService.Create(context.TODO(), m); err != nil {
recordOperationLog(c, "asset-add", "创建资产: "+assetName, "failed", err.Error())
return err return err
} }
recordOperationLog(c, "asset-add", "创建资产: "+assetName, "success", "")
return Success(c, nil) return Success(c, nil)
} }
@@ -167,9 +185,12 @@ func (assetApi AssetApi) AssetUpdateEndpoint(c echo.Context) error {
if err := c.Bind(&m); err != nil { if err := c.Bind(&m); err != nil {
return err return err
} }
assetName, _ := m["name"].(string)
if err := service.AssetService.UpdateById(id, m); err != nil { if err := service.AssetService.UpdateById(id, m); err != nil {
recordOperationLog(c, "asset-edit", "更新资产: "+assetName, "failed", err.Error())
return err return err
} }
recordOperationLog(c, "asset-edit", "更新资产: "+assetName, "success", "")
return Success(c, nil) return Success(c, nil)
} }
@@ -178,10 +199,12 @@ func (assetApi AssetApi) AssetDeleteEndpoint(c echo.Context) error {
split := strings.Split(id, ",") split := strings.Split(id, ",")
for i := range split { for i := range split {
if err := service.AssetService.DeleteById(split[i]); err != nil { if err := service.AssetService.DeleteById(split[i]); err != nil {
recordOperationLog(c, "asset-del", "删除资产: "+id, "failed", err.Error())
return err return err
} }
} }
recordOperationLog(c, "asset-del", "删除资产: "+id, "success", "")
return Success(c, nil) return Success(c, nil)
} }
+2 -2
View File
@@ -127,7 +127,7 @@ func buildAssetTree(assets []model.Asset, groups []model.AssetGroup, groupId str
node := maps.Map{ node := maps.Map{
"id": g.ID, "id": g.ID,
"name": g.Name, "name": g.Name,
"key": g.ID, "key": "group_" + g.ID,
"title": g.Name, "title": g.Name,
"value": g.ID, "value": g.ID,
} }
@@ -144,7 +144,7 @@ func buildAssetTree(assets []model.Asset, groups []model.AssetGroup, groupId str
nodes = append(nodes, maps.Map{ nodes = append(nodes, maps.Map{
"id": a.ID, "id": a.ID,
"name": a.Name, "name": a.Name,
"key": a.ID, "key": "asset_" + a.ID,
"title": a.Name, "title": a.Name,
"value": a.ID, "value": a.ID,
"isLeaf": true, "isLeaf": true,
+45
View File
@@ -1,10 +1,15 @@
package api package api
import ( import (
"context"
"fmt" "fmt"
"next-terminal/server/common"
"next-terminal/server/common/maps" "next-terminal/server/common/maps"
"next-terminal/server/global/session" "next-terminal/server/global/session"
"next-terminal/server/log" "next-terminal/server/log"
"next-terminal/server/model"
"next-terminal/server/repository"
"next-terminal/server/utils"
"os" "os"
"path" "path"
"strconv" "strconv"
@@ -17,6 +22,28 @@ import (
type FileSystemApi struct{} type FileSystemApi struct{}
func (api FileSystemApi) recordFilesystemLog(ctx context.Context, sessionId, action, fileName string) {
sess, err := repository.SessionRepository.FindById(ctx, sessionId)
if err != nil {
log.Error("Failed to find session for filesystem log", log.NamedError("err", err))
return
}
filesystemLog := &model.FilesystemLog{
ID: utils.UUID(),
AssetId: sess.AssetId,
SessionId: sessionId,
UserId: sess.Creator,
Action: action,
FileName: fileName,
Created: common.NowJsonTime(),
}
if err := repository.FilesystemLogRepository.Create(ctx, filesystemLog); err != nil {
log.Error("Failed to create filesystem log", log.NamedError("err", err))
}
}
func (api FileSystemApi) LsEndpoint(c echo.Context) error { func (api FileSystemApi) LsEndpoint(c echo.Context) error {
sessionId := c.Param("id") sessionId := c.Param("id")
dir := c.QueryParam("dir") dir := c.QueryParam("dir")
@@ -84,6 +111,8 @@ func (api FileSystemApi) MkdirEndpoint(c echo.Context) error {
return Fail(c, -1, "failed to create directory: "+err.Error()) return Fail(c, -1, "failed to create directory: "+err.Error())
} }
go api.recordFilesystemLog(context.TODO(), sessionId, "mkdir", dir)
return Success(c, nil) return Success(c, nil)
} }
@@ -110,6 +139,8 @@ func (api FileSystemApi) TouchEndpoint(c echo.Context) error {
} }
f.Close() f.Close()
go api.recordFilesystemLog(context.TODO(), sessionId, "touch", filename)
return Success(c, nil) return Success(c, nil)
} }
@@ -150,6 +181,8 @@ func (api FileSystemApi) RmEndpoint(c echo.Context) error {
} }
} }
go api.recordFilesystemLog(context.TODO(), sessionId, "rm", filename)
return Success(c, nil) return Success(c, nil)
} }
@@ -200,6 +233,8 @@ func (api FileSystemApi) RenameEndpoint(c echo.Context) error {
return Fail(c, -1, "failed to rename: "+err.Error()) return Fail(c, -1, "failed to rename: "+err.Error())
} }
go api.recordFilesystemLog(context.TODO(), sessionId, "rename", oldName+" -> "+newName)
return Success(c, nil) return Success(c, nil)
} }
@@ -238,6 +273,8 @@ func (api FileSystemApi) EditEndpoint(c echo.Context) error {
return Fail(c, -1, "failed to write file: "+err.Error()) return Fail(c, -1, "failed to write file: "+err.Error())
} }
go api.recordFilesystemLog(context.TODO(), sessionId, "edit", req.Filename)
return Success(c, nil) return Success(c, nil)
} }
@@ -269,6 +306,8 @@ func (api FileSystemApi) ReadEndpoint(c echo.Context) error {
return Fail(c, -1, "failed to read file: "+err.Error()) return Fail(c, -1, "failed to read file: "+err.Error())
} }
go api.recordFilesystemLog(context.TODO(), sessionId, "read", filename)
return Success(c, maps.Map{ return Success(c, maps.Map{
"content": content, "content": content,
"path": filename, "path": filename,
@@ -322,6 +361,8 @@ func (api FileSystemApi) ChmodEndpoint(c echo.Context) error {
return Fail(c, -1, "failed to chmod: "+err.Error()) return Fail(c, -1, "failed to chmod: "+err.Error())
} }
go api.recordFilesystemLog(context.TODO(), sessionId, "chmod", filename+" ("+modeStr+")")
return Success(c, nil) return Success(c, nil)
} }
@@ -369,6 +410,8 @@ func (api FileSystemApi) DownloadEndpoint(c echo.Context) error {
return Fail(c, -1, "failed to stat file: "+err.Error()) return Fail(c, -1, "failed to stat file: "+err.Error())
} }
go api.recordFilesystemLog(context.TODO(), sessionId, "download", filename)
c.Response().Header().Set("Content-Disposition", "attachment; filename="+path.Base(filename)) c.Response().Header().Set("Content-Disposition", "attachment; filename="+path.Base(filename))
c.Response().Header().Set("Content-Type", "application/octet-stream") c.Response().Header().Set("Content-Type", "application/octet-stream")
c.Response().Header().Set("Content-Length", strconv.FormatInt(stat.Size(), 10)) c.Response().Header().Set("Content-Length", strconv.FormatInt(stat.Size(), 10))
@@ -425,6 +468,8 @@ func (api FileSystemApi) UploadEndpoint(c echo.Context) error {
} }
} }
go api.recordFilesystemLog(context.TODO(), sessionId, "upload", destPath)
return Success(c, maps.Map{ return Success(c, maps.Map{
"path": destPath, "path": destPath,
"size": file.Size, "size": file.Size,
+17 -30
View File
@@ -65,10 +65,10 @@ func (api PortalApi) AssetsTreeEndpoint(c echo.Context) error {
tree := make([]maps.Map, 0) tree := make([]maps.Map, 0)
for _, g := range groups { for _, g := range groups {
node := maps.Map{ node := maps.Map{
"key": g.ID, "key": "group_" + g.ID,
"title": g.Name, "title": g.Name,
"isLeaf": false, "isLeaf": false,
"children": buildPortalAssetChildren(items, g.ID, keyword), "children": []interface{}{},
} }
tree = append(tree, node) tree = append(tree, node)
} }
@@ -77,7 +77,7 @@ func (api PortalApi) AssetsTreeEndpoint(c echo.Context) error {
continue continue
} }
node := maps.Map{ node := maps.Map{
"key": a.ID, "key": "asset_" + a.ID,
"title": a.Name, "title": a.Name,
"isLeaf": true, "isLeaf": true,
"extra": maps.Map{ "extra": maps.Map{
@@ -93,29 +93,6 @@ func (api PortalApi) AssetsTreeEndpoint(c echo.Context) error {
return Success(c, tree) return Success(c, tree)
} }
func buildPortalAssetChildren(assets []model.Asset, groupId, keyword string) []maps.Map {
children := make([]maps.Map, 0)
for _, a := range assets {
if keyword != "" && !containsKeyword(a.Name, keyword) {
continue
}
node := maps.Map{
"key": a.ID,
"title": a.Name,
"isLeaf": true,
"extra": maps.Map{
"protocol": a.Protocol,
"logo": "",
"status": "unknown",
"network": a.IP,
"wolEnabled": false,
},
}
children = append(children, node)
}
return children
}
func containsKeyword(name, keyword string) bool { func containsKeyword(name, keyword string) bool {
if keyword == "" { if keyword == "" {
return true return true
@@ -139,7 +116,7 @@ func (api PortalApi) WebsitesTreeEndpoint(c echo.Context) error {
continue continue
} }
node := maps.Map{ node := maps.Map{
"key": w.ID, "key": "website_" + w.ID,
"title": w.Name, "title": w.Name,
"isLeaf": true, "isLeaf": true,
"extra": maps.Map{ "extra": maps.Map{
@@ -159,7 +136,7 @@ func (api PortalApi) AssetsGroupTreeEndpoint(c echo.Context) error {
tree := make([]maps.Map, 0) tree := make([]maps.Map, 0)
for _, g := range groups { for _, g := range groups {
node := maps.Map{ node := maps.Map{
"key": g.ID, "key": "group_" + g.ID,
"title": g.Name, "title": g.Name,
"isLeaf": false, "isLeaf": false,
"children": []interface{}{}, "children": []interface{}{},
@@ -186,7 +163,8 @@ func (api PortalApi) CreateSessionEndpoint(c echo.Context) error {
account, _ := GetCurrentAccount(c) account, _ := GetCurrentAccount(c)
s, err := service.SessionService.Create(c.RealIP(), assetId, nt.Native, account) clientIP := service.PropertyService.GetClientIP(c)
s, err := service.SessionService.Create(clientIP, assetId, nt.Native, account)
if err != nil { if err != nil {
return err return err
} }
@@ -201,7 +179,16 @@ func (api PortalApi) CreateSessionEndpoint(c echo.Context) error {
"id": s.ID, "id": s.ID,
"protocol": s.Protocol, "protocol": s.Protocol,
"assetName": assetName, "assetName": assetName,
"strategy": maps.Map{}, "strategy": maps.Map{
"upload": s.Upload == "1",
"download": s.Download == "1",
"delete": s.Delete == "1",
"rename": s.Rename == "1",
"edit": s.Edit == "1",
"copy": s.Copy == "1",
"paste": s.Paste == "1",
"fileSystem": s.FileSystem == "1",
},
"url": "", "url": "",
"watermark": maps.Map{}, "watermark": maps.Map{},
"readonly": false, "readonly": false,
+2 -1
View File
@@ -202,7 +202,8 @@ func (api SessionApi) SessionCreateEndpoint(c echo.Context) error {
user, _ := GetCurrentAccount(c) user, _ := GetCurrentAccount(c)
s, err := service.SessionService.Create(c.RealIP(), assetId, mode, user) clientIP := service.PropertyService.GetClientIP(c)
s, err := service.SessionService.Create(clientIP, assetId, mode, user)
if err != nil { if err != nil {
return err return err
} }
+150 -10
View File
@@ -2,7 +2,11 @@ package api
import ( import (
"context" "context"
"strconv"
"next-terminal/server/common/maps" "next-terminal/server/common/maps"
"next-terminal/server/model"
"next-terminal/server/repository"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
@@ -10,18 +14,37 @@ import (
type SessionCommandApi struct{} type SessionCommandApi struct{}
func (api SessionCommandApi) AllEndpoint(c echo.Context) error { func (api SessionCommandApi) AllEndpoint(c echo.Context) error {
return Success(c, []interface{}{}) sessionId := c.QueryParam("sessionId")
items, err := repository.SessionCommandRepository.FindBySessionId(context.TODO(), sessionId)
if err != nil {
return err
}
return Success(c, items)
} }
func (api SessionCommandApi) PagingEndpoint(c echo.Context) error { func (api SessionCommandApi) PagingEndpoint(c echo.Context) error {
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
sessionId := c.QueryParam("sessionId")
items, total, err := repository.SessionCommandRepository.FindByPage(context.TODO(), pageIndex, pageSize, sessionId)
if err != nil {
return err
}
return Success(c, maps.Map{ return Success(c, maps.Map{
"total": 0, "total": total,
"items": []interface{}{}, "items": items,
}) })
} }
func (api SessionCommandApi) GetEndpoint(c echo.Context) error { func (api SessionCommandApi) GetEndpoint(c echo.Context) error {
return Success(c, nil) id := c.Param("id")
var item model.SessionCommand
err := repository.SessionCommandRepository.GetDB(context.TODO()).Where("id = ?", id).First(&item).Error
if err != nil {
return err
}
return Success(c, item)
} }
type OperationLogApi struct{} type OperationLogApi struct{}
@@ -31,17 +54,55 @@ func (api OperationLogApi) AllEndpoint(c echo.Context) error {
} }
func (api OperationLogApi) PagingEndpoint(c echo.Context) error { func (api OperationLogApi) PagingEndpoint(c echo.Context) error {
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
accountName := c.QueryParam("accountName")
action := c.QueryParam("action")
status := c.QueryParam("status")
order := c.QueryParam("order")
field := c.QueryParam("field")
items, total, err := repository.OperationLogRepository.FindByPage(context.TODO(), pageIndex, pageSize, accountName, action, status, order, field)
if err != nil {
return err
}
result := make([]maps.Map, 0)
for _, item := range items {
result = append(result, maps.Map{
"id": item.ID,
"accountId": item.AccountId,
"accountName": item.AccountName,
"action": item.Action,
"content": item.Content,
"ip": item.IP,
"region": item.Region,
"userAgent": item.UserAgent,
"status": item.Status,
"errorMessage": item.ErrorMessage,
"remark": item.Remark,
"createdAt": item.Created,
})
}
return Success(c, maps.Map{ return Success(c, maps.Map{
"total": 0, "total": total,
"items": []interface{}{}, "items": result,
}) })
} }
func (api OperationLogApi) ClearEndpoint(c echo.Context) error { func (api OperationLogApi) ClearEndpoint(c echo.Context) error {
if err := repository.OperationLogRepository.DeleteAll(context.TODO()); err != nil {
return err
}
return Success(c, nil) return Success(c, nil)
} }
func (api OperationLogApi) DeleteEndpoint(c echo.Context) error { func (api OperationLogApi) DeleteEndpoint(c echo.Context) error {
id := c.Param("id")
if err := repository.OperationLogRepository.DeleteById(context.TODO(), id); err != nil {
return err
}
return Success(c, nil) return Success(c, nil)
} }
@@ -111,17 +172,57 @@ func (api FilesystemLogApi) AllEndpoint(c echo.Context) error {
} }
func (api FilesystemLogApi) PagingEndpoint(c echo.Context) error { func (api FilesystemLogApi) PagingEndpoint(c echo.Context) error {
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
action := c.QueryParam("action")
items, total, err := repository.FilesystemLogRepository.FindByPage(context.TODO(), pageIndex, pageSize, action)
if err != nil {
return err
}
result := make([]maps.Map, 0)
for _, item := range items {
var assetName, userName string
if item.AssetId != "" {
asset, _ := repository.AssetRepository.FindById(context.TODO(), item.AssetId)
assetName = asset.Name
}
if item.UserId != "" {
user, _ := repository.UserRepository.FindById(context.TODO(), item.UserId)
userName = user.Username
}
result = append(result, maps.Map{
"id": item.ID,
"assetId": item.AssetId,
"sessionId": item.SessionId,
"userId": item.UserId,
"action": item.Action,
"fileName": item.FileName,
"createdAt": item.Created,
"assetName": assetName,
"userName": userName,
})
}
return Success(c, maps.Map{ return Success(c, maps.Map{
"total": 0, "total": total,
"items": []interface{}{}, "items": result,
}) })
} }
func (api FilesystemLogApi) DeleteEndpoint(c echo.Context) error { func (api FilesystemLogApi) DeleteEndpoint(c echo.Context) error {
id := c.Param("id")
if err := repository.FilesystemLogRepository.DeleteById(context.TODO(), id); err != nil {
return err
}
return Success(c, nil) return Success(c, nil)
} }
func (api FilesystemLogApi) ClearEndpoint(c echo.Context) error { func (api FilesystemLogApi) ClearEndpoint(c echo.Context) error {
if err := repository.FilesystemLogRepository.DeleteAll(context.TODO()); err != nil {
return err
}
return Success(c, nil) return Success(c, nil)
} }
@@ -161,17 +262,56 @@ func (api AccessLogApi) AllEndpoint(c echo.Context) error {
} }
func (api AccessLogApi) PagingEndpoint(c echo.Context) error { func (api AccessLogApi) PagingEndpoint(c echo.Context) error {
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
domain := c.QueryParam("domain")
websiteId := c.QueryParam("websiteId")
accountId := c.QueryParam("accountId")
items, total, err := repository.AccessLogRepository.FindByPage(context.TODO(), pageIndex, pageSize, domain, websiteId, accountId)
if err != nil {
return err
}
result := make([]maps.Map, 0)
for _, item := range items {
result = append(result, maps.Map{
"id": item.ID,
"domain": item.Domain,
"websiteId": item.WebsiteId,
"accountId": item.AccountId,
"method": item.Method,
"uri": item.Uri,
"statusCode": item.StatusCode,
"responseSize": item.ResponseSize,
"clientIp": item.ClientIp,
"region": item.Region,
"userAgent": item.UserAgent,
"referer": item.Referer,
"requestTime": item.RequestTime,
"responseTime": item.ResponseTime,
"createdAt": item.Created,
})
}
return Success(c, maps.Map{ return Success(c, maps.Map{
"total": 0, "total": total,
"items": []interface{}{}, "items": result,
}) })
} }
func (api AccessLogApi) DeleteEndpoint(c echo.Context) error { func (api AccessLogApi) DeleteEndpoint(c echo.Context) error {
id := c.Param("id")
if err := repository.AccessLogRepository.DeleteById(context.TODO(), id); err != nil {
return err
}
return Success(c, nil) return Success(c, nil)
} }
func (api AccessLogApi) ClearEndpoint(c echo.Context) error { func (api AccessLogApi) ClearEndpoint(c echo.Context) error {
if err := repository.AccessLogRepository.DeleteAll(context.TODO()); err != nil {
return err
}
return Success(c, nil) return Success(c, nil)
} }
+48 -2
View File
@@ -537,13 +537,59 @@ func (api DbWorkOrderApi) DeleteEndpoint(c echo.Context) error {
type DatabaseSQLLogApi struct{} type DatabaseSQLLogApi struct{}
func (api DatabaseSQLLogApi) PagingEndpoint(c echo.Context) error { func (api DatabaseSQLLogApi) PagingEndpoint(c echo.Context) error {
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
assetId := c.QueryParam("assetId")
userId := c.QueryParam("userId")
status := c.QueryParam("status")
source := c.QueryParam("source")
order := c.QueryParam("order")
field := c.QueryParam("field")
items, total, err := repository.DatabaseSQLLogRepository.FindByPage(context.TODO(), pageIndex, pageSize, assetId, userId, status, source, order, field)
if err != nil {
return err
}
result := make([]maps.Map, 0)
for _, item := range items {
var assetName, userName string
if item.AssetId != "" {
asset, _ := repository.DatabaseAssetRepository.FindById(context.TODO(), item.AssetId)
assetName = asset.Name
}
if item.UserId != "" {
user, _ := repository.UserRepository.FindById(context.TODO(), item.UserId)
userName = user.Username
}
result = append(result, maps.Map{
"id": item.ID,
"assetId": item.AssetId,
"assetName": assetName,
"database": item.Database,
"userId": item.UserId,
"userName": userName,
"clientIp": item.ClientIP,
"sql": item.SQL,
"durationMs": item.DurationMs,
"rowsAffected": item.RowsAffected,
"status": item.Status,
"errorMessage": item.ErrorMessage,
"source": item.Source,
"createdAt": item.Created,
})
}
return Success(c, maps.Map{ return Success(c, maps.Map{
"total": 0, "total": total,
"items": []interface{}{}, "items": result,
}) })
} }
func (api DatabaseSQLLogApi) ClearEndpoint(c echo.Context) error { func (api DatabaseSQLLogApi) ClearEndpoint(c echo.Context) error {
if err := repository.DatabaseSQLLogRepository.DeleteAll(context.TODO()); err != nil {
return err
}
return Success(c, nil) return Success(c, nil)
} }
+2 -2
View File
@@ -188,7 +188,7 @@ func (api WebTerminalApi) SshEndpoint(c echo.Context) error {
if err != nil { if err != nil {
service.SessionService.CloseSessionById(sessionId, TunnelClosed, "远程连接已关闭") service.SessionService.CloseSessionById(sessionId, TunnelClosed, "远程连接已关闭")
} else { } else {
_ = termHandler.SendMessageToWebSocket(dto.NewMessage(Ping, "")) _ = termHandler.SendMessageToWebSocket(dto.NewMessage(Ping, msg.Content))
} }
} }
@@ -358,7 +358,7 @@ func (api WebTerminalApi) AccessTerminalEndpoint(c echo.Context) error {
termHandler.Write([]byte(msg.Content)) termHandler.Write([]byte(msg.Content))
case Ping: case Ping:
termHandler.SendRequest() termHandler.SendRequest()
termHandler.SendMessageToWebSocket(dto.NewMessage(Ping, "")) termHandler.SendMessageToWebSocket(dto.NewMessage(Ping, msg.Content))
} }
} }
return nil return nil
+117 -3
View File
@@ -3,18 +3,27 @@ package api
import ( import (
"bytes" "bytes"
"context" "context"
"strings"
"sync" "sync"
"time" "time"
"unicode/utf8" "unicode/utf8"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"next-terminal/server/common"
"next-terminal/server/common/term" "next-terminal/server/common/term"
"next-terminal/server/dto" "next-terminal/server/dto"
"next-terminal/server/global/session" "next-terminal/server/global/session"
"next-terminal/server/model"
"next-terminal/server/repository"
"next-terminal/server/utils"
) )
const maxOutputLength = 10000
type TermHandler struct { type TermHandler struct {
sessionId string sessionId string
userId string
assetId string
isRecording bool isRecording bool
webSocket *websocket.Conn webSocket *websocket.Conn
nextTerminal *term.NextTerminal nextTerminal *term.NextTerminal
@@ -24,6 +33,10 @@ type TermHandler struct {
tick *time.Ticker tick *time.Ticker
mutex sync.Mutex mutex sync.Mutex
buf bytes.Buffer buf bytes.Buffer
commandBuf bytes.Buffer
outputBuf bytes.Buffer
lastCommand *model.SessionCommand
outputMutex sync.Mutex
} }
func NewTermHandler(userId, assetId, sessionId string, isRecording bool, ws *websocket.Conn, nextTerminal *term.NextTerminal) *TermHandler { func NewTermHandler(userId, assetId, sessionId string, isRecording bool, ws *websocket.Conn, nextTerminal *term.NextTerminal) *TermHandler {
@@ -32,6 +45,8 @@ func NewTermHandler(userId, assetId, sessionId string, isRecording bool, ws *web
return &TermHandler{ return &TermHandler{
sessionId: sessionId, sessionId: sessionId,
userId: userId,
assetId: assetId,
isRecording: isRecording, isRecording: isRecording,
webSocket: ws, webSocket: ws,
nextTerminal: nextTerminal, nextTerminal: nextTerminal,
@@ -48,9 +63,9 @@ func (r *TermHandler) Start() {
} }
func (r *TermHandler) Stop() { func (r *TermHandler) Stop() {
// 会话结束时记录最后一个命令
r.tick.Stop() r.tick.Stop()
r.cancel() r.cancel()
r.saveLastCommandOutput()
} }
func (r *TermHandler) readFormTunnel() { func (r *TermHandler) readFormTunnel() {
@@ -86,8 +101,8 @@ func (r *TermHandler) writeToWebsocket() {
if r.isRecording && r.nextTerminal.Recorder != nil { if r.isRecording && r.nextTerminal.Recorder != nil {
_ = r.nextTerminal.Recorder.WriteData(s) _ = r.nextTerminal.Recorder.WriteData(s)
} }
// 监控
SendObData(r.sessionId, s) SendObData(r.sessionId, s)
r.collectOutput(s)
r.buf.Reset() r.buf.Reset()
case data := <-r.dataChan: case data := <-r.dataChan:
if data != utf8.RuneError { if data != utf8.RuneError {
@@ -101,12 +116,111 @@ func (r *TermHandler) writeToWebsocket() {
} }
} }
func (r *TermHandler) collectOutput(output string) {
r.outputMutex.Lock()
defer r.outputMutex.Unlock()
if r.outputBuf.Len()+len(output) <= maxOutputLength {
r.outputBuf.WriteString(output)
}
}
func (r *TermHandler) getAndClearOutput() string {
r.outputMutex.Lock()
defer r.outputMutex.Unlock()
output := r.outputBuf.String()
r.outputBuf.Reset()
return output
}
func (r *TermHandler) saveLastCommandOutput() {
r.outputMutex.Lock()
defer r.outputMutex.Unlock()
if r.lastCommand != nil && r.outputBuf.Len() > 0 {
output := cleanOutput(r.outputBuf.String())
if len(output) > maxOutputLength {
output = output[:maxOutputLength]
}
r.lastCommand.Output = output
_ = repository.SessionCommandRepository.Create(context.TODO(), r.lastCommand)
r.lastCommand = nil
r.outputBuf.Reset()
}
}
func cleanOutput(output string) string {
output = strings.ReplaceAll(output, "\x1b[0m", "")
output = strings.ReplaceAll(output, "\x1b[?2004h", "")
output = strings.ReplaceAll(output, "\x1b[?2004l", "")
var result strings.Builder
for _, line := range strings.Split(output, "\n") {
line = strings.TrimSpace(line)
if line != "" {
result.WriteString(line)
result.WriteString("\n")
}
}
return strings.TrimSpace(result.String())
}
func (r *TermHandler) Write(input []byte) error { func (r *TermHandler) Write(input []byte) error {
// 正常的字符输入 for _, b := range input {
switch b {
case 13: // Enter key
command := strings.TrimSpace(r.commandBuf.String())
if command != "" {
r.saveLastCommandOutput()
r.lastCommand = &model.SessionCommand{
ID: utils.UUID(),
SessionId: r.sessionId,
RiskLevel: detectRiskLevel(command),
Command: command,
Output: "",
Created: common.NowJsonTime(),
}
}
r.commandBuf.Reset()
case 127, 8: // Backspace, Delete
if r.commandBuf.Len() > 0 {
r.commandBuf.Truncate(r.commandBuf.Len() - 1)
}
case 27: // Escape sequence (arrow keys, etc.)
default:
if b >= 32 && b < 127 { // Printable ASCII
r.commandBuf.WriteByte(b)
}
}
}
_, err := r.nextTerminal.Write(input) _, err := r.nextTerminal.Write(input)
return err return err
} }
func detectRiskLevel(command string) int {
highRiskPatterns := []string{
"rm -rf /", "rm -rf /*", "mkfs", "dd if=", "> /dev/sd",
"chmod -R 777", "chown -R", ":(){ :|:& };:",
}
mediumRiskPatterns := []string{
"rm -rf", "rm -r", "passwd", "useradd", "userdel",
"chmod 777", "chmod -R", "chown", "wget", "curl",
"apt-get", "yum install", "dnf install", "pacman",
"systemctl stop", "systemctl disable", "service stop",
"iptables -F", "ufw disable", "setenforce 0",
}
lowerCmd := strings.ToLower(command)
for _, pattern := range highRiskPatterns {
if strings.Contains(lowerCmd, strings.ToLower(pattern)) {
return 2
}
}
for _, pattern := range mediumRiskPatterns {
if strings.Contains(lowerCmd, strings.ToLower(pattern)) {
return 1
}
}
return 0
}
func (r *TermHandler) WindowChange(h int, w int) error { func (r *TermHandler) WindowChange(h int, w int) error {
return r.nextTerminal.WindowChange(h, w) return r.nextTerminal.WindowChange(h, w)
} }
+8
View File
@@ -25,9 +25,11 @@ func (userApi UserApi) CreateEndpoint(c echo.Context) (err error) {
} }
if err := service.UserService.CreateUser(item); err != nil { if err := service.UserService.CreateUser(item); err != nil {
recordOperationLog(c, "user-add", "创建用户: "+item.Username, "failed", err.Error())
return err return err
} }
recordOperationLog(c, "user-add", "创建用户: "+item.Username, "success", "")
return Success(c, item) return Success(c, item)
} }
@@ -68,9 +70,11 @@ func (userApi UserApi) UpdateEndpoint(c echo.Context) error {
} }
if err := service.UserService.UpdateUser(id, item); err != nil { if err := service.UserService.UpdateUser(id, item); err != nil {
recordOperationLog(c, "user-edit", "更新用户: "+item.Username, "failed", err.Error())
return err return err
} }
recordOperationLog(c, "user-edit", "更新用户: "+item.Username, "success", "")
return Success(c, nil) return Success(c, nil)
} }
@@ -83,9 +87,11 @@ func (userApi UserApi) UpdateStatusEndpoint(c echo.Context) error {
} }
if err := service.UserService.UpdateStatusById(id, status); err != nil { if err := service.UserService.UpdateStatusById(id, status); err != nil {
recordOperationLog(c, "user-edit", "更新用户状态", "failed", err.Error())
return err return err
} }
recordOperationLog(c, "user-edit", "更新用户状态", "success", "")
return Success(c, nil) return Success(c, nil)
} }
@@ -102,10 +108,12 @@ func (userApi UserApi) DeleteEndpoint(c echo.Context) error {
return Fail(c, -1, "不允许删除自身账户") return Fail(c, -1, "不允许删除自身账户")
} }
if err := service.UserService.DeleteUserById(userId); err != nil { if err := service.UserService.DeleteUserById(userId); err != nil {
recordOperationLog(c, "user-del", "删除用户: "+ids, "failed", err.Error())
return err return err
} }
} }
recordOperationLog(c, "user-del", "删除用户: "+ids, "success", "")
return Success(c, nil) return Success(c, nil)
} }
+1 -1
View File
@@ -169,7 +169,7 @@ func buildWebsiteGroupTree(groups []model.WebsiteGroup, parentId string) []maps.
"id": g.ID, "id": g.ID,
"name": g.Name, "name": g.Name,
"title": g.Name, "title": g.Name,
"key": g.ID, "key": "wgroup_" + g.ID,
"value": g.ID, "value": g.ID,
} }
children := buildWebsiteGroupTree(groups, g.ID) children := buildWebsiteGroupTree(groups, g.ID)
+2 -1
View File
@@ -3,6 +3,7 @@ package middleware
import ( import (
"net" "net"
"next-terminal/server/common/nt" "next-terminal/server/common/nt"
"next-terminal/server/service"
"strings" "strings"
"next-terminal/server/api" "next-terminal/server/api"
@@ -20,7 +21,7 @@ func TcpWall(next echo.HandlerFunc) echo.HandlerFunc {
return next(c) return next(c)
} }
ip := c.RealIP() ip := service.PropertyService.GetClientIP(c)
var pass = true var pass = true
+1 -1
View File
@@ -55,7 +55,7 @@ func setupDB() *gorm.DB {
&model.Role{}, &model.RoleMenuRef{}, &model.UserRoleRef{}, &model.Role{}, &model.RoleMenuRef{}, &model.UserRoleRef{},
&model.LoginPolicy{}, &model.LoginPolicyUserRef{}, &model.TimePeriod{}, &model.LoginPolicy{}, &model.LoginPolicyUserRef{}, &model.TimePeriod{},
&model.StorageLog{}, &model.Authorised{}, &model.Logo{}, &model.AssetGroup{}, &model.StorageLog{}, &model.Authorised{}, &model.Logo{}, &model.AssetGroup{},
&model.AgentGateway{}, &model.SshGateway{}, &model.GatewayGroup{}, &model.Website{}, &model.Certificate{}, &model.Snippet{}, &model.SessionAudit{}, &model.Department{}, &model.UserDepartmentRef{}, &model.DatabaseAsset{}, &model.CommandFilter{}, &model.CommandFilterRule{}, &model.AuthorisedAsset{}, &model.AuthorisedDatabaseAsset{}, &model.AuthorisedWebsite{}, &model.WebsiteGroup{}); err != nil { &model.AgentGateway{}, &model.SshGateway{}, &model.GatewayGroup{}, &model.Website{}, &model.Certificate{}, &model.Snippet{}, &model.SessionAudit{}, &model.Department{}, &model.UserDepartmentRef{}, &model.DatabaseAsset{}, &model.CommandFilter{}, &model.CommandFilterRule{}, &model.AuthorisedAsset{}, &model.AuthorisedDatabaseAsset{}, &model.AuthorisedWebsite{}, &model.WebsiteGroup{}, &model.AccessLog{}, &model.FilesystemLog{}, &model.OperationLog{}, &model.DatabaseSQLLog{}, &model.SessionCommand{}); err != nil {
panic(fmt.Errorf("初始化数据库表结构异常: %v", err.Error())) panic(fmt.Errorf("初始化数据库表结构异常: %v", err.Error()))
} }
return db return db
+25
View File
@@ -0,0 +1,25 @@
package model
import "next-terminal/server/common"
type AccessLog struct {
ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
Domain string `gorm:"type:varchar(500);index" json:"domain"`
WebsiteId string `gorm:"type:varchar(36);index" json:"websiteId"`
AccountId string `gorm:"type:varchar(36);index" json:"accountId"`
Method string `gorm:"type:varchar(10)" json:"method"`
Uri string `gorm:"type:varchar(2000)" json:"uri"`
StatusCode int `json:"statusCode"`
ResponseSize int64 `json:"responseSize"`
ClientIp string `gorm:"type:varchar(50);index" json:"clientIp"`
Region string `gorm:"type:varchar(100)" json:"region"`
UserAgent string `gorm:"type:varchar(500)" json:"userAgent"`
Referer string `gorm:"type:varchar(2000)" json:"referer"`
RequestTime int `json:"requestTime"`
ResponseTime int `json:"responseTime"`
Created common.JsonTime `gorm:"type:datetime;index" json:"createdAt"`
}
func (r *AccessLog) TableName() string {
return "access_logs"
}
+24
View File
@@ -0,0 +1,24 @@
package model
import (
"next-terminal/server/common"
)
type DatabaseSQLLog struct {
ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
AssetId string `gorm:"index,type:varchar(36)" json:"assetId"`
Database string `gorm:"type:varchar(200)" json:"database"`
UserId string `gorm:"index,type:varchar(36)" json:"userId"`
ClientIP string `gorm:"type:varchar(50);index" json:"clientIp"`
SQL string `gorm:"type:text" json:"sql"`
DurationMs int `json:"durationMs"`
RowsAffected int `json:"rowsAffected"`
Status string `gorm:"type:varchar(20);index" json:"status"`
ErrorMessage string `gorm:"type:text" json:"errorMessage"`
Source string `gorm:"type:varchar(50);index" json:"source"`
Created common.JsonTime `gorm:"type:datetime;index" json:"createdAt"`
}
func (r *DatabaseSQLLog) TableName() string {
return "database_sql_logs"
}
+19
View File
@@ -0,0 +1,19 @@
package model
import (
"next-terminal/server/common"
)
type FilesystemLog struct {
ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
AssetId string `gorm:"index,type:varchar(36)" json:"assetId"`
SessionId string `gorm:"index,type:varchar(36)" json:"sessionId"`
UserId string `gorm:"index,type:varchar(36)" json:"userId"`
Action string `gorm:"type:varchar(50);index" json:"action"`
FileName string `gorm:"type:varchar(500)" json:"fileName"`
Created common.JsonTime `gorm:"type:datetime;index" json:"createdAt"`
}
func (r *FilesystemLog) TableName() string {
return "filesystem_logs"
}
+24
View File
@@ -0,0 +1,24 @@
package model
import (
"next-terminal/server/common"
)
type OperationLog struct {
ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
AccountId string `gorm:"index,type:varchar(36)" json:"accountId"`
AccountName string `gorm:"type:varchar(200)" json:"accountName"`
Action string `gorm:"type:varchar(100);index" json:"action"`
Content string `gorm:"type:text" json:"content"`
IP string `gorm:"type:varchar(50);index" json:"ip"`
Region string `gorm:"type:varchar(200)" json:"region"`
UserAgent string `gorm:"type:varchar(500)" json:"userAgent"`
Status string `gorm:"type:varchar(20);index" json:"status"`
ErrorMessage string `gorm:"type:text" json:"errorMessage"`
Remark string `gorm:"type:varchar(500)" json:"remark"`
Created common.JsonTime `gorm:"type:datetime;index" json:"createdAt"`
}
func (r *OperationLog) TableName() string {
return "operation_logs"
}
+18
View File
@@ -0,0 +1,18 @@
package model
import (
"next-terminal/server/common"
)
type SessionCommand struct {
ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
SessionId string `gorm:"index,type:varchar(36)" json:"sessionId"`
RiskLevel int `json:"riskLevel"`
Command string `gorm:"type:text" json:"command"`
Output string `gorm:"type:text" json:"output"`
Created common.JsonTime `gorm:"type:datetime;index" json:"createdAt"`
}
func (r *SessionCommand) TableName() string {
return "session_commands"
}
+59
View File
@@ -0,0 +1,59 @@
package repository
import (
"context"
"next-terminal/server/model"
)
var AccessLogRepository = new(accessLogRepository)
type accessLogRepository struct {
baseRepository
}
func (r accessLogRepository) Create(c context.Context, o *model.AccessLog) error {
return r.GetDB(c).Create(o).Error
}
func (r accessLogRepository) FindById(c context.Context, id string) (o model.AccessLog, err error) {
err = r.GetDB(c).Where("id = ?", id).First(&o).Error
return
}
func (r accessLogRepository) FindByPage(c context.Context, pageIndex, pageSize int, domain, websiteId, accountId string) (o []model.AccessLog, total int64, err error) {
db := r.GetDB(c).Model(&model.AccessLog{})
if domain != "" {
db = db.Where("domain LIKE ?", "%"+domain+"%")
}
if websiteId != "" {
db = db.Where("website_id = ?", websiteId)
}
if accountId != "" {
db = db.Where("account_id = ?", accountId)
}
err = db.Count(&total).Error
if err != nil {
return nil, 0, err
}
err = db.Order("created desc").Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&o).Error
return
}
func (r accessLogRepository) DeleteById(c context.Context, id string) error {
return r.GetDB(c).Where("id = ?", id).Delete(&model.AccessLog{}).Error
}
func (r accessLogRepository) DeleteAll(c context.Context) error {
return r.GetDB(c).Where("1 = 1").Delete(&model.AccessLog{}).Error
}
func (r accessLogRepository) FindOutTimeLog(c context.Context, limit int) (o []model.AccessLog, err error) {
err = r.GetDB(c).Where("strftime('%s', created) < strftime('%s', 'now', '-' || ? || ' days')", limit).Find(&o).Error
return
}
func (r accessLogRepository) Count(c context.Context) (total int64, err error) {
err = r.GetDB(c).Model(&model.AccessLog{}).Count(&total).Error
return
}
+79
View File
@@ -0,0 +1,79 @@
package repository
import (
"context"
"time"
"next-terminal/server/model"
)
var DatabaseSQLLogRepository = new(databaseSQLLogRepository)
type databaseSQLLogRepository struct {
baseRepository
}
func (r databaseSQLLogRepository) FindByPage(c context.Context, pageIndex, pageSize int, assetId, userId, status, source string, order, field string) (o []model.DatabaseSQLLog, total int64, err error) {
m := model.DatabaseSQLLog{}
db := r.GetDB(c).Table(m.TableName())
dbCounter := r.GetDB(c).Table(m.TableName())
if assetId != "" {
db = db.Where("asset_id = ?", assetId)
dbCounter = dbCounter.Where("asset_id = ?", assetId)
}
if userId != "" {
db = db.Where("user_id = ?", userId)
dbCounter = dbCounter.Where("user_id = ?", userId)
}
if status != "" {
db = db.Where("status = ?", status)
dbCounter = dbCounter.Where("status = ?", status)
}
if source != "" {
db = db.Where("source = ?", source)
dbCounter = dbCounter.Where("source = ?", source)
}
err = dbCounter.Count(&total).Error
if err != nil {
return nil, 0, err
}
orderBy := "created desc"
if order != "" && field != "" {
orderBy = field + " " + order
}
err = db.Order(orderBy).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&o).Error
if o == nil {
o = make([]model.DatabaseSQLLog, 0)
}
return
}
func (r databaseSQLLogRepository) Create(c context.Context, o *model.DatabaseSQLLog) error {
return r.GetDB(c).Create(o).Error
}
func (r databaseSQLLogRepository) DeleteById(c context.Context, id string) error {
return r.GetDB(c).Where("id = ?", id).Delete(&model.DatabaseSQLLog{}).Error
}
func (r databaseSQLLogRepository) DeleteAll(c context.Context) error {
return r.GetDB(c).Where("1 = 1").Delete(&model.DatabaseSQLLog{}).Error
}
func (r databaseSQLLogRepository) FindOutTimeLog(c context.Context, dayLimit int) (o []model.DatabaseSQLLog, err error) {
limitTime := time.Now().Add(time.Duration(-dayLimit*24) * time.Hour)
err = r.GetDB(c).Where("created < ?", limitTime).Find(&o).Error
return
}
func (r databaseSQLLogRepository) Count(c context.Context) (total int64, err error) {
err = r.GetDB(c).Model(&model.DatabaseSQLLog{}).Count(&total).Error
return
}
+59
View File
@@ -0,0 +1,59 @@
package repository
import (
"context"
"time"
"next-terminal/server/model"
)
var FilesystemLogRepository = new(filesystemLogRepository)
type filesystemLogRepository struct {
baseRepository
}
func (r filesystemLogRepository) FindByPage(c context.Context, pageIndex, pageSize int, action string) (o []model.FilesystemLog, total int64, err error) {
m := model.FilesystemLog{}
db := r.GetDB(c).Table(m.TableName())
dbCounter := r.GetDB(c).Table(m.TableName())
if action != "" {
db = db.Where("action = ?", action)
dbCounter = dbCounter.Where("action = ?", action)
}
err = dbCounter.Count(&total).Error
if err != nil {
return nil, 0, err
}
err = db.Order("created desc").Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&o).Error
if o == nil {
o = make([]model.FilesystemLog, 0)
}
return
}
func (r filesystemLogRepository) Create(c context.Context, o *model.FilesystemLog) error {
return r.GetDB(c).Create(o).Error
}
func (r filesystemLogRepository) DeleteById(c context.Context, id string) error {
return r.GetDB(c).Where("id = ?", id).Delete(&model.FilesystemLog{}).Error
}
func (r filesystemLogRepository) DeleteAll(c context.Context) error {
return r.GetDB(c).Where("1 = 1").Delete(&model.FilesystemLog{}).Error
}
func (r filesystemLogRepository) FindOutTimeLog(c context.Context, dayLimit int) (o []model.FilesystemLog, err error) {
limitTime := time.Now().Add(time.Duration(-dayLimit*24) * time.Hour)
err = r.GetDB(c).Where("created < ?", limitTime).Find(&o).Error
return
}
func (r filesystemLogRepository) Count(c context.Context) (total int64, err error) {
err = r.GetDB(c).Model(&model.FilesystemLog{}).Count(&total).Error
return
}
+74
View File
@@ -0,0 +1,74 @@
package repository
import (
"context"
"time"
"next-terminal/server/model"
)
var OperationLogRepository = new(operationLogRepository)
type operationLogRepository struct {
baseRepository
}
func (r operationLogRepository) FindByPage(c context.Context, pageIndex, pageSize int, accountName, action, status string, order, field string) (o []model.OperationLog, total int64, err error) {
m := model.OperationLog{}
db := r.GetDB(c).Table(m.TableName())
dbCounter := r.GetDB(c).Table(m.TableName())
if accountName != "" {
db = db.Where("account_name like ?", "%"+accountName+"%")
dbCounter = dbCounter.Where("account_name like ?", "%"+accountName+"%")
}
if action != "" {
db = db.Where("action = ?", action)
dbCounter = dbCounter.Where("action = ?", action)
}
if status != "" {
db = db.Where("status = ?", status)
dbCounter = dbCounter.Where("status = ?", status)
}
err = dbCounter.Count(&total).Error
if err != nil {
return nil, 0, err
}
orderBy := "created desc"
if order != "" && field != "" {
orderBy = field + " " + order
}
err = db.Order(orderBy).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&o).Error
if o == nil {
o = make([]model.OperationLog, 0)
}
return
}
func (r operationLogRepository) Create(c context.Context, o *model.OperationLog) error {
return r.GetDB(c).Create(o).Error
}
func (r operationLogRepository) DeleteById(c context.Context, id string) error {
return r.GetDB(c).Where("id = ?", id).Delete(&model.OperationLog{}).Error
}
func (r operationLogRepository) DeleteAll(c context.Context) error {
return r.GetDB(c).Where("1 = 1").Delete(&model.OperationLog{}).Error
}
func (r operationLogRepository) FindOutTimeLog(c context.Context, dayLimit int) (o []model.OperationLog, err error) {
limitTime := time.Now().Add(time.Duration(-dayLimit*24) * time.Hour)
err = r.GetDB(c).Where("created < ?", limitTime).Find(&o).Error
return
}
func (r operationLogRepository) Count(c context.Context) (total int64, err error) {
err = r.GetDB(c).Model(&model.OperationLog{}).Count(&total).Error
return
}
+48
View File
@@ -0,0 +1,48 @@
package repository
import (
"context"
"next-terminal/server/model"
)
var SessionCommandRepository = new(sessionCommandRepository)
type sessionCommandRepository struct {
baseRepository
}
func (r sessionCommandRepository) FindBySessionId(c context.Context, sessionId string) (o []model.SessionCommand, err error) {
err = r.GetDB(c).Where("session_id = ?", sessionId).Order("created asc").Find(&o).Error
return
}
func (r sessionCommandRepository) FindByPage(c context.Context, pageIndex, pageSize int, sessionId string) (o []model.SessionCommand, total int64, err error) {
m := model.SessionCommand{}
db := r.GetDB(c).Table(m.TableName())
dbCounter := r.GetDB(c).Table(m.TableName())
if sessionId != "" {
db = db.Where("session_id = ?", sessionId)
dbCounter = dbCounter.Where("session_id = ?", sessionId)
}
err = dbCounter.Count(&total).Error
if err != nil {
return nil, 0, err
}
err = db.Order("created desc").Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&o).Error
if o == nil {
o = make([]model.SessionCommand, 0)
}
return
}
func (r sessionCommandRepository) Create(c context.Context, o *model.SessionCommand) error {
return r.GetDB(c).Create(o).Error
}
func (r sessionCommandRepository) DeleteBySessionId(c context.Context, sessionId string) error {
return r.GetDB(c).Where("session_id = ?", sessionId).Delete(&model.SessionCommand{}).Error
}
+47
View File
@@ -0,0 +1,47 @@
package service
import (
"context"
"next-terminal/server/common"
"next-terminal/server/model"
"next-terminal/server/repository"
"next-terminal/server/utils"
)
var DatabaseSQLLogService = new(databaseSQLLogService)
type databaseSQLLogService struct {
baseService
}
type DatabaseSQLLogParams struct {
AssetId string
Database string
UserId string
ClientIP string
SQL string
DurationMs int
RowsAffected int
Status string
ErrorMessage string
Source string
}
func (s databaseSQLLogService) Record(ctx context.Context, params DatabaseSQLLogParams) error {
log := &model.DatabaseSQLLog{
ID: utils.UUID(),
AssetId: params.AssetId,
Database: params.Database,
UserId: params.UserId,
ClientIP: params.ClientIP,
SQL: params.SQL,
DurationMs: params.DurationMs,
RowsAffected: params.RowsAffected,
Status: params.Status,
ErrorMessage: params.ErrorMessage,
Source: params.Source,
Created: common.NowJsonTime(),
}
return repository.DatabaseSQLLogRepository.Create(ctx, log)
}
+47
View File
@@ -0,0 +1,47 @@
package service
import (
"context"
"next-terminal/server/common"
"next-terminal/server/model"
"next-terminal/server/repository"
"next-terminal/server/utils"
)
var OperationLogService = new(operationLogService)
type operationLogService struct {
baseService
}
type OperationLogParams struct {
AccountId string
AccountName string
Action string
Content string
IP string
Region string
UserAgent string
Status string
ErrorMessage string
Remark string
}
func (s operationLogService) Record(ctx context.Context, params OperationLogParams) error {
log := &model.OperationLog{
ID: utils.UUID(),
AccountId: params.AccountId,
AccountName: params.AccountName,
Action: params.Action,
Content: params.Content,
IP: params.IP,
Region: params.Region,
UserAgent: params.UserAgent,
Status: params.Status,
ErrorMessage: params.ErrorMessage,
Remark: params.Remark,
Created: common.NowJsonTime(),
}
return repository.OperationLogRepository.Create(ctx, log)
}
+88
View File
@@ -4,12 +4,15 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"net"
"strings"
"next-terminal/server/common/guacamole" "next-terminal/server/common/guacamole"
"next-terminal/server/env" "next-terminal/server/env"
"next-terminal/server/model" "next-terminal/server/model"
"next-terminal/server/repository" "next-terminal/server/repository"
"github.com/labstack/echo/v4"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -40,10 +43,19 @@ var defaultProperties = map[string]string{
guacamole.EnableMenuAnimations: "true", guacamole.EnableMenuAnimations: "true",
guacamole.DisableBitmapCaching: "false", guacamole.DisableBitmapCaching: "false",
guacamole.DisableOffscreenCaching: "false", guacamole.DisableOffscreenCaching: "false",
guacamole.ColorDepth: "",
guacamole.Cursor: "",
guacamole.SwapRedBlue: "false",
"cron-log-saved-limit": "360", "cron-log-saved-limit": "360",
"login-log-saved-limit": "360", "login-log-saved-limit": "360",
"session-saved-limit": "360", "session-saved-limit": "360",
"access-log-saved-limit": "30",
"filesystem-log-saved-limit": "30",
"operation-log-saved-limit": "30",
"database-sql-log-saved-limit": "30",
"user-default-storage-size": "5120", "user-default-storage-size": "5120",
"ip-extractor": "direct",
"ip-trust-list": "",
} }
func (service propertyService) InitProperties() error { func (service propertyService) InitProperties() error {
@@ -60,6 +72,9 @@ func (service propertyService) InitProperties() error {
func (service propertyService) CreateIfAbsent(propertyMap map[string]string, name, value string) error { func (service propertyService) CreateIfAbsent(propertyMap map[string]string, name, value string) error {
if len(propertyMap[name]) == 0 { if len(propertyMap[name]) == 0 {
if value == "" {
value = "-"
}
property := model.Property{ property := model.Property{
Name: name, Name: name,
Value: value, Value: value,
@@ -117,3 +132,76 @@ func (service propertyService) Update(item map[string]interface{}) error {
}) })
} }
func (service propertyService) GetClientIP(c echo.Context) string {
propertyMap := repository.PropertyRepository.FindAllMap(context.TODO())
extractor := propertyMap["ip-extractor"]
trustList := propertyMap["ip-trust-list"]
directIP := c.RealIP()
if extractor == "" || extractor == "direct" {
return directIP
}
if !service.isTrustedIP(directIP, trustList) {
return directIP
}
switch extractor {
case "x-real-ip":
xRealIP := c.Request().Header.Get("X-Real-IP")
if xRealIP != "" {
return xRealIP
}
case "x-forwarded-for":
xForwardedFor := c.Request().Header.Get("X-Forwarded-For")
if xForwardedFor != "" {
ips := strings.Split(xForwardedFor, ",")
if len(ips) > 0 {
ip := strings.TrimSpace(ips[0])
if ip != "" {
return ip
}
}
}
}
return directIP
}
func (service propertyService) isTrustedIP(clientIP string, trustList string) bool {
if trustList == "" {
return false
}
trustIPs := strings.Split(trustList, ",")
clientIPAddr := net.ParseIP(clientIP)
if clientIPAddr == nil {
return false
}
for _, trustIP := range trustIPs {
trustIP = strings.TrimSpace(trustIP)
if trustIP == "" {
continue
}
if strings.Contains(trustIP, "/") {
_, ipNet, err := net.ParseCIDR(trustIP)
if err == nil && ipNet.Contains(clientIPAddr) {
return true
}
} else {
if trustIP == clientIP {
return true
}
trustIPAddr := net.ParseIP(trustIP)
if trustIPAddr != nil && trustIPAddr.Equal(clientIPAddr) {
return true
}
}
}
return false
}
+116
View File
@@ -43,6 +43,10 @@ func (t *Ticker) SetupTicker() {
deleteOutTimeSession() deleteOutTimeSession()
deleteOutTimeLoginLog() deleteOutTimeLoginLog()
deleteOutTimeJobLog() deleteOutTimeJobLog()
deleteOutTimeAccessLog()
deleteOutTimeFilesystemLog()
deleteOutTimeOperationLog()
deleteOutTimeDatabaseSQLLog()
} }
}() }()
@@ -297,3 +301,115 @@ func deleteOutTimeJobLog() {
} }
} }
} }
func deleteOutTimeAccessLog() {
property, err := repository.PropertyRepository.FindByName(context.TODO(), "access-log-saved-limit")
if err != nil {
return
}
if property.Value == "" || property.Value == "-" {
return
}
limit, err := strconv.Atoi(property.Value)
if err != nil {
return
}
accessLogs, err := repository.AccessLogRepository.FindOutTimeLog(context.TODO(), limit)
if err != nil {
return
}
if len(accessLogs) > 0 {
for i := range accessLogs {
err := repository.AccessLogRepository.DeleteById(context.TODO(), accessLogs[i].ID)
if err != nil {
log.Error("删除访问日志失败", log.NamedError("err", err))
}
}
}
}
func deleteOutTimeFilesystemLog() {
property, err := repository.PropertyRepository.FindByName(context.TODO(), "filesystem-log-saved-limit")
if err != nil {
return
}
if property.Value == "" || property.Value == "-" {
return
}
limit, err := strconv.Atoi(property.Value)
if err != nil {
return
}
logs, err := repository.FilesystemLogRepository.FindOutTimeLog(context.TODO(), limit)
if err != nil {
return
}
if len(logs) > 0 {
for i := range logs {
err := repository.FilesystemLogRepository.DeleteById(context.TODO(), logs[i].ID)
if err != nil {
log.Error("删除文件系统日志失败", log.NamedError("err", err))
}
}
}
}
func deleteOutTimeOperationLog() {
property, err := repository.PropertyRepository.FindByName(context.TODO(), "operation-log-saved-limit")
if err != nil {
return
}
if property.Value == "" || property.Value == "-" {
return
}
limit, err := strconv.Atoi(property.Value)
if err != nil {
return
}
logs, err := repository.OperationLogRepository.FindOutTimeLog(context.TODO(), limit)
if err != nil {
return
}
if len(logs) > 0 {
for i := range logs {
err := repository.OperationLogRepository.DeleteById(context.TODO(), logs[i].ID)
if err != nil {
log.Error("删除操作日志失败", log.NamedError("err", err))
}
}
}
}
func deleteOutTimeDatabaseSQLLog() {
property, err := repository.PropertyRepository.FindByName(context.TODO(), "database-sql-log-saved-limit")
if err != nil {
return
}
if property.Value == "" || property.Value == "-" {
return
}
limit, err := strconv.Atoi(property.Value)
if err != nil {
return
}
logs, err := repository.DatabaseSQLLogRepository.FindOutTimeLog(context.TODO(), limit)
if err != nil {
return
}
if len(logs) > 0 {
for i := range logs {
err := repository.DatabaseSQLLogRepository.DeleteById(context.TODO(), logs[i].ID)
if err != nil {
log.Error("删除数据库SQL日志失败", log.NamedError("err", err))
}
}
}
}
+7 -2
View File
@@ -160,10 +160,15 @@ const AccessPage = () => {
// 处理树节点双击 // 处理树节点双击
const handleNodeDoubleClick = useCallback((node: any) => { const handleNodeDoubleClick = useCallback((node: any) => {
let assetId = node.key as string;
if (typeof assetId === 'string' && assetId.startsWith('asset_')) {
assetId = assetId.substring(6);
}
// 检查是否需要 WOL 唤醒 // 检查是否需要 WOL 唤醒
if (node.extra?.status === 'inactive' && node.extra?.wolEnabled) { if (node.extra?.status === 'inactive' && node.extra?.wolEnabled) {
setWolAssetInfo({ setWolAssetInfo({
id: node.key as string, id: assetId,
name: node.title as string, name: node.title as string,
protocol: node.extra?.protocol, protocol: node.extra?.protocol,
}); });
@@ -173,7 +178,7 @@ const AccessPage = () => {
// 直接打开连接 // 直接打开连接
openAssetTab({ openAssetTab({
id: node.key, id: assetId,
name: node.title, name: node.title,
protocol: node.extra?.protocol, protocol: node.extra?.protocol,
}); });
+7 -1
View File
@@ -61,7 +61,13 @@ const AccessSshChooser = ({handleOk, handleCancel, open}: Props) => {
const onCheck: TreeProps['onCheck'] = (checkedKeysValue, {checkedNodes}) => { const onCheck: TreeProps['onCheck'] = (checkedKeysValue, {checkedNodes}) => {
// console.log('onCheck', checkedKeysValue, checkedNodes); // console.log('onCheck', checkedKeysValue, checkedNodes);
let keys = checkedNodes.filter(item => item.isLeaf).map((item) => item.key); let keys = checkedNodes.filter(item => item.isLeaf).map((item) => {
let key = item.key as string;
if (key.startsWith('asset_')) {
return key.substring(6);
}
return key;
});
setSshAssetKeys(keys as string[]); setSshAssetKeys(keys as string[]);
}; };
+3 -6
View File
@@ -762,7 +762,6 @@ const FileSystemPage = forwardRef<FileSystem, Props>(({
console.log(`Upload response - Status: ${xhr.status}, Response: ${xhr.responseText}`); console.log(`Upload response - Status: ${xhr.status}, Response: ${xhr.responseText}`);
if (xhr.status >= 200 && xhr.status < 300) { if (xhr.status >= 200 && xhr.status < 300) {
// 检查响应体是否包含错误信息
let hasError = false; let hasError = false;
let errorMessage = ''; let errorMessage = '';
@@ -772,15 +771,13 @@ const FileSystemPage = forwardRef<FileSystem, Props>(({
const result = JSON.parse(responseText); const result = JSON.parse(responseText);
console.log('Upload response parsed:', result); console.log('Upload response parsed:', result);
// 检查是否是标准错误响应格式 if (result.error === true || (result.code && result.code !== 1 && result.code !== 0)) {
if (result.error === true || (result.message && result.code)) {
hasError = true; hasError = true;
errorMessage = result.message || 'Upload failed'; errorMessage = result.message || 'Upload failed';
console.error('Upload failed with parsed error:', errorMessage); console.error('Upload failed with parsed error:', errorMessage);
} }
} }
} catch (e) { } catch (e) {
// JSON解析失败,可能是成功的空响应,继续处理为成功
console.log('Upload response parse failed, treating as success:', e); console.log('Upload response parse failed, treating as success:', e);
} }
@@ -1126,7 +1123,7 @@ const FileSystemPage = forwardRef<FileSystem, Props>(({
return ( return (
<div> <div>
<Drawer title="FileSystem" <Drawer title={t('fs.title')}
placement="right" placement="right"
onClose={onClose} onClose={onClose}
open={open} open={open}
@@ -1314,7 +1311,7 @@ const FileSystemPage = forwardRef<FileSystem, Props>(({
)} )}
<Table <Table
virtual virtual
// scroll={{y: window.innerHeight - 240}} scroll={{y: 400}}
rowKey={'path'} rowKey={'path'}
columns={fileColumns} columns={fileColumns}
rowSelection={rowSelection} rowSelection={rowSelection}
+12 -2
View File
@@ -69,6 +69,16 @@ const AssetPage = () => {
let [groupId, setGroupId] = useState(searchParams.get('groupId') || ''); let [groupId, setGroupId] = useState(searchParams.get('groupId') || '');
let [dataSource, setDataSource] = useState<Asset[]>([]); let [dataSource, setDataSource] = useState<Asset[]>([]);
const handleTreeSelect = (key: string) => {
if (key.startsWith('group_')) {
setGroupId(key.substring(6));
} else if (key.startsWith('asset_')) {
setGroupId('');
} else {
setGroupId(key);
}
};
let [selectedTags, setSelectedTags] = useState<string[]>([]); let [selectedTags, setSelectedTags] = useState<string[]>([]);
let [groupChooserOpen, setGroupChooserOpen] = useState(false); let [groupChooserOpen, setGroupChooserOpen] = useState(false);
let [gatewayChooserOpen, setGatewayChooserOpen] = useState(false); let [gatewayChooserOpen, setGatewayChooserOpen] = useState(false);
@@ -608,7 +618,7 @@ const AssetPage = () => {
/* 移动端:垂直布局,树在上,标签过?+ 表格在下 */ /* 移动端:垂直布局,树在上,标签过?+ 表格在下 */
<> <>
<div className="mb-4 bg-white dark:bg-gray-800 rounded-lg"> <div className="mb-4 bg-white dark:bg-gray-800 rounded-lg">
<AssetTree selected={groupId} onSelect={setGroupId}/> <AssetTree selected={groupId} onSelect={handleTreeSelect}/>
</div> </div>
{tagFilter} {tagFilter}
<ProTable {...tableProps}/> <ProTable {...tableProps}/>
@@ -621,7 +631,7 @@ const AssetPage = () => {
)}> )}>
<div className="relative rounded-md bg-gray-50 dark:bg-[#141414]"> <div className="relative rounded-md bg-gray-50 dark:bg-[#141414]">
{!isTreeCollapsed && ( {!isTreeCollapsed && (
<AssetTree selected={groupId} onSelect={setGroupId}/> <AssetTree selected={groupId} onSelect={handleTreeSelect}/>
)} )}
<div <div
className={cn( className={cn(
+15 -3
View File
@@ -30,7 +30,7 @@ const AssetTree = ({selected, onSelect}: Props) => {
let [op, setOP] = useState<OP>(); let [op, setOP] = useState<OP>();
let [expandedKeys, setExpandedKeys] = useState([]); let [expandedKeys, setExpandedKeys] = useState([]);
let [selectedKeys, setSelectedKeys] = useState<React.Key[]>([selected]); let [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
const [theme] = useNTTheme(); const [theme] = useNTTheme();
let query = useQuery({ let query = useQuery({
@@ -38,6 +38,14 @@ const AssetTree = ({selected, onSelect}: Props) => {
queryFn: assetApi.getGroups, queryFn: assetApi.getGroups,
}); });
useEffect(() => {
if (selected) {
setSelectedKeys(['group_' + selected]);
} else {
setSelectedKeys([]);
}
}, [selected]);
useEffect(() => { useEffect(() => {
if (Array.isArray(query.data) && query.data.length > 0) { if (Array.isArray(query.data) && query.data.length > 0) {
setTreeData(query.data); setTreeData(query.data);
@@ -197,7 +205,11 @@ const AssetTree = ({selected, onSelect}: Props) => {
danger: true, danger: true,
icon: <TrashIcon className={'h-4 w-4'}/>, icon: <TrashIcon className={'h-4 w-4'}/>,
onClick: () => { onClick: () => {
assetApi.deleteGroup(contextMenu.node.key as string).then(() => { let groupKey = contextMenu.node.key as string;
if (groupKey.startsWith('group_')) {
groupKey = groupKey.substring(6);
}
assetApi.deleteGroup(groupKey).then(() => {
query.refetch(); query.refetch();
}) })
}, },
@@ -205,7 +217,7 @@ const AssetTree = ({selected, onSelect}: Props) => {
]; ];
const handleRightClick = ({event, node}) => { const handleRightClick = ({event, node}) => {
if (node.key === 'default') { if (node.key === 'default' || node.key?.toString().startsWith('asset_')) {
return; return;
} }
// console.log(`handleRightClick`, event, node) // console.log(`handleRightClick`, event, node)
-1
View File
@@ -58,7 +58,6 @@ const DbProxySetting = ({get, set}: SettingProps) => {
<ProFormSwitch <ProFormSwitch
name="db-proxy-enabled" name="db-proxy-enabled"
label={t('db.proxy.enabled')} label={t('db.proxy.enabled')}
rules={[{required: true}]}
checkedChildren={t('general.enabled')} checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')} unCheckedChildren={t('general.disabled')}
fieldProps={{ fieldProps={{
+5 -6
View File
@@ -18,14 +18,13 @@ const LogSetting = ({get, set}: SettingProps) => {
style: {display: 'none'} style: {display: 'none'}
} }
}}> }}>
<ProFormSwitch name="recording-enabled" <ProFormSwitch name="enable-recording"
label={t('identity.user.recording')} label={t('identity.user.recording')}
rules={[{required: true}]}
checkedChildren={t('general.enabled')} checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')} unCheckedChildren={t('general.disabled')}
/> />
<ProFormSelect name="session-saved-limit-days" <ProFormSelect name="session-saved-limit"
label={t('settings.log.session.saved_limit_days')} label={t('settings.log.session.saved_limit_days')}
fieldProps={{ fieldProps={{
options: [ options: [
@@ -40,7 +39,7 @@ const LogSetting = ({get, set}: SettingProps) => {
}} }}
addonAfter={t('general.days')} addonAfter={t('general.days')}
/> />
<ProFormSelect name="login-log-saved-limit-days" <ProFormSelect name="login-log-saved-limit"
label={t('settings.log.login_log.saved_limit_days')} label={t('settings.log.login_log.saved_limit_days')}
fieldProps={{ fieldProps={{
options: [ options: [
@@ -53,7 +52,7 @@ const LogSetting = ({get, set}: SettingProps) => {
}} }}
addonAfter={t('general.days')} addonAfter={t('general.days')}
/> />
<ProFormSelect name="cron-log-saved-limit-days" <ProFormSelect name="cron-log-saved-limit"
label={t('settings.log.cron_log.saved_limit_days')} label={t('settings.log.cron_log.saved_limit_days')}
fieldProps={{ fieldProps={{
options: [ options: [
@@ -66,7 +65,7 @@ const LogSetting = ({get, set}: SettingProps) => {
}} }}
addonAfter={t('general.days')} addonAfter={t('general.days')}
/> />
<ProFormSelect name="access-log-saved-limit-days" <ProFormSelect name="access-log-saved-limit"
label={t('settings.log.access_log.saved_limit_days')} label={t('settings.log.access_log.saved_limit_days')}
fieldProps={{ fieldProps={{
options: [ options: [
+7 -17
View File
@@ -1,10 +1,8 @@
import React, {useEffect, useState} from 'react'; import React, {useState} from 'react';
import {Alert, App, Card, Col, Form, Row, Typography} from "antd"; import {Alert, App, Card, Col, Row, Typography} from "antd";
import {SettingProps} from "./SettingPage"; import {SettingProps} from "./SettingPage";
import {useQuery} from "@tanstack/react-query";
import {ProForm, ProFormDigit, ProFormSwitch, ProFormText, ProFormTextArea} from "@ant-design/pro-components"; import {ProForm, ProFormDigit, ProFormSwitch, ProFormText, ProFormTextArea} from "@ant-design/pro-components";
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
import requests from "@/api/core/requests";
import propertyApi from "@/api/property-api"; import propertyApi from "@/api/property-api";
import {useMobile} from "@/hook/use-mobile"; import {useMobile} from "@/hook/use-mobile";
import {cn} from "@/lib/utils"; import {cn} from "@/lib/utils";
@@ -15,22 +13,15 @@ const MailSetting = ({get, set}: SettingProps) => {
const { isMobile } = useMobile(); const { isMobile } = useMobile();
let {t} = useTranslation(); let {t} = useTranslation();
const [form] = Form.useForm();
let [enabled, setEnabled] = useState(false); let [enabled, setEnabled] = useState(false);
let {message} = App.useApp(); let {message} = App.useApp();
let query = useQuery({ const wrapGet = async () => {
queryKey: ['get-property'], let values = await get();
queryFn: get, setEnabled(values['mail-enabled']);
}); return values;
useEffect(() => {
if (query.data) {
form.setFieldsValue(query.data);
setEnabled(query.data['mail-enabled']);
} }
}, [query.data]);
const handleSendTestMail = async (values: any) => { const handleSendTestMail = async (values: any) => {
await propertyApi.sendMail(values); await propertyApi.sendMail(values);
@@ -43,14 +34,13 @@ const MailSetting = ({get, set}: SettingProps) => {
<Row gutter={16}> <Row gutter={16}>
<Col span={isMobile ? 24 : 12}> <Col span={isMobile ? 24 : 12}>
<Card> <Card>
<ProForm onFinish={set} request={get} submitter={{ <ProForm onFinish={set} request={wrapGet} submitter={{
resetButtonProps: { resetButtonProps: {
style: {display: 'none'} style: {display: 'none'}
} }
}}> }}>
<ProFormSwitch name="mail-enabled" <ProFormSwitch name="mail-enabled"
label={t('settings.mail.enabled')} label={t('settings.mail.enabled')}
rules={[{required: true}]}
checkedChildren={t('general.enabled')} checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')} unCheckedChildren={t('general.disabled')}
fieldProps={{ fieldProps={{
-8
View File
@@ -19,49 +19,41 @@ const RdpSetting = ({get, set}: SettingProps) => {
}}> }}>
<ProFormSwitch name="enable-wallpaper" <ProFormSwitch name="enable-wallpaper"
label={t('settings.rdp.enable.wallpaper')} label={t('settings.rdp.enable.wallpaper')}
rules={[{required: true}]}
checkedChildren={t('general.enabled')} checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')} unCheckedChildren={t('general.disabled')}
/> />
<ProFormSwitch name="enable-theming" <ProFormSwitch name="enable-theming"
label={t("settings.rdp.enable.theming")} label={t("settings.rdp.enable.theming")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')} checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')} unCheckedChildren={t('general.disabled')}
/> />
<ProFormSwitch name="enable-font-smoothing" <ProFormSwitch name="enable-font-smoothing"
label={t("settings.rdp.enable.font_smoothing")} label={t("settings.rdp.enable.font_smoothing")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')} checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')} unCheckedChildren={t('general.disabled')}
/> />
<ProFormSwitch name="enable-full-window-drag" <ProFormSwitch name="enable-full-window-drag"
label={t("settings.rdp.enable.full_window_drag")} label={t("settings.rdp.enable.full_window_drag")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')} checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')} unCheckedChildren={t('general.disabled')}
/> />
<ProFormSwitch name="enable-desktop-composition" <ProFormSwitch name="enable-desktop-composition"
label={t("settings.rdp.enable.desktop_composition")} label={t("settings.rdp.enable.desktop_composition")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')} checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')} unCheckedChildren={t('general.disabled')}
/> />
<ProFormSwitch name="enable-menu-animations" <ProFormSwitch name="enable-menu-animations"
label={t("settings.rdp.enable.menu_animations")} label={t("settings.rdp.enable.menu_animations")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')} checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')} unCheckedChildren={t('general.disabled')}
/> />
<ProFormSwitch name="disable-bitmap-caching" <ProFormSwitch name="disable-bitmap-caching"
label={t("settings.rdp.disable.bitmap_caching")} label={t("settings.rdp.disable.bitmap_caching")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')} checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')} unCheckedChildren={t('general.disabled')}
/> />
<ProFormSwitch name="disable-offscreen-caching" <ProFormSwitch name="disable-offscreen-caching"
label={t("settings.rdp.disable.offscreen_caching")} label={t("settings.rdp.disable.offscreen_caching")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')} checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')} unCheckedChildren={t('general.disabled')}
/> />
@@ -57,14 +57,12 @@ const SecuritySetting = ({get, set}: SettingProps) => {
<ProFormSwitch <ProFormSwitch
name="login-captcha-enabled" name="login-captcha-enabled"
label={t("settings.security.captcha")} label={t("settings.security.captcha")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')} checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')} unCheckedChildren={t('general.disabled')}
/> />
<ProFormSwitch <ProFormSwitch
name="login-force-totp-enabled" name="login-force-totp-enabled"
label={t("settings.security.force_otp")} label={t("settings.security.force_otp")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')} checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')} unCheckedChildren={t('general.disabled')}
/> />
@@ -72,7 +70,6 @@ const SecuritySetting = ({get, set}: SettingProps) => {
<ProFormSwitch <ProFormSwitch
name="disable-password-login" name="disable-password-login"
label={t("settings.security.disable_password_login")} label={t("settings.security.disable_password_login")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')} checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')} unCheckedChildren={t('general.disabled')}
/> />
@@ -82,7 +79,6 @@ const SecuritySetting = ({get, set}: SettingProps) => {
<ProFormSwitch <ProFormSwitch
name="access-require-mfa" name="access-require-mfa"
label={t("settings.security.access_require_mfa")} label={t("settings.security.access_require_mfa")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')} checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')} unCheckedChildren={t('general.disabled')}
/> />
@@ -102,7 +98,6 @@ const SecuritySetting = ({get, set}: SettingProps) => {
<ProFormSwitch <ProFormSwitch
name="login-session-count-custom" name="login-session-count-custom"
label={t("settings.security.session.count_custom")} label={t("settings.security.session.count_custom")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')} checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')} unCheckedChildren={t('general.disabled')}
fieldProps={{ fieldProps={{
@@ -229,7 +224,6 @@ const SecuritySetting = ({get, set}: SettingProps) => {
<ProFormSwitch <ProFormSwitch
name="login-lock-enabled" name="login-lock-enabled"
label={t("settings.security.login_lock.enabled")} label={t("settings.security.login_lock.enabled")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')} checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')} unCheckedChildren={t('general.disabled')}
fieldProps={{ fieldProps={{
-1
View File
@@ -94,7 +94,6 @@ const SshdSetting = ({get, set}: SettingProps) => {
}}> }}>
<ProFormSwitch name="ssh-server-enabled" <ProFormSwitch name="ssh-server-enabled"
label={t("settings.sshd.enabled")} label={t("settings.sshd.enabled")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')} checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')} unCheckedChildren={t('general.disabled')}
fieldProps={{ fieldProps={{
+3 -17
View File
@@ -1,8 +1,7 @@
import React, {useEffect} from 'react'; import React from 'react';
import {Form, Typography} from "antd"; import {Typography} from "antd";
import {SettingProps} from "./SettingPage"; import {SettingProps} from "./SettingPage";
import {useQuery} from "@tanstack/react-query"; import {ProForm, ProFormSelect, ProFormSwitch} from "@ant-design/pro-components";
import {ProForm, ProFormDigit, ProFormSelect, ProFormSwitch} from "@ant-design/pro-components";
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
const {Title} = Typography; const {Title} = Typography;
@@ -10,18 +9,6 @@ const {Title} = Typography;
const VncSetting = ({get, set}: SettingProps) => { const VncSetting = ({get, set}: SettingProps) => {
let {t} = useTranslation(); let {t} = useTranslation();
const [form] = Form.useForm();
let query = useQuery({
queryKey: ['get-property'],
queryFn: get,
});
useEffect(() => {
if (query.data) {
form.setFieldsValue(query.data);
}
}, [query.data]);
return ( return (
<div> <div>
@@ -55,7 +42,6 @@ const VncSetting = ({get, set}: SettingProps) => {
/> />
<ProFormSwitch name="swap-red-blue" <ProFormSwitch name="swap-red-blue"
label={t("settings.vnc.swap_red_blue")} label={t("settings.vnc.swap_red_blue")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')} checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')} unCheckedChildren={t('general.disabled')}
/> />
+1
View File
@@ -198,6 +198,7 @@
} }
}, },
"fs": { "fs": {
"title": "File Manager",
"operations": { "operations": {
"batch_download": "Batch Download", "batch_download": "Batch Download",
"create_dir": "Create Folder", "create_dir": "Create Folder",
+1
View File
@@ -440,6 +440,7 @@
} }
}, },
"fs": { "fs": {
"title": "文件管理",
"operations": { "operations": {
"batch_download": "批量下载", "batch_download": "批量下载",
"create_dir": "创建文件夹", "create_dir": "创建文件夹",