From ded4dc492a54fb6a82030ed4d28dc262b06b9ca9 Mon Sep 17 00:00:00 2001 From: dushixiang Date: Sun, 20 Nov 2022 17:36:27 +0800 Subject: [PATCH] =?UTF-8?q?-=20=E4=BF=AE=E5=A4=8Dmysql=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E4=B8=8B=E3=80=8C=E8=B5=84=E4=BA=A7=E6=8E=88=E6=9D=83=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E3=80=8D=E3=80=8C=E7=94=A8=E6=88=B7=E6=8E=88=E6=9D=83?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E3=80=8D=E3=80=8C=E7=94=A8=E6=88=B7=E7=BB=84?= =?UTF-8?q?=E6=8E=88=E6=9D=83=E5=88=97=E8=A1=A8=E3=80=8D=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E7=9A=84=E9=97=AE=E9=A2=98=20fixed=20#315=20?= =?UTF-8?q?-=20=E4=BF=AE=E5=A4=8D=E8=B5=84=E4=BA=A7=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E3=80=81=E4=BF=AE=E6=94=B9=E6=97=A0=E6=9D=83=E9=99=90=E7=9A=84?= =?UTF-8?q?=E7=BC=BA=E9=99=B7=20fixed=20#314=20-=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=89=A7=E8=A1=8C=E5=8A=A8=E6=80=81=E6=8C=87=E4=BB=A4=E6=97=B6?= =?UTF-8?q?=E5=A4=9A=E8=A1=8C=E5=A4=B1=E8=B4=A5=E4=B8=94=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E6=89=A7=E8=A1=8C=E7=9A=84=E9=97=AE=E9=A2=98?= =?UTF-8?q?=20fixed=20#313=20#310=20-=20=E4=BF=AE=E5=A4=8D=E8=AE=A1?= =?UTF-8?q?=E5=88=92=E4=BB=BB=E5=8A=A1=E6=97=A0=E6=B3=95=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E8=B5=84=E4=BA=A7=E7=9A=84=E9=97=AE=E9=A2=98=20fixed=20#312=20?= =?UTF-8?q?-=20=E4=BF=AE=E5=A4=8D=E5=AF=BC=E5=85=A5=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E5=A4=87=E4=BB=BD=E6=97=A0=E6=95=88=E7=9A=84=E9=97=AE=E9=A2=98?= =?UTF-8?q?=20fixed=20#303=20-=20=E5=A2=9E=E5=8A=A0=E3=80=8C=E8=B5=84?= =?UTF-8?q?=E4=BA=A7=E8=AF=A6=E6=83=85=E3=80=8D=E3=80=8C=E8=B5=84=E4=BA=A7?= =?UTF-8?q?=E6=8E=88=E6=9D=83=E3=80=8D=E3=80=8C=E7=94=A8=E6=88=B7=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E3=80=8D=E3=80=8C=E7=94=A8=E6=88=B7=E6=8E=88=E6=9D=83?= =?UTF-8?q?=E3=80=8D=E3=80=8C=E7=94=A8=E6=88=B7=E7=BB=84=E8=AF=A6=E6=83=85?= =?UTF-8?q?=E3=80=8D=E3=80=8C=E7=94=A8=E6=88=B7=E7=BB=84=E6=8E=88=E6=9D=83?= =?UTF-8?q?=E3=80=8D=E3=80=8C=E8=A7=92=E8=89=B2=E8=AF=A6=E6=83=85=E3=80=8D?= =?UTF-8?q?=E3=80=8C=E6=8E=88=E6=9D=83=E7=AD=96=E7=95=A5=E8=AF=A6=E6=83=85?= =?UTF-8?q?=E3=80=8D=E6=8C=89=E9=92=AE=20-=20=E4=BF=AE=E5=A4=8D=E8=B5=84?= =?UTF-8?q?=E4=BA=A7=E5=88=97=E8=A1=A8=E4=BD=BF=E7=94=A8IP=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E6=97=A0=E6=95=88=E7=9A=84=E9=97=AE=E9=A2=98=20-=20?= =?UTF-8?q?=E8=B5=84=E4=BA=A7=E5=88=97=E8=A1=A8=E5=A2=9E=E5=8A=A0=E6=9C=80?= =?UTF-8?q?=E8=BF=91=E6=8E=A5=E5=85=A5=E6=97=B6=E9=97=B4=E6=8E=92=E5=BA=8F?= =?UTF-8?q?=E3=80=81=E5=A2=9E=E5=8A=A0=E4=BF=AE=E6=94=B9=E6=AF=8F=E9=A1=B5?= =?UTF-8?q?=E6=95=B0=E9=87=8F=20fixed=20#311=20-=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E9=A1=B5=E9=9D=A2=E5=8F=8C=E5=9B=A0=E7=B4=A0?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=E8=BE=93=E5=85=A5=E6=A1=86=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=8E=B7=E5=8F=96=E7=84=A6=E7=82=B9=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98=20fixed=20#311=20-=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E6=99=AE=E9=80=9A=E9=A1=B5=E9=9D=A2=E8=B5=84=E4=BA=A7=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E6=9C=80=E5=90=8E=E6=8E=A5=E5=85=A5=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E6=8E=92=E5=BA=8F=20fixed=20#311=20-=20=E8=AE=A1=E5=88=92?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E5=A2=9E=E5=8A=A0=E6=89=A7=E8=A1=8C=E6=9C=AC?= =?UTF-8?q?=E6=9C=BA=E7=B3=BB=E7=BB=9F=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/common/nt/const.go | 1 + server/model/asset.go | 26 +++++---- server/repository/asset.go | 16 ++++-- server/repository/authorised.go | 9 +-- server/service/job.go | 7 ++- server/service/job_exec_shell.go | 55 +++++++++++++++---- server/service/menu_default_data.go | 6 ++ server/service/session.go | 3 + server/utils/command.go | 18 ++++++ server/utils/command_test.go | 22 ++++++++ web/src/components/Login.js | 35 +++++++----- web/src/components/asset/Asset.js | 39 +++++++++++-- web/src/components/authorised/Strategy.js | 6 +- web/src/components/devops/BatchCommand.js | 2 +- web/src/components/devops/ExecuteCommand.js | 35 ++++++++---- web/src/components/devops/Job.js | 10 +++- web/src/components/devops/JobModal.js | 52 +++++++++++------- web/src/components/session/OfflineSession.js | 21 +++++++ web/src/components/user/Role.js | 4 ++ web/src/components/user/UserGroup.js | 22 +++++++- web/src/components/user/UserGroupDetail.js | 20 +++++-- web/src/components/user/user/User.js | 26 ++++++++- .../components/user/user/UserDetailPage.js | 29 +++++++--- web/src/components/worker/MyAsset.js | 13 ++++- web/src/dd/prompt-modal/prompt-modal.js | 45 +++++++++++++++ web/src/layout/ManagerLayout.js | 11 +++- 26 files changed, 421 insertions(+), 112 deletions(-) create mode 100644 server/utils/command.go create mode 100644 server/utils/command_test.go create mode 100644 web/src/dd/prompt-modal/prompt-modal.js diff --git a/server/common/nt/const.go b/server/common/nt/const.go index 8a7b89c..b45f025 100644 --- a/server/common/nt/const.go +++ b/server/common/nt/const.go @@ -27,6 +27,7 @@ const ( JobStatusNotRunning = "not-running" // 计划任务未运行状态 FuncCheckAssetStatusJob = "check-asset-status-job" // 检测资产是否在线 FuncShellJob = "shell-job" // 执行Shell脚本 + JobModeSelf = "self" // 本机 JobModeAll = "all" // 全部资产 JobModeCustom = "custom" // 自定义选择资产 diff --git a/server/model/asset.go b/server/model/asset.go index 8373e23..77b2dfc 100644 --- a/server/model/asset.go +++ b/server/model/asset.go @@ -22,6 +22,7 @@ type Asset struct { Active bool `json:"active"` ActiveMessage string `gorm:"type:varchar(200)" json:"activeMessage"` Created common.JsonTime `json:"created"` + LastAccessTime common.JsonTime `json:"lastAccessTime"` Tags string `json:"tags"` Owner string `gorm:"index,type:varchar(36)" json:"owner"` Encrypted bool `json:"encrypted"` @@ -29,18 +30,19 @@ type Asset struct { } type AssetForPage struct { - ID string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - IP string `json:"ip"` - Protocol string `json:"protocol"` - Port int `json:"port"` - Active bool `json:"active"` - ActiveMessage string `json:"activeMessage"` - Created common.JsonTime `json:"created"` - Tags string `json:"tags"` - Owner string `json:"owner"` - OwnerName string `json:"ownerName"` + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + IP string `json:"ip"` + Protocol string `json:"protocol"` + Port int `json:"port"` + Active bool `json:"active"` + ActiveMessage string `json:"activeMessage"` + Created common.JsonTime `json:"created"` + LastAccessTime common.JsonTime `json:"lastAccessTime"` + Tags string `json:"tags"` + Owner string `json:"owner"` + OwnerName string `json:"ownerName"` } func (r *Asset) TableName() string { diff --git a/server/repository/asset.go b/server/repository/asset.go index 87d6397..4d06094 100644 --- a/server/repository/asset.go +++ b/server/repository/asset.go @@ -6,6 +6,7 @@ import ( "strconv" "strings" + "next-terminal/server/common" "next-terminal/server/common/maps" "next-terminal/server/common/nt" "next-terminal/server/config" @@ -44,7 +45,7 @@ func (r assetRepository) FindByProtocolAndIds(c context.Context, protocol string } func (r assetRepository) Find(c context.Context, pageIndex, pageSize int, name, protocol, tags, ip, port, active, order, field string) (o []model.AssetForPage, total int64, err error) { - db := r.GetDB(c).Table("assets").Select("assets.id,assets.name,assets.ip,assets.port,assets.protocol,assets.active,assets.active_message,assets.owner,assets.created,assets.tags,assets.description, users.nickname as owner_name").Joins("left join users on assets.owner = users.id") + db := r.GetDB(c).Table("assets").Select("assets.id,assets.name,assets.ip,assets.port,assets.protocol,assets.active,assets.active_message,assets.owner,assets.created,assets.last_access_time,assets.tags,assets.description, users.nickname as owner_name").Joins("left join users on assets.owner = users.id") dbCounter := r.GetDB(c).Table("assets") if len(name) > 0 { @@ -104,7 +105,8 @@ func (r assetRepository) Find(c context.Context, pageIndex, pageSize int, name, case "protocol": case "ip": case "active": - + case "lastAccessTime": + field = "last_access_time" default: field = "created" } @@ -285,7 +287,7 @@ func (r assetRepository) ExistById(c context.Context, id string) (bool, error) { } func (r assetRepository) FindMyAssets(c context.Context, pageIndex, pageSize int, name, protocol, tags string, assetIds []string, order, field string) (o []model.AssetForPage, total int64, err error) { - db := r.GetDB(c).Table("assets").Select("assets.id,assets.name,assets.protocol,assets.active,assets.active_message,assets.tags,assets.description"). + db := r.GetDB(c).Table("assets").Select("assets.id,assets.name,assets.protocol,assets.active,assets.active_message,assets.tags,assets.description,assets.last_access_time,"). Where("id in ?", assetIds) dbCounter := r.GetDB(c).Table("assets").Where("id in ?", assetIds) @@ -328,7 +330,8 @@ func (r assetRepository) FindMyAssets(c context.Context, pageIndex, pageSize int case "protocol": case "ip": case "active": - + case "lastAccessTime": + field = "last_access_time" default: field = "created" } @@ -362,3 +365,8 @@ func (r assetRepository) FindMyAssetTags(c context.Context, assetIds []string) ( return utils.Distinct(o), nil } + +func (r assetRepository) UpdateLastAccessTime(ctx context.Context, assetId string, now common.JsonTime) error { + asset := &model.Asset{ID: assetId, LastAccessTime: now} + return r.GetDB(ctx).Table("assets").Updates(asset).Error +} diff --git a/server/repository/authorised.go b/server/repository/authorised.go index 96a17b0..ddec485 100644 --- a/server/repository/authorised.go +++ b/server/repository/authorised.go @@ -71,8 +71,7 @@ func (r authorisedRepository) FindAssetPage(c context.Context, pageIndex, pageSi db := r.GetDB(c).Table("assets"). Select("authorised.id, authorised.created, assets.id as asset_id, assets.name as asset_name, strategies.id as strategy_id, strategies.name as strategy_name "). Joins("left join authorised on authorised.asset_id = assets.id"). - Joins("left join strategies on strategies.id = authorised.strategy_id"). - Group("assets.id") + Joins("left join strategies on strategies.id = authorised.strategy_id") dbCounter := r.GetDB(c).Table("assets").Joins("left join authorised on assets.id = authorised.asset_id").Group("assets.id") if assetName != "" { @@ -110,8 +109,7 @@ func (r authorisedRepository) FindUserPage(c context.Context, pageIndex, pageSiz db := r.GetDB(c).Table("users"). Select("authorised.id, authorised.created, users.id as user_id, users.nickname as user_name, strategies.id as strategy_id, strategies.name as strategy_name "). Joins("left join authorised on authorised.user_id = users.id"). - Joins("left join strategies on strategies.id = authorised.strategy_id"). - Group("users.id") + Joins("left join strategies on strategies.id = authorised.strategy_id") dbCounter := r.GetDB(c).Table("assets").Joins("left join authorised on assets.id = authorised.asset_id").Group("assets.id") if userName != "" { @@ -140,8 +138,7 @@ func (r authorisedRepository) FindUserGroupPage(c context.Context, pageIndex, pa db := r.GetDB(c).Table("user_groups"). Select("authorised.id, authorised.created, user_groups.id as user_group_id, user_groups.name as user_group_name, strategies.id as strategy_id, strategies.name as strategy_name "). Joins("left join authorised on authorised.user_group_id = user_groups.id"). - Joins("left join strategies on strategies.id = authorised.strategy_id"). - Group("user_groups.id") + Joins("left join strategies on strategies.id = authorised.strategy_id") dbCounter := r.GetDB(c).Table("assets").Joins("left join authorised on assets.id = authorised.asset_id").Group("assets.id") if userName != "" { diff --git a/server/service/job.go b/server/service/job.go index 246d765..b9ea80e 100644 --- a/server/service/job.go +++ b/server/service/job.go @@ -55,7 +55,12 @@ func getJob(j *model.Job) (job cron.Job, err error) { Metadata: j.Metadata, } case nt.FuncShellJob: - job = ShellJob{ID: j.ID, Mode: j.Mode, ResourceIds: j.ResourceIds, Metadata: j.Metadata} + job = ShellJob{ + ID: j.ID, + Mode: j.Mode, + ResourceIds: j.ResourceIds, + Metadata: j.Metadata, + } default: return nil, errors.New("未识别的任务") } diff --git a/server/service/job_exec_shell.go b/server/service/job_exec_shell.go index f9883b4..c00a3dc 100644 --- a/server/service/job_exec_shell.go +++ b/server/service/job_exec_shell.go @@ -5,12 +5,12 @@ import ( "encoding/json" "errors" "fmt" - "next-terminal/server/common" - "next-terminal/server/common/nt" - "next-terminal/server/common/term" "strings" "time" + "next-terminal/server/common" + "next-terminal/server/common/nt" + "next-terminal/server/common/term" "next-terminal/server/log" "next-terminal/server/model" "next-terminal/server/repository" @@ -35,13 +35,19 @@ func (r ShellJob) Run() { return } - var assets []model.Asset - if r.Mode == nt.JobModeAll { - assets, _ = repository.AssetRepository.FindByProtocol(context.TODO(), "ssh") - } else { - assets, _ = repository.AssetRepository.FindByProtocolAndIds(context.TODO(), "ssh", strings.Split(r.ResourceIds, ",")) + switch r.Mode { + case nt.JobModeAll: + assets, _ := repository.AssetRepository.FindByProtocol(context.TODO(), "ssh") + r.executeShellByAssets(assets) + case nt.JobModeCustom: + assets, _ := repository.AssetRepository.FindByProtocolAndIds(context.TODO(), "ssh", strings.Split(r.ResourceIds, ",")) + r.executeShellByAssets(assets) + case nt.JobModeSelf: + r.executeShellByLocal() } +} +func (r ShellJob) executeShellByAssets(assets []model.Asset) { if len(assets) == 0 { return } @@ -89,7 +95,7 @@ func (r ShellJob) Run() { go func() { t1 := time.Now() - result, err := exec(metadataShell.Shell, asset.AccessGatewayId, ip, port, username, password, privateKey, passphrase) + result, err := execute(metadataShell.Shell, asset.AccessGatewayId, ip, port, username, password, privateKey, passphrase) elapsed := time.Since(t1) var msg string if err != nil { @@ -124,7 +130,36 @@ func (r ShellJob) Run() { _ = repository.JobLogRepository.Create(context.TODO(), &jobLog) } -func exec(shell, accessGatewayId, ip string, port int, username, password, privateKey, passphrase string) (string, error) { +func (r ShellJob) executeShellByLocal() { + var metadataShell MetadataShell + err := json.Unmarshal([]byte(r.Metadata), &metadataShell) + if err != nil { + log.Error("JSON数据解析失败", log.String("err", err.Error())) + return + } + + now := time.Now() + var msg = "" + log.Debug("run local command", log.String("cmd", metadataShell.Shell)) + output, outerr, err := utils.Exec(metadataShell.Shell) + if err != nil { + msg = fmt.Sprintf("命令执行失败,错误内容为:「%v」,耗时「%v」", err.Error(), time.Since(now).String()) + } else { + msg = fmt.Sprintf("命令执行成功,stdout 返回值「%v」,stderr 返回值「%v」,耗时「%v」", output, outerr, time.Since(now).String()) + } + + _ = repository.JobRepository.UpdateLastUpdatedById(context.Background(), r.ID) + jobLog := model.JobLog{ + ID: utils.UUID(), + JobId: r.ID, + Timestamp: common.NowJsonTime(), + Message: msg, + } + + _ = repository.JobLogRepository.Create(context.Background(), &jobLog) +} + +func execute(shell, accessGatewayId, ip string, port int, username, password, privateKey, passphrase string) (string, error) { if accessGatewayId != "" && accessGatewayId != "-" { g, err := GatewayService.GetGatewayById(accessGatewayId) if err != nil { diff --git a/server/service/menu_default_data.go b/server/service/menu_default_data.go index f6b3ad9..7ed7953 100644 --- a/server/service/menu_default_data.go +++ b/server/service/menu_default_data.go @@ -33,10 +33,12 @@ var DefaultMenu = []*model.Menu{ ), model.NewMenu("asset-add", "新建", "asset", model.NewPermission("POST", "/assets"), + model.NewPermission("GET", "/access-gateways"), ), model.NewMenu("asset-edit", "编辑", "asset", model.NewPermission("GET", "/assets/:id"), model.NewPermission("PUT", "/assets/:id"), + model.NewPermission("GET", "/access-gateways"), ), model.NewMenu("asset-del", "删除", "asset", model.NewPermission("DELETE", "/assets/:id"), @@ -191,6 +193,10 @@ var DefaultMenu = []*model.Menu{ model.NewPermission("POST", "/storage-logs/clear"), ), + model.NewMenu("session-command", "命令日志", "log-audit", + model.NewPermission("GET", "/session-commands/paging"), + ), + model.NewMenu("ops", "系统运维", "root"), model.NewMenu("job", "计划任务", "ops", diff --git a/server/service/session.go b/server/service/session.go index 40f16fa..a969fe5 100644 --- a/server/service/session.go +++ b/server/service/session.go @@ -366,6 +366,9 @@ func (service sessionService) Create(clientIp, assetId, mode string, user *model if err := repository.SessionRepository.Create(context.TODO(), s); err != nil { return nil, err } + if err := repository.AssetRepository.UpdateLastAccessTime(context.Background(), s.AssetId, common.NowJsonTime()); err != nil { + return nil, err + } return s, nil } diff --git a/server/utils/command.go b/server/utils/command.go new file mode 100644 index 0000000..fd7ecb0 --- /dev/null +++ b/server/utils/command.go @@ -0,0 +1,18 @@ +package utils + +import ( + "bytes" + "os/exec" +) + +// Exec 执行shell命令 +func Exec(command string) (string, string, error) { + var stdout bytes.Buffer + var stderr bytes.Buffer + + cmd := exec.Command("bash", "-c", command) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + return stdout.String(), stderr.String(), err +} diff --git a/server/utils/command_test.go b/server/utils/command_test.go new file mode 100644 index 0000000..2989ead --- /dev/null +++ b/server/utils/command_test.go @@ -0,0 +1,22 @@ +package utils + +import "testing" + +func TestExec(t *testing.T) { + commands := []string{ + `pwd`, + `whoami`, + `cat /etc/resolv.conf`, + `echo "test" > /tmp/ddtest`, + `rm -rf /tmp/ddtest`, + } + for _, command := range commands { + output, errout, err := Exec(command) + if err != nil { + t.Fatal(err) + } + t.Log("output:", output) + t.Log("errout:", errout) + } + +} diff --git a/web/src/components/Login.js b/web/src/components/Login.js index fc5caff..e1d96f7 100644 --- a/web/src/components/Login.js +++ b/web/src/components/Login.js @@ -2,14 +2,15 @@ import React, {useEffect, useState} from 'react'; import {Button, Card, Checkbox, Form, Input, message, Modal, Typography} from "antd"; import './Login.css' import request from "../common/request"; -import {LockOutlined, LockTwoTone, UserOutlined} from '@ant-design/icons'; +import {LockOutlined, UserOutlined} from '@ant-design/icons'; import {setToken} from "../utils/utils"; import brandingApi from "../api/branding"; import strings from "../utils/strings"; import {useNavigate} from "react-router-dom"; import {setCurrentUser} from "../service/permission"; +import PromptModal from "../dd/prompt-modal/prompt-modal"; -const {Title} = Typography; +const {Title, Text} = Typography; const LoginForm = () => { @@ -17,6 +18,8 @@ const LoginForm = () => { let [inLogin, setInLogin] = useState(false); let [branding, setBranding] = useState({}); + let [prompt, setPrompt] = useState(false); + let [account, setAccount] = useState({}); useEffect(() => { const x = async () => { @@ -62,17 +65,6 @@ const LoginForm = () => { return false; } - const showTOTP = (loginAccount) => { - let value = ''; - Modal.confirm({ - title: '双因素认证', - icon: , - content: value = e.target.value} onPressEnter={() => handleOk(loginAccount, value)} - placeholder="请输入双因素认证码"/>, - onOk: () => handleOk(loginAccount, value), - }); - } - const handleSubmit = async params => { setInLogin(true); @@ -80,7 +72,8 @@ const LoginForm = () => { let result = await request.post('/login', params); if (result.code === 100) { // 进行双因素认证 - showTOTP(params); + setPrompt(true); + setAccount(params); return; } if (result.code !== 1) { @@ -100,7 +93,7 @@ const LoginForm = () => {
{branding['name']} - {/*一个轻量级的堡垒机系统*/} + {branding['description']}
@@ -120,6 +113,18 @@ const LoginForm = () => {
+ + { + handleOk(account, value) + }} + onCancel={() => setPrompt(false)} + placeholder={"请输入双因素认证码"} + > + + ); diff --git a/web/src/components/asset/Asset.js b/web/src/components/asset/Asset.js index aca77c6..1a20dac 100644 --- a/web/src/components/asset/Asset.js +++ b/web/src/components/asset/Asset.js @@ -14,7 +14,7 @@ import { Tooltip, Upload } from "antd"; -import {Link} from "react-router-dom"; +import {Link, useNavigate} from "react-router-dom"; import {ProTable, TableDropdown} from "@ant-design/pro-components"; import assetApi from "../../api/asset"; import tagApi from "../../api/tag"; @@ -61,6 +61,7 @@ const Asset = () => { const [columnsStateMap, setColumnsStateMap] = useColumnState(ColumnState.ASSET); const tagQuery = useQuery('getAllTag', tagApi.getAll); + let navigate = useNavigate(); const columns = [ { @@ -120,7 +121,7 @@ const Asset = () => { key: 'network', sorter: true, fieldProps: { - placeholder: '示例: 127.0.0.1:22' + placeholder: '示例: 127、127.0.0.1、:22、127.0.0.1:22' }, render: (text, record) => { return `${record['ip'] + ':' + record['port']}`; @@ -201,8 +202,15 @@ const Asset = () => { { title: '创建时间', key: 'created', - dataIndex: 'created', sorter: true, + dataIndex: 'created', + hideInSearch: true, + }, + { + title: '最后接入时间', + key: 'lastAccessTime', + sorter: true, + dataIndex: 'lastAccessTime', hideInSearch: true, }, { @@ -270,12 +278,32 @@ const Asset = () => { case "change-owner": handleChangeOwner(record); break; + case 'asset-detail': + navigate(`/asset/${record['id']}?activeKey=info`); + break; + case 'asset-authorised-user': + navigate(`/asset/${record['id']}?activeKey=bind-user`); + break; + case 'asset-authorised-user-group': + navigate(`/asset/${record['id']}?activeKey=bind-user-group`); + break; } }} menus={[ {key: 'copy', name: '复制', disabled: !hasMenu('asset-copy')}, {key: 'test', name: '连通性测试', disabled: !hasMenu('asset-conn-test')}, {key: 'change-owner', name: '更换所有者', disabled: !hasMenu('asset-change-owner')}, + {key: 'asset-detail', name: '详情', disabled: !hasMenu('asset-detail')}, + { + key: 'asset-authorised-user', + name: '授权用户', + disabled: !hasMenu('asset-authorised-user') + }, + { + key: 'asset-authorised-user-group', + name: '授权用户组', + disabled: !hasMenu('asset-authorised-user-group') + }, ]} />, ] @@ -362,6 +390,8 @@ const Asset = () => { if (split.length >= 2) { ip = split[0]; port = split[1]; + } else { + ip = split[0]; } } @@ -379,7 +409,7 @@ const Asset = () => { order: order } let result = await api.getPaging(queryParams); - setItems(result['items']) + setItems(result['items']); return { data: items, success: true, @@ -402,6 +432,7 @@ const Asset = () => { }} pagination={{ defaultPageSize: 10, + showSizeChanger: true }} dateFormatter="string" headerTitle="资产列表" diff --git a/web/src/components/authorised/Strategy.js b/web/src/components/authorised/Strategy.js index bae29a9..7ccd8c9 100644 --- a/web/src/components/authorised/Strategy.js +++ b/web/src/components/authorised/Strategy.js @@ -112,6 +112,10 @@ const Strategy = () => { valueType: 'option', key: 'option', render: (text, record, _, action) => [ + + 详情 + + , { labelWidth: 'auto', }} pagination={{ - pageSize: 10, + defaultPageSize: 10, }} dateFormatter="string" headerTitle="授权策略" diff --git a/web/src/components/devops/BatchCommand.js b/web/src/components/devops/BatchCommand.js index 04d3a14..016cfeb 100644 --- a/web/src/components/devops/BatchCommand.js +++ b/web/src/components/devops/BatchCommand.js @@ -1,5 +1,5 @@ import React, {useState} from 'react'; -import {Badge, Col, Divider, Layout, Row, Space, Table, Tag, Tooltip, Typography} from "antd"; +import {Badge, Divider, Layout, Space, Table, Tag, Tooltip, Typography} from "antd"; import {ProTable} from "@ant-design/pro-components"; import {PROTOCOL_COLORS} from "../../common/constants"; import assetApi from "../../api/asset"; diff --git a/web/src/components/devops/ExecuteCommand.js b/web/src/components/devops/ExecuteCommand.js index 5cc07bc..41f5e5b 100644 --- a/web/src/components/devops/ExecuteCommand.js +++ b/web/src/components/devops/ExecuteCommand.js @@ -15,6 +15,7 @@ import {wsServer} from "../../common/env"; import {CloseOutlined} from "@ant-design/icons"; import {useQuery} from "react-query"; import {xtermScrollPretty} from "../../utils/xterm-scroll-pretty"; +import strings from "../../utils/strings"; const {Search} = Input; const {Content} = Layout; @@ -25,8 +26,21 @@ const ExecuteCommand = () => { const [searchParams, _] = useSearchParams(); let commandId = searchParams.get('commandId'); - let commandQuery = useQuery('commandQuery', () => commandApi.getById(commandId)); - let [inputValue, setInputValue] = useState(commandQuery.data?.content); + let commandQuery = useQuery('commandQuery', () => commandApi.getById(commandId),{ + onSuccess: data => { + let commands = data.content.split('\n'); + if (!commands) { + return; + } + + items.forEach(item => { + if (getReady(item['id']) === false) { + initTerm(item['id'], commands); + } + }) + } + }); + let [inputValue, setInputValue] = useState(''); let items = JSON.parse(searchParams.get('assets')); let [assets, setAssets] = useState(items); @@ -39,13 +53,6 @@ const ExecuteCommand = () => { useEffect(() => { - items.forEach(item => { - console.log(getReady(item['id'])); - if (getReady(item['id']) === false) { - initTerm(item['id']); - } - }) - window.addEventListener('resize', handleWindowResize); return function cleanup() { @@ -104,7 +111,7 @@ const ExecuteCommand = () => { return readies[id]; } - const initTerm = async (assetId) => { + const initTerm = async (assetId, commands) => { let session = await sessionApi.create(assetId, 'native'); let sessionId = session['id']; @@ -151,6 +158,14 @@ const ExecuteCommand = () => { case Message.Connected: term.clear(); sessionApi.connect(sessionId); + + for (let i = 0; i < commands.length; i++) { + let command = commands[i]; + if (!strings.hasText(command)) { + continue + } + webSocket.send(new Message(Message.Data, command + String.fromCharCode(13)).toString()); + } break; case Message.Data: term.write(msg['content']); diff --git a/web/src/components/devops/Job.js b/web/src/components/devops/Job.js index 3eae057..fd0f0c3 100644 --- a/web/src/components/devops/Job.js +++ b/web/src/components/devops/Job.js @@ -44,7 +44,8 @@ const Job = () => { key: 'status', hideInSearch: true, render: (status, record, index) => { - return handleChangeStatus(record['id'], checked ? 'running' : 'not-running', index)} /> } @@ -218,7 +219,7 @@ const Job = () => { labelWidth: 'auto', }} pagination={{ - pageSize: 10, + defaultPageSize: 10, }} dateFormatter="string" headerTitle="计划任务列表" @@ -245,6 +246,11 @@ const Job = () => { setConfirmLoading(true); try { + if (values['func'] === 'shell-job') { + values['metadata'] = JSON.stringify({ + 'shell': values['shell'] + }); + } let success; if (values['id']) { success = await api.updateById(values['id'], values); diff --git a/web/src/components/devops/JobModal.js b/web/src/components/devops/JobModal.js index c50a15e..a591376 100644 --- a/web/src/components/devops/JobModal.js +++ b/web/src/components/devops/JobModal.js @@ -33,6 +33,14 @@ const JobModal = ({ const getItem = async () => { let data = await jobApi.getById(id); if (data) { + if (data['func'] === 'shell-job') { + try { + data['shell'] = JSON.parse(data['metadata'])['shell']; + } catch (e) { + data['shell'] = ''; + } + + } form.setFieldsValue(data); setMode(data['mode']); setFunc(data['func']); @@ -65,10 +73,11 @@ const JobModal = ({ form .validateFields() .then(async values => { - let ok = await handleOk(values); - if (ok) { - form.resetFields(); + if (values['resourceIds']) { + values['resourceIds'] = values['resourceIds'].join(','); } + form.resetFields(); + handleOk(values); }); }} onCancel={() => { @@ -100,7 +109,8 @@ const JobModal = ({ { func === 'shell-job' ? - +