- 修复mysql模式下「资产授权列表」「用户授权列表」「用户组授权列表」无法使用的问题 fixed #315
- 修复资产新增、修改无权限的缺陷 fixed #314 - 修复执行动态指令时多行失败且无法自动执行的问题 fixed #313 #310 - 修复计划任务无法选择资产的问题 fixed #312 - 修复导入导出备份无效的问题 fixed #303 - 增加「资产详情」「资产授权」「用户详情」「用户授权」「用户组详情」「用户组授权」「角色详情」「授权策略详情」按钮 - 修复资产列表使用IP搜索无效的问题 - 资产列表增加最近接入时间排序、增加修改每页数量 fixed #311 - 修复登录页面双因素认证输入框无法自动获取焦点的问题 fixed #311 - 增加普通页面资产列表最后接入时间排序 fixed #311 - 计划任务增加执行本机系统命令
This commit is contained in:
parent
49797164ce
commit
ded4dc492a
@ -27,6 +27,7 @@ const (
|
|||||||
JobStatusNotRunning = "not-running" // 计划任务未运行状态
|
JobStatusNotRunning = "not-running" // 计划任务未运行状态
|
||||||
FuncCheckAssetStatusJob = "check-asset-status-job" // 检测资产是否在线
|
FuncCheckAssetStatusJob = "check-asset-status-job" // 检测资产是否在线
|
||||||
FuncShellJob = "shell-job" // 执行Shell脚本
|
FuncShellJob = "shell-job" // 执行Shell脚本
|
||||||
|
JobModeSelf = "self" // 本机
|
||||||
JobModeAll = "all" // 全部资产
|
JobModeAll = "all" // 全部资产
|
||||||
JobModeCustom = "custom" // 自定义选择资产
|
JobModeCustom = "custom" // 自定义选择资产
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ type Asset struct {
|
|||||||
Active bool `json:"active"`
|
Active bool `json:"active"`
|
||||||
ActiveMessage string `gorm:"type:varchar(200)" json:"activeMessage"`
|
ActiveMessage string `gorm:"type:varchar(200)" json:"activeMessage"`
|
||||||
Created common.JsonTime `json:"created"`
|
Created common.JsonTime `json:"created"`
|
||||||
|
LastAccessTime common.JsonTime `json:"lastAccessTime"`
|
||||||
Tags string `json:"tags"`
|
Tags string `json:"tags"`
|
||||||
Owner string `gorm:"index,type:varchar(36)" json:"owner"`
|
Owner string `gorm:"index,type:varchar(36)" json:"owner"`
|
||||||
Encrypted bool `json:"encrypted"`
|
Encrypted bool `json:"encrypted"`
|
||||||
@ -38,6 +39,7 @@ type AssetForPage struct {
|
|||||||
Active bool `json:"active"`
|
Active bool `json:"active"`
|
||||||
ActiveMessage string `json:"activeMessage"`
|
ActiveMessage string `json:"activeMessage"`
|
||||||
Created common.JsonTime `json:"created"`
|
Created common.JsonTime `json:"created"`
|
||||||
|
LastAccessTime common.JsonTime `json:"lastAccessTime"`
|
||||||
Tags string `json:"tags"`
|
Tags string `json:"tags"`
|
||||||
Owner string `json:"owner"`
|
Owner string `json:"owner"`
|
||||||
OwnerName string `json:"ownerName"`
|
OwnerName string `json:"ownerName"`
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"next-terminal/server/common"
|
||||||
"next-terminal/server/common/maps"
|
"next-terminal/server/common/maps"
|
||||||
"next-terminal/server/common/nt"
|
"next-terminal/server/common/nt"
|
||||||
"next-terminal/server/config"
|
"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) {
|
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")
|
dbCounter := r.GetDB(c).Table("assets")
|
||||||
|
|
||||||
if len(name) > 0 {
|
if len(name) > 0 {
|
||||||
@ -104,7 +105,8 @@ func (r assetRepository) Find(c context.Context, pageIndex, pageSize int, name,
|
|||||||
case "protocol":
|
case "protocol":
|
||||||
case "ip":
|
case "ip":
|
||||||
case "active":
|
case "active":
|
||||||
|
case "lastAccessTime":
|
||||||
|
field = "last_access_time"
|
||||||
default:
|
default:
|
||||||
field = "created"
|
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) {
|
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)
|
Where("id in ?", assetIds)
|
||||||
dbCounter := r.GetDB(c).Table("assets").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 "protocol":
|
||||||
case "ip":
|
case "ip":
|
||||||
case "active":
|
case "active":
|
||||||
|
case "lastAccessTime":
|
||||||
|
field = "last_access_time"
|
||||||
default:
|
default:
|
||||||
field = "created"
|
field = "created"
|
||||||
}
|
}
|
||||||
@ -362,3 +365,8 @@ func (r assetRepository) FindMyAssetTags(c context.Context, assetIds []string) (
|
|||||||
|
|
||||||
return utils.Distinct(o), nil
|
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
|
||||||
|
}
|
||||||
|
@ -71,8 +71,7 @@ func (r authorisedRepository) FindAssetPage(c context.Context, pageIndex, pageSi
|
|||||||
db := r.GetDB(c).Table("assets").
|
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 ").
|
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 authorised on authorised.asset_id = assets.id").
|
||||||
Joins("left join strategies on strategies.id = authorised.strategy_id").
|
Joins("left join strategies on strategies.id = authorised.strategy_id")
|
||||||
Group("assets.id")
|
|
||||||
dbCounter := r.GetDB(c).Table("assets").Joins("left join authorised on assets.id = authorised.asset_id").Group("assets.id")
|
dbCounter := r.GetDB(c).Table("assets").Joins("left join authorised on assets.id = authorised.asset_id").Group("assets.id")
|
||||||
|
|
||||||
if assetName != "" {
|
if assetName != "" {
|
||||||
@ -110,8 +109,7 @@ func (r authorisedRepository) FindUserPage(c context.Context, pageIndex, pageSiz
|
|||||||
db := r.GetDB(c).Table("users").
|
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 ").
|
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 authorised on authorised.user_id = users.id").
|
||||||
Joins("left join strategies on strategies.id = authorised.strategy_id").
|
Joins("left join strategies on strategies.id = authorised.strategy_id")
|
||||||
Group("users.id")
|
|
||||||
dbCounter := r.GetDB(c).Table("assets").Joins("left join authorised on assets.id = authorised.asset_id").Group("assets.id")
|
dbCounter := r.GetDB(c).Table("assets").Joins("left join authorised on assets.id = authorised.asset_id").Group("assets.id")
|
||||||
|
|
||||||
if userName != "" {
|
if userName != "" {
|
||||||
@ -140,8 +138,7 @@ func (r authorisedRepository) FindUserGroupPage(c context.Context, pageIndex, pa
|
|||||||
db := r.GetDB(c).Table("user_groups").
|
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 ").
|
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 authorised on authorised.user_group_id = user_groups.id").
|
||||||
Joins("left join strategies on strategies.id = authorised.strategy_id").
|
Joins("left join strategies on strategies.id = authorised.strategy_id")
|
||||||
Group("user_groups.id")
|
|
||||||
dbCounter := r.GetDB(c).Table("assets").Joins("left join authorised on assets.id = authorised.asset_id").Group("assets.id")
|
dbCounter := r.GetDB(c).Table("assets").Joins("left join authorised on assets.id = authorised.asset_id").Group("assets.id")
|
||||||
|
|
||||||
if userName != "" {
|
if userName != "" {
|
||||||
|
@ -55,7 +55,12 @@ func getJob(j *model.Job) (job cron.Job, err error) {
|
|||||||
Metadata: j.Metadata,
|
Metadata: j.Metadata,
|
||||||
}
|
}
|
||||||
case nt.FuncShellJob:
|
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:
|
default:
|
||||||
return nil, errors.New("未识别的任务")
|
return nil, errors.New("未识别的任务")
|
||||||
}
|
}
|
||||||
|
@ -5,12 +5,12 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"next-terminal/server/common"
|
|
||||||
"next-terminal/server/common/nt"
|
|
||||||
"next-terminal/server/common/term"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"next-terminal/server/common"
|
||||||
|
"next-terminal/server/common/nt"
|
||||||
|
"next-terminal/server/common/term"
|
||||||
"next-terminal/server/log"
|
"next-terminal/server/log"
|
||||||
"next-terminal/server/model"
|
"next-terminal/server/model"
|
||||||
"next-terminal/server/repository"
|
"next-terminal/server/repository"
|
||||||
@ -35,13 +35,19 @@ func (r ShellJob) Run() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var assets []model.Asset
|
switch r.Mode {
|
||||||
if r.Mode == nt.JobModeAll {
|
case nt.JobModeAll:
|
||||||
assets, _ = repository.AssetRepository.FindByProtocol(context.TODO(), "ssh")
|
assets, _ := repository.AssetRepository.FindByProtocol(context.TODO(), "ssh")
|
||||||
} else {
|
r.executeShellByAssets(assets)
|
||||||
assets, _ = repository.AssetRepository.FindByProtocolAndIds(context.TODO(), "ssh", strings.Split(r.ResourceIds, ","))
|
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 {
|
if len(assets) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -89,7 +95,7 @@ func (r ShellJob) Run() {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
t1 := time.Now()
|
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)
|
elapsed := time.Since(t1)
|
||||||
var msg string
|
var msg string
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -124,7 +130,36 @@ func (r ShellJob) Run() {
|
|||||||
_ = repository.JobLogRepository.Create(context.TODO(), &jobLog)
|
_ = 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 != "-" {
|
if accessGatewayId != "" && accessGatewayId != "-" {
|
||||||
g, err := GatewayService.GetGatewayById(accessGatewayId)
|
g, err := GatewayService.GetGatewayById(accessGatewayId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -33,10 +33,12 @@ var DefaultMenu = []*model.Menu{
|
|||||||
),
|
),
|
||||||
model.NewMenu("asset-add", "新建", "asset",
|
model.NewMenu("asset-add", "新建", "asset",
|
||||||
model.NewPermission("POST", "/assets"),
|
model.NewPermission("POST", "/assets"),
|
||||||
|
model.NewPermission("GET", "/access-gateways"),
|
||||||
),
|
),
|
||||||
model.NewMenu("asset-edit", "编辑", "asset",
|
model.NewMenu("asset-edit", "编辑", "asset",
|
||||||
model.NewPermission("GET", "/assets/:id"),
|
model.NewPermission("GET", "/assets/:id"),
|
||||||
model.NewPermission("PUT", "/assets/:id"),
|
model.NewPermission("PUT", "/assets/:id"),
|
||||||
|
model.NewPermission("GET", "/access-gateways"),
|
||||||
),
|
),
|
||||||
model.NewMenu("asset-del", "删除", "asset",
|
model.NewMenu("asset-del", "删除", "asset",
|
||||||
model.NewPermission("DELETE", "/assets/:id"),
|
model.NewPermission("DELETE", "/assets/:id"),
|
||||||
@ -191,6 +193,10 @@ var DefaultMenu = []*model.Menu{
|
|||||||
model.NewPermission("POST", "/storage-logs/clear"),
|
model.NewPermission("POST", "/storage-logs/clear"),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
model.NewMenu("session-command", "命令日志", "log-audit",
|
||||||
|
model.NewPermission("GET", "/session-commands/paging"),
|
||||||
|
),
|
||||||
|
|
||||||
model.NewMenu("ops", "系统运维", "root"),
|
model.NewMenu("ops", "系统运维", "root"),
|
||||||
|
|
||||||
model.NewMenu("job", "计划任务", "ops",
|
model.NewMenu("job", "计划任务", "ops",
|
||||||
|
@ -366,6 +366,9 @@ func (service sessionService) Create(clientIp, assetId, mode string, user *model
|
|||||||
if err := repository.SessionRepository.Create(context.TODO(), s); err != nil {
|
if err := repository.SessionRepository.Create(context.TODO(), s); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := repository.AssetRepository.UpdateLastAccessTime(context.Background(), s.AssetId, common.NowJsonTime()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
18
server/utils/command.go
Normal file
18
server/utils/command.go
Normal file
@ -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
|
||||||
|
}
|
22
server/utils/command_test.go
Normal file
22
server/utils/command_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -2,14 +2,15 @@ import React, {useEffect, useState} from 'react';
|
|||||||
import {Button, Card, Checkbox, Form, Input, message, Modal, Typography} from "antd";
|
import {Button, Card, Checkbox, Form, Input, message, Modal, Typography} from "antd";
|
||||||
import './Login.css'
|
import './Login.css'
|
||||||
import request from "../common/request";
|
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 {setToken} from "../utils/utils";
|
||||||
import brandingApi from "../api/branding";
|
import brandingApi from "../api/branding";
|
||||||
import strings from "../utils/strings";
|
import strings from "../utils/strings";
|
||||||
import {useNavigate} from "react-router-dom";
|
import {useNavigate} from "react-router-dom";
|
||||||
import {setCurrentUser} from "../service/permission";
|
import {setCurrentUser} from "../service/permission";
|
||||||
|
import PromptModal from "../dd/prompt-modal/prompt-modal";
|
||||||
|
|
||||||
const {Title} = Typography;
|
const {Title, Text} = Typography;
|
||||||
|
|
||||||
const LoginForm = () => {
|
const LoginForm = () => {
|
||||||
|
|
||||||
@ -17,6 +18,8 @@ const LoginForm = () => {
|
|||||||
|
|
||||||
let [inLogin, setInLogin] = useState(false);
|
let [inLogin, setInLogin] = useState(false);
|
||||||
let [branding, setBranding] = useState({});
|
let [branding, setBranding] = useState({});
|
||||||
|
let [prompt, setPrompt] = useState(false);
|
||||||
|
let [account, setAccount] = useState({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const x = async () => {
|
const x = async () => {
|
||||||
@ -62,17 +65,6 @@ const LoginForm = () => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const showTOTP = (loginAccount) => {
|
|
||||||
let value = '';
|
|
||||||
Modal.confirm({
|
|
||||||
title: '双因素认证',
|
|
||||||
icon: <LockTwoTone/>,
|
|
||||||
content: <Input onChange={e => value = e.target.value} onPressEnter={() => handleOk(loginAccount, value)}
|
|
||||||
placeholder="请输入双因素认证码"/>,
|
|
||||||
onOk: () => handleOk(loginAccount, value),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = async params => {
|
const handleSubmit = async params => {
|
||||||
setInLogin(true);
|
setInLogin(true);
|
||||||
|
|
||||||
@ -80,7 +72,8 @@ const LoginForm = () => {
|
|||||||
let result = await request.post('/login', params);
|
let result = await request.post('/login', params);
|
||||||
if (result.code === 100) {
|
if (result.code === 100) {
|
||||||
// 进行双因素认证
|
// 进行双因素认证
|
||||||
showTOTP(params);
|
setPrompt(true);
|
||||||
|
setAccount(params);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (result.code !== 1) {
|
if (result.code !== 1) {
|
||||||
@ -100,7 +93,7 @@ const LoginForm = () => {
|
|||||||
<Card className='login-card' title={null}>
|
<Card className='login-card' title={null}>
|
||||||
<div style={{textAlign: "center", margin: '15px auto 30px auto', color: '#1890ff'}}>
|
<div style={{textAlign: "center", margin: '15px auto 30px auto', color: '#1890ff'}}>
|
||||||
<Title level={1}>{branding['name']}</Title>
|
<Title level={1}>{branding['name']}</Title>
|
||||||
{/*<Text>一个轻量级的堡垒机系统</Text>*/}
|
<Text>{branding['description']}</Text>
|
||||||
</div>
|
</div>
|
||||||
<Form onFinish={handleSubmit} className="login-form">
|
<Form onFinish={handleSubmit} className="login-form">
|
||||||
<Form.Item name='username' rules={[{required: true, message: '请输入登录账号!'}]}>
|
<Form.Item name='username' rules={[{required: true, message: '请输入登录账号!'}]}>
|
||||||
@ -120,6 +113,18 @@ const LoginForm = () => {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<PromptModal
|
||||||
|
title={'双因素认证'}
|
||||||
|
open={prompt}
|
||||||
|
onOk={(value) => {
|
||||||
|
handleOk(account, value)
|
||||||
|
}}
|
||||||
|
onCancel={() => setPrompt(false)}
|
||||||
|
placeholder={"请输入双因素认证码"}
|
||||||
|
>
|
||||||
|
|
||||||
|
</PromptModal>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
);
|
);
|
||||||
|
@ -14,7 +14,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Upload
|
Upload
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import {Link} from "react-router-dom";
|
import {Link, useNavigate} from "react-router-dom";
|
||||||
import {ProTable, TableDropdown} from "@ant-design/pro-components";
|
import {ProTable, TableDropdown} from "@ant-design/pro-components";
|
||||||
import assetApi from "../../api/asset";
|
import assetApi from "../../api/asset";
|
||||||
import tagApi from "../../api/tag";
|
import tagApi from "../../api/tag";
|
||||||
@ -61,6 +61,7 @@ const Asset = () => {
|
|||||||
const [columnsStateMap, setColumnsStateMap] = useColumnState(ColumnState.ASSET);
|
const [columnsStateMap, setColumnsStateMap] = useColumnState(ColumnState.ASSET);
|
||||||
|
|
||||||
const tagQuery = useQuery('getAllTag', tagApi.getAll);
|
const tagQuery = useQuery('getAllTag', tagApi.getAll);
|
||||||
|
let navigate = useNavigate();
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
@ -120,7 +121,7 @@ const Asset = () => {
|
|||||||
key: 'network',
|
key: 'network',
|
||||||
sorter: true,
|
sorter: true,
|
||||||
fieldProps: {
|
fieldProps: {
|
||||||
placeholder: '示例: 127.0.0.1:22'
|
placeholder: '示例: 127、127.0.0.1、:22、127.0.0.1:22'
|
||||||
},
|
},
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
return `${record['ip'] + ':' + record['port']}`;
|
return `${record['ip'] + ':' + record['port']}`;
|
||||||
@ -201,8 +202,15 @@ const Asset = () => {
|
|||||||
{
|
{
|
||||||
title: '创建时间',
|
title: '创建时间',
|
||||||
key: 'created',
|
key: 'created',
|
||||||
dataIndex: 'created',
|
|
||||||
sorter: true,
|
sorter: true,
|
||||||
|
dataIndex: 'created',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '最后接入时间',
|
||||||
|
key: 'lastAccessTime',
|
||||||
|
sorter: true,
|
||||||
|
dataIndex: 'lastAccessTime',
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -270,12 +278,32 @@ const Asset = () => {
|
|||||||
case "change-owner":
|
case "change-owner":
|
||||||
handleChangeOwner(record);
|
handleChangeOwner(record);
|
||||||
break;
|
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={[
|
menus={[
|
||||||
{key: 'copy', name: '复制', disabled: !hasMenu('asset-copy')},
|
{key: 'copy', name: '复制', disabled: !hasMenu('asset-copy')},
|
||||||
{key: 'test', name: '连通性测试', disabled: !hasMenu('asset-conn-test')},
|
{key: 'test', name: '连通性测试', disabled: !hasMenu('asset-conn-test')},
|
||||||
{key: 'change-owner', name: '更换所有者', disabled: !hasMenu('asset-change-owner')},
|
{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) {
|
if (split.length >= 2) {
|
||||||
ip = split[0];
|
ip = split[0];
|
||||||
port = split[1];
|
port = split[1];
|
||||||
|
} else {
|
||||||
|
ip = split[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -379,7 +409,7 @@ const Asset = () => {
|
|||||||
order: order
|
order: order
|
||||||
}
|
}
|
||||||
let result = await api.getPaging(queryParams);
|
let result = await api.getPaging(queryParams);
|
||||||
setItems(result['items'])
|
setItems(result['items']);
|
||||||
return {
|
return {
|
||||||
data: items,
|
data: items,
|
||||||
success: true,
|
success: true,
|
||||||
@ -402,6 +432,7 @@ const Asset = () => {
|
|||||||
}}
|
}}
|
||||||
pagination={{
|
pagination={{
|
||||||
defaultPageSize: 10,
|
defaultPageSize: 10,
|
||||||
|
showSizeChanger: true
|
||||||
}}
|
}}
|
||||||
dateFormatter="string"
|
dateFormatter="string"
|
||||||
headerTitle="资产列表"
|
headerTitle="资产列表"
|
||||||
|
@ -112,6 +112,10 @@ const Strategy = () => {
|
|||||||
valueType: 'option',
|
valueType: 'option',
|
||||||
key: 'option',
|
key: 'option',
|
||||||
render: (text, record, _, action) => [
|
render: (text, record, _, action) => [
|
||||||
|
<Show menu={'strategy-detail'} key={'strategy-get'}>
|
||||||
|
<Link key="get" to={`/strategy/${record['id']}`}>详情</Link>
|
||||||
|
</Show>
|
||||||
|
,
|
||||||
<Show menu={'strategy-edit'} key={'strategy-edit'}>
|
<Show menu={'strategy-edit'} key={'strategy-edit'}>
|
||||||
<a
|
<a
|
||||||
key="edit"
|
key="edit"
|
||||||
@ -182,7 +186,7 @@ const Strategy = () => {
|
|||||||
labelWidth: 'auto',
|
labelWidth: 'auto',
|
||||||
}}
|
}}
|
||||||
pagination={{
|
pagination={{
|
||||||
pageSize: 10,
|
defaultPageSize: 10,
|
||||||
}}
|
}}
|
||||||
dateFormatter="string"
|
dateFormatter="string"
|
||||||
headerTitle="授权策略"
|
headerTitle="授权策略"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, {useState} from 'react';
|
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 {ProTable} from "@ant-design/pro-components";
|
||||||
import {PROTOCOL_COLORS} from "../../common/constants";
|
import {PROTOCOL_COLORS} from "../../common/constants";
|
||||||
import assetApi from "../../api/asset";
|
import assetApi from "../../api/asset";
|
||||||
|
@ -15,6 +15,7 @@ import {wsServer} from "../../common/env";
|
|||||||
import {CloseOutlined} from "@ant-design/icons";
|
import {CloseOutlined} from "@ant-design/icons";
|
||||||
import {useQuery} from "react-query";
|
import {useQuery} from "react-query";
|
||||||
import {xtermScrollPretty} from "../../utils/xterm-scroll-pretty";
|
import {xtermScrollPretty} from "../../utils/xterm-scroll-pretty";
|
||||||
|
import strings from "../../utils/strings";
|
||||||
|
|
||||||
const {Search} = Input;
|
const {Search} = Input;
|
||||||
const {Content} = Layout;
|
const {Content} = Layout;
|
||||||
@ -25,8 +26,21 @@ const ExecuteCommand = () => {
|
|||||||
const [searchParams, _] = useSearchParams();
|
const [searchParams, _] = useSearchParams();
|
||||||
let commandId = searchParams.get('commandId');
|
let commandId = searchParams.get('commandId');
|
||||||
|
|
||||||
let commandQuery = useQuery('commandQuery', () => commandApi.getById(commandId));
|
let commandQuery = useQuery('commandQuery', () => commandApi.getById(commandId),{
|
||||||
let [inputValue, setInputValue] = useState(commandQuery.data?.content);
|
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 items = JSON.parse(searchParams.get('assets'));
|
||||||
let [assets, setAssets] = useState(items);
|
let [assets, setAssets] = useState(items);
|
||||||
@ -39,13 +53,6 @@ const ExecuteCommand = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
items.forEach(item => {
|
|
||||||
console.log(getReady(item['id']));
|
|
||||||
if (getReady(item['id']) === false) {
|
|
||||||
initTerm(item['id']);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
window.addEventListener('resize', handleWindowResize);
|
window.addEventListener('resize', handleWindowResize);
|
||||||
|
|
||||||
return function cleanup() {
|
return function cleanup() {
|
||||||
@ -104,7 +111,7 @@ const ExecuteCommand = () => {
|
|||||||
return readies[id];
|
return readies[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
const initTerm = async (assetId) => {
|
const initTerm = async (assetId, commands) => {
|
||||||
let session = await sessionApi.create(assetId, 'native');
|
let session = await sessionApi.create(assetId, 'native');
|
||||||
let sessionId = session['id'];
|
let sessionId = session['id'];
|
||||||
|
|
||||||
@ -151,6 +158,14 @@ const ExecuteCommand = () => {
|
|||||||
case Message.Connected:
|
case Message.Connected:
|
||||||
term.clear();
|
term.clear();
|
||||||
sessionApi.connect(sessionId);
|
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;
|
break;
|
||||||
case Message.Data:
|
case Message.Data:
|
||||||
term.write(msg['content']);
|
term.write(msg['content']);
|
||||||
|
@ -44,7 +44,8 @@ const Job = () => {
|
|||||||
key: 'status',
|
key: 'status',
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
render: (status, record, index) => {
|
render: (status, record, index) => {
|
||||||
return <Switch disabled={!hasMenu('job-change-status')} checkedChildren="开启" unCheckedChildren="关闭" checked={status === 'running'}
|
return <Switch disabled={!hasMenu('job-change-status')} checkedChildren="开启" unCheckedChildren="关闭"
|
||||||
|
checked={status === 'running'}
|
||||||
onChange={(checked) => handleChangeStatus(record['id'], checked ? 'running' : 'not-running', index)}
|
onChange={(checked) => handleChangeStatus(record['id'], checked ? 'running' : 'not-running', index)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@ -218,7 +219,7 @@ const Job = () => {
|
|||||||
labelWidth: 'auto',
|
labelWidth: 'auto',
|
||||||
}}
|
}}
|
||||||
pagination={{
|
pagination={{
|
||||||
pageSize: 10,
|
defaultPageSize: 10,
|
||||||
}}
|
}}
|
||||||
dateFormatter="string"
|
dateFormatter="string"
|
||||||
headerTitle="计划任务列表"
|
headerTitle="计划任务列表"
|
||||||
@ -245,6 +246,11 @@ const Job = () => {
|
|||||||
setConfirmLoading(true);
|
setConfirmLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (values['func'] === 'shell-job') {
|
||||||
|
values['metadata'] = JSON.stringify({
|
||||||
|
'shell': values['shell']
|
||||||
|
});
|
||||||
|
}
|
||||||
let success;
|
let success;
|
||||||
if (values['id']) {
|
if (values['id']) {
|
||||||
success = await api.updateById(values['id'], values);
|
success = await api.updateById(values['id'], values);
|
||||||
|
@ -33,6 +33,14 @@ const JobModal = ({
|
|||||||
const getItem = async () => {
|
const getItem = async () => {
|
||||||
let data = await jobApi.getById(id);
|
let data = await jobApi.getById(id);
|
||||||
if (data) {
|
if (data) {
|
||||||
|
if (data['func'] === 'shell-job') {
|
||||||
|
try {
|
||||||
|
data['shell'] = JSON.parse(data['metadata'])['shell'];
|
||||||
|
} catch (e) {
|
||||||
|
data['shell'] = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
form.setFieldsValue(data);
|
form.setFieldsValue(data);
|
||||||
setMode(data['mode']);
|
setMode(data['mode']);
|
||||||
setFunc(data['func']);
|
setFunc(data['func']);
|
||||||
@ -65,10 +73,11 @@ const JobModal = ({
|
|||||||
form
|
form
|
||||||
.validateFields()
|
.validateFields()
|
||||||
.then(async values => {
|
.then(async values => {
|
||||||
let ok = await handleOk(values);
|
if (values['resourceIds']) {
|
||||||
if (ok) {
|
values['resourceIds'] = values['resourceIds'].join(',');
|
||||||
form.resetFields();
|
|
||||||
}
|
}
|
||||||
|
form.resetFields();
|
||||||
|
handleOk(values);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
@ -100,7 +109,8 @@ const JobModal = ({
|
|||||||
|
|
||||||
{
|
{
|
||||||
func === 'shell-job' ?
|
func === 'shell-job' ?
|
||||||
<Form.Item label="Shell脚本" name='shell' rules={[{required: true, message: '请输入Shell脚本'}]}>
|
<Form.Item label="Shell脚本" name='shell'
|
||||||
|
rules={[{required: true, message: '请输入Shell脚本'}]}>
|
||||||
<TextArea autoSize={{minRows: 5, maxRows: 10}} placeholder="在此处填写Shell脚本内容"/>
|
<TextArea autoSize={{minRows: 5, maxRows: 10}} placeholder="在此处填写Shell脚本内容"/>
|
||||||
</Form.Item> : undefined
|
</Form.Item> : undefined
|
||||||
}
|
}
|
||||||
@ -115,11 +125,12 @@ const JobModal = ({
|
|||||||
}}>
|
}}>
|
||||||
<Radio value={'all'}>全部资产</Radio>
|
<Radio value={'all'}>全部资产</Radio>
|
||||||
<Radio value={'custom'}>自定义</Radio>
|
<Radio value={'custom'}>自定义</Radio>
|
||||||
|
<Radio value={'self'}>本机</Radio>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
{
|
{
|
||||||
mode === 'custom' ?
|
mode === 'custom' &&
|
||||||
<Spin tip='加载中...' spinning={resourcesLoading}>
|
<Spin tip='加载中...' spinning={resourcesLoading}>
|
||||||
<Form.Item label="已选择资产" name='resourceIds' rules={[{required: true}]}>
|
<Form.Item label="已选择资产" name='resourceIds' rules={[{required: true}]}>
|
||||||
<Select
|
<Select
|
||||||
@ -135,7 +146,6 @@ const JobModal = ({
|
|||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Spin>
|
</Spin>
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -33,6 +33,12 @@ const OfflineSession = () => {
|
|||||||
dataIndex: 'clientIp',
|
dataIndex: 'clientIp',
|
||||||
key: 'clientIp',
|
key: 'clientIp',
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
|
},{
|
||||||
|
title: 'IP定位',
|
||||||
|
tip: '国家|区域|省份|城市|ISP',
|
||||||
|
dataIndex: 'region',
|
||||||
|
key: 'region',
|
||||||
|
hideInSearch: true,
|
||||||
}, {
|
}, {
|
||||||
title: '接入方式',
|
title: '接入方式',
|
||||||
dataIndex: 'mode',
|
dataIndex: 'mode',
|
||||||
@ -102,6 +108,7 @@ const OfflineSession = () => {
|
|||||||
key: 'option',
|
key: 'option',
|
||||||
render: (text, record, _, action) => {
|
render: (text, record, _, action) => {
|
||||||
let disablePlayback = record['recording'] !== '1';
|
let disablePlayback = record['recording'] !== '1';
|
||||||
|
let disableCmdRecord = record['commandCount'] === 0;
|
||||||
return [
|
return [
|
||||||
<Show menu={'offline-session-playback'} key={'offline-session-playback'}>
|
<Show menu={'offline-session-playback'} key={'offline-session-playback'}>
|
||||||
<Button
|
<Button
|
||||||
@ -127,6 +134,19 @@ const OfflineSession = () => {
|
|||||||
回放
|
回放
|
||||||
</Button>
|
</Button>
|
||||||
</Show>,
|
</Show>,
|
||||||
|
<Show menu={'offline-session-command'} key={'offline-session-command'}>
|
||||||
|
<Button
|
||||||
|
key='command'
|
||||||
|
disabled={disableCmdRecord}
|
||||||
|
type="link"
|
||||||
|
size='small'
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedRow(record);
|
||||||
|
setSessionCommandVisible(true);
|
||||||
|
}}>
|
||||||
|
命令记录({record['commandCount']})
|
||||||
|
</Button>
|
||||||
|
</Show>,
|
||||||
<Show menu={'offline-session-del'} key={'offline-session-del'}>
|
<Show menu={'offline-session-del'} key={'offline-session-del'}>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
key={'confirm-delete'}
|
key={'confirm-delete'}
|
||||||
@ -196,6 +216,7 @@ const OfflineSession = () => {
|
|||||||
}}
|
}}
|
||||||
pagination={{
|
pagination={{
|
||||||
defaultPageSize: 10,
|
defaultPageSize: 10,
|
||||||
|
showSizeChanger: true
|
||||||
}}
|
}}
|
||||||
dateFormatter="string"
|
dateFormatter="string"
|
||||||
headerTitle="离线会话列表"
|
headerTitle="离线会话列表"
|
||||||
|
@ -60,6 +60,10 @@ const Role = () => {
|
|||||||
valueType: 'option',
|
valueType: 'option',
|
||||||
key: 'option',
|
key: 'option',
|
||||||
render: (text, record, _, action) => [
|
render: (text, record, _, action) => [
|
||||||
|
<Show menu={'role-detail'} key={'role-get'}>
|
||||||
|
<Link key="get" to={`/role/${record['id']}`}>详情</Link>
|
||||||
|
</Show>
|
||||||
|
,
|
||||||
<Show menu={'role-edit'} key={'role-edit'}>
|
<Show menu={'role-edit'} key={'role-edit'}>
|
||||||
<a
|
<a
|
||||||
key="edit"
|
key="edit"
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import React, {useState} from 'react';
|
import React, {useState} from 'react';
|
||||||
|
|
||||||
import {Button, Layout, Popconfirm} from "antd";
|
import {Button, Layout, Popconfirm} from "antd";
|
||||||
import {ProTable} from "@ant-design/pro-components";
|
import {ProTable, TableDropdown} from "@ant-design/pro-components";
|
||||||
import UserGroupModal from "./UserGroupModal";
|
import UserGroupModal from "./UserGroupModal";
|
||||||
import userGroupApi from "../../api/user-group";
|
import userGroupApi from "../../api/user-group";
|
||||||
import {Link} from "react-router-dom";
|
import {Link, useNavigate} from "react-router-dom";
|
||||||
import ColumnState, {useColumnState} from "../../hook/column-state";
|
import ColumnState, {useColumnState} from "../../hook/column-state";
|
||||||
import {hasMenu} from "../../service/permission";
|
import {hasMenu} from "../../service/permission";
|
||||||
import Show from "../../dd/fi/show";
|
import Show from "../../dd/fi/show";
|
||||||
@ -21,6 +21,7 @@ const UserGroup = () => {
|
|||||||
let [selectedRowKey, setSelectedRowKey] = useState(undefined);
|
let [selectedRowKey, setSelectedRowKey] = useState(undefined);
|
||||||
|
|
||||||
const [columnsStateMap, setColumnsStateMap] = useColumnState(ColumnState.USER_GROUP);
|
const [columnsStateMap, setColumnsStateMap] = useColumnState(ColumnState.USER_GROUP);
|
||||||
|
let navigate = useNavigate();
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
@ -77,6 +78,23 @@ const UserGroup = () => {
|
|||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</Show>
|
</Show>
|
||||||
,
|
,
|
||||||
|
<TableDropdown
|
||||||
|
key="actionGroup"
|
||||||
|
onSelect={(key) => {
|
||||||
|
switch (key) {
|
||||||
|
case 'user-group-detail':
|
||||||
|
navigate(`/user-group/${record['id']}?activeKey=info`);
|
||||||
|
break;
|
||||||
|
case 'user-group-authorised-asset':
|
||||||
|
navigate(`/user-group/${record['id']}?activeKey=asset`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
menus={[
|
||||||
|
{key: 'user-group-detail', name: '详情', disabled: !hasMenu('user-group-detail')},
|
||||||
|
{key: 'user-group-authorised-asset', name: '授权资产', disabled: !hasMenu('user-group-authorised-asset')},
|
||||||
|
]}
|
||||||
|
/>,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -3,6 +3,8 @@ import {useParams, useSearchParams} from "react-router-dom";
|
|||||||
import {Tabs} from "antd";
|
import {Tabs} from "antd";
|
||||||
import UserGroupInfo from "./UserGroupInfo";
|
import UserGroupInfo from "./UserGroupInfo";
|
||||||
import UserAsset from "./user/UserAsset";
|
import UserAsset from "./user/UserAsset";
|
||||||
|
import {hasMenu} from "../../service/permission";
|
||||||
|
import UserInfo from "./user/UserInfo";
|
||||||
|
|
||||||
const UserGroupDetail = () => {
|
const UserGroupDetail = () => {
|
||||||
let params = useParams();
|
let params = useParams();
|
||||||
@ -22,12 +24,18 @@ const UserGroupDetail = () => {
|
|||||||
<div>
|
<div>
|
||||||
<div className="page-detail-warp">
|
<div className="page-detail-warp">
|
||||||
<Tabs activeKey={activeKey} onChange={handleTagChange}>
|
<Tabs activeKey={activeKey} onChange={handleTagChange}>
|
||||||
|
{
|
||||||
|
hasMenu('user-group-detail') &&
|
||||||
<Tabs.TabPane tab="基本信息" key="info">
|
<Tabs.TabPane tab="基本信息" key="info">
|
||||||
<UserGroupInfo active={activeKey === 'info'} id={id}/>
|
<UserGroupInfo active={activeKey === 'info'} id={id}/>
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
hasMenu('user-group-detail') &&
|
||||||
<Tabs.TabPane tab="授权的资产" key="asset">
|
<Tabs.TabPane tab="授权的资产" key="asset">
|
||||||
<UserAsset active={activeKey === 'asset'} id={id} type={'userGroupId'}/>
|
<UserAsset active={activeKey === 'asset'} id={id} type={'userGroupId'}/>
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
|
}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,8 +2,8 @@ import React, {useState} from 'react';
|
|||||||
|
|
||||||
import {Button, Input, Layout, message, Modal, Popconfirm, Switch, Table} from "antd";
|
import {Button, Input, Layout, message, Modal, Popconfirm, Switch, Table} from "antd";
|
||||||
import UserModal from "./UserModal";
|
import UserModal from "./UserModal";
|
||||||
import {Link} from "react-router-dom";
|
import {Link, useNavigate} from "react-router-dom";
|
||||||
import {ProTable} from "@ant-design/pro-components";
|
import {ProTable, TableDropdown} from "@ant-design/pro-components";
|
||||||
import userApi from "../../../api/user";
|
import userApi from "../../../api/user";
|
||||||
import arrays from "../../../utils/array";
|
import arrays from "../../../utils/array";
|
||||||
import {ExclamationCircleOutlined, LockTwoTone} from "@ant-design/icons";
|
import {ExclamationCircleOutlined, LockTwoTone} from "@ant-design/icons";
|
||||||
@ -25,6 +25,7 @@ const User = () => {
|
|||||||
let [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
let [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
||||||
|
|
||||||
const [columnsStateMap, setColumnsStateMap] = useColumnState(ColumnState.USER);
|
const [columnsStateMap, setColumnsStateMap] = useColumnState(ColumnState.USER);
|
||||||
|
let navigate = useNavigate();
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
@ -112,6 +113,27 @@ const User = () => {
|
|||||||
<a key='delete' className='danger'>删除</a>
|
<a key='delete' className='danger'>删除</a>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</Show>,
|
</Show>,
|
||||||
|
<TableDropdown
|
||||||
|
key="actionGroup"
|
||||||
|
onSelect={(key) => {
|
||||||
|
switch (key) {
|
||||||
|
case 'user-detail':
|
||||||
|
navigate(`/user/${record['id']}?activeKey=info`);
|
||||||
|
break;
|
||||||
|
case 'user-authorised-asset':
|
||||||
|
navigate(`/user/${record['id']}?activeKey=asset`);
|
||||||
|
break;
|
||||||
|
case 'user-login-policy':
|
||||||
|
navigate(`/user/${record['id']}?activeKey=login-policy`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
menus={[
|
||||||
|
{key: 'user-detail', name: '详情', disabled: !hasMenu('user-detail')},
|
||||||
|
{key: 'user-authorised-asset', name: '授权资产', disabled: !hasMenu('user-authorised-asset')},
|
||||||
|
{key: 'user-login-policy', name: '登录策略', disabled: !hasMenu('user-login-policy')},
|
||||||
|
]}
|
||||||
|
/>,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -4,6 +4,8 @@ import UserInfo from "./UserInfo";
|
|||||||
import UserLoginPolicy from "./UserLoginPolicy";
|
import UserLoginPolicy from "./UserLoginPolicy";
|
||||||
import UserAsset from "./UserAsset";
|
import UserAsset from "./UserAsset";
|
||||||
import {useParams, useSearchParams} from "react-router-dom";
|
import {useParams, useSearchParams} from "react-router-dom";
|
||||||
|
import {hasMenu} from "../../../service/permission";
|
||||||
|
import AssetUser from "../../asset/AssetUser";
|
||||||
|
|
||||||
const UserDetail = () => {
|
const UserDetail = () => {
|
||||||
|
|
||||||
@ -23,15 +25,24 @@ const UserDetail = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="page-detail-warp">
|
<div className="page-detail-warp">
|
||||||
<Tabs activeKey={activeKey} onChange={handleTagChange}>
|
<Tabs activeKey={activeKey} onChange={handleTagChange}>
|
||||||
|
{
|
||||||
|
hasMenu('user-detail') &&
|
||||||
<Tabs.TabPane tab="基本信息" key="info">
|
<Tabs.TabPane tab="基本信息" key="info">
|
||||||
<UserInfo active={activeKey === 'info'} userId={id}/>
|
<UserInfo active={activeKey === 'info'} userId={id}/>
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
hasMenu('user-authorised-asset') &&
|
||||||
<Tabs.TabPane tab="授权的资产" key="asset">
|
<Tabs.TabPane tab="授权的资产" key="asset">
|
||||||
<UserAsset active={activeKey === 'asset'} id={id} type={'userId'}/>
|
<UserAsset active={activeKey === 'asset'} id={id} type={'userId'}/>
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
hasMenu('user-login-policy') &&
|
||||||
<Tabs.TabPane tab="登录策略" key="login-policy">
|
<Tabs.TabPane tab="登录策略" key="login-policy">
|
||||||
<UserLoginPolicy active={activeKey === 'login-policy'} userId={id}/>
|
<UserLoginPolicy active={activeKey === 'login-policy'} userId={id}/>
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
|
}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -127,6 +127,13 @@ const MyAsset = () => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '最后接入时间',
|
||||||
|
key: 'lastAccessTime',
|
||||||
|
sorter: true,
|
||||||
|
dataIndex: 'lastAccessTime',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
valueType: 'option',
|
valueType: 'option',
|
||||||
@ -146,8 +153,8 @@ const MyAsset = () => {
|
|||||||
<a
|
<a
|
||||||
key="access"
|
key="access"
|
||||||
href={url}
|
href={url}
|
||||||
rel="noreferrer"
|
// rel="noreferrer"
|
||||||
target='_blank'
|
// target='_blank'
|
||||||
>
|
>
|
||||||
接入
|
接入
|
||||||
</a>,
|
</a>,
|
||||||
@ -194,7 +201,7 @@ const MyAsset = () => {
|
|||||||
labelWidth: 'auto',
|
labelWidth: 'auto',
|
||||||
}}
|
}}
|
||||||
pagination={{
|
pagination={{
|
||||||
defaultPageSize: 10,
|
pageSize: 10,
|
||||||
}}
|
}}
|
||||||
dateFormatter="string"
|
dateFormatter="string"
|
||||||
headerTitle="资产列表"
|
headerTitle="资产列表"
|
||||||
|
45
web/src/dd/prompt-modal/prompt-modal.js
Normal file
45
web/src/dd/prompt-modal/prompt-modal.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import React, {useEffect, useRef} from 'react';
|
||||||
|
import {Form, Input, Modal} from "antd";
|
||||||
|
|
||||||
|
const PromptModal = ({title, open, onOk, onCancel, placeholder}) => {
|
||||||
|
const ref = useRef(null);
|
||||||
|
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
// 解决 modal 异步弹出导致无法获取焦点的问题
|
||||||
|
setTimeout(() => {
|
||||||
|
ref.current.focus({
|
||||||
|
cursor: 'start',
|
||||||
|
});
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
form.resetFields();
|
||||||
|
}, [form, open]);
|
||||||
|
|
||||||
|
|
||||||
|
const handleOk = () => {
|
||||||
|
form
|
||||||
|
.validateFields()
|
||||||
|
.then(values => {
|
||||||
|
onOk(values['prompt'])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal title={title} open={open} onOk={handleOk} onCancel={onCancel}>
|
||||||
|
<Form form={form}>
|
||||||
|
<Form.Item name={'prompt'}>
|
||||||
|
<Input ref={ref}
|
||||||
|
onPressEnter={handleOk}
|
||||||
|
placeholder={placeholder}>
|
||||||
|
</Input>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PromptModal;
|
@ -1,6 +1,6 @@
|
|||||||
import React, {Suspense, useEffect, useState} from 'react';
|
import React, {Suspense, useEffect, useState} from 'react';
|
||||||
import {Breadcrumb, Dropdown, Layout, Menu, Popconfirm} from "antd";
|
import {Breadcrumb, Dropdown, Layout, Menu, Popconfirm} from "antd";
|
||||||
import {DesktopOutlined, DownOutlined, LogoutOutlined,} from "@ant-design/icons";
|
import {BugTwoTone, DesktopOutlined, DownOutlined, LogoutOutlined} from "@ant-design/icons";
|
||||||
import {Link, Outlet, useLocation, useNavigate} from "react-router-dom";
|
import {Link, Outlet, useLocation, useNavigate} from "react-router-dom";
|
||||||
import {getCurrentUser, isAdmin} from "../service/permission";
|
import {getCurrentUser, isAdmin} from "../service/permission";
|
||||||
import LogoWithName from "../images/logo-with-name.png";
|
import LogoWithName from "../images/logo-with-name.png";
|
||||||
@ -19,6 +19,7 @@ const breadcrumbMatchMap = {
|
|||||||
'/role/': '角色详情',
|
'/role/': '角色详情',
|
||||||
'/user-group/': '用户组详情',
|
'/user-group/': '用户组详情',
|
||||||
'/login-policy/': '登录策略详情',
|
'/login-policy/': '登录策略详情',
|
||||||
|
'/command-filter/': '命令过滤器详情',
|
||||||
'/strategy/': '授权策略详情',
|
'/strategy/': '授权策略详情',
|
||||||
};
|
};
|
||||||
const breadcrumbNameMap = {};
|
const breadcrumbNameMap = {};
|
||||||
@ -112,11 +113,15 @@ const ManagerLayout = () => {
|
|||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<Link to={'/my-asset'}><DesktopOutlined/> 我的资产</Link>
|
<Link to={'/my-asset'}><DesktopOutlined/> 我的资产</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<Link to={'/debug/pprof'}><BugTwoTone/> DEBUG</Link>
|
||||||
|
<a target='_blank' href={`/debug/pprof/`}></a>
|
||||||
|
</Menu.Item>
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
key='login-btn-pop'
|
key='login-btn-pop'
|
||||||
title="您确定要退出登录吗?"
|
title="您确定要退出登录吗?"
|
||||||
onConfirm={async ()=>{
|
onConfirm={async () => {
|
||||||
await accountApi.logout();
|
await accountApi.logout();
|
||||||
navigate('/login');
|
navigate('/login');
|
||||||
}}
|
}}
|
||||||
@ -189,7 +194,7 @@ const ManagerLayout = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
<Suspense fallback={<Landing/>}>
|
<Suspense fallback={<div className={'page-container'}><Landing/></div>}>
|
||||||
<Outlet/>
|
<Outlet/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user