Merge remote-tracking branch 'origin/dev'
# Conflicts: # web/src/components/command/DynamicCommand.js # web/src/components/command/DynamicCommandModal.js # web/src/components/credential/CredentialModal.js # web/src/components/devops/JobModal.js # web/src/components/devops/SecurityModal.js # web/src/components/user/User.js # web/src/components/user/UserGroup.js
This commit is contained in:
commit
21a68926af
@ -18,7 +18,7 @@ Next Terminal基于 [Apache Guacamole](https://guacamole.apache.org/) 开发,
|
||||
- 批量执行命令
|
||||
- 在线会话管理(监控、强制断开)
|
||||
- 离线会话管理(查看录屏)
|
||||
- 双因素认证 感谢 [naiba](https://github.com/naiba) 贡献
|
||||
- 双因素认证
|
||||
- 资产标签
|
||||
- 资产授权
|
||||
- 多用户&用户分组
|
||||
|
62
docs/faq.md
62
docs/faq.md
@ -18,7 +18,8 @@ location / {
|
||||
<details>
|
||||
<summary>访问realvnc提示验证失败?</summary>
|
||||
|
||||
把加密类型修改为 Prefer On
|
||||
1. 把密码类型修改为VNC
|
||||
2. 把加密类型修改为 Prefer On
|
||||
|
||||
</details>
|
||||
|
||||
@ -39,7 +40,7 @@ docker pull dushixiang/next-terminal:latest
|
||||
```shell
|
||||
docker rm <container-id> -f
|
||||
```
|
||||
再重新执行一次 [docker方式安装命令](install-naive.md)
|
||||
再重新执行一次 [docker方式安装命令](install-docker.md)
|
||||
|
||||
</details>
|
||||
|
||||
@ -88,3 +89,60 @@ Mar 5 20:00:16.923 [DEBU] 用户「admin」密码初始化为: next-terminal
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>TOTP客户端丢了怎么办?</summary>
|
||||
首先需要进入程序所在目录,使用docker安装的程序目录为:/usr/local/next-terminal
|
||||
|
||||
执行命令
|
||||
|
||||
```shell
|
||||
./next-terminal --reset-totp admin
|
||||
```
|
||||
|
||||
其中 admin 为用户登录账号,成功之后会输出
|
||||
|
||||
``` shell
|
||||
|
||||
_______ __ ___________ .__ .__
|
||||
\ \ ____ ___ ____/ |_ \__ ___/__________ _____ |__| ____ _____ | |
|
||||
/ | \_/ __ \\ \/ /\ __\ | |_/ __ \_ __ \/ \| |/ \\__ \ | |
|
||||
/ | \ ___/ > < | | | |\ ___/| | \/ Y Y \ | | \/ __ \| |__
|
||||
\____|__ /\___ >__/\_ \ |__| |____| \___ >__| |__|_| /__|___| (____ /____/
|
||||
\/ \/ \/ \/ \/ \/ \/ v0.4.0
|
||||
|
||||
当前数据库模式为:mysql
|
||||
Mar 5 20:00:16.923 [DEBU] 用户「admin」已重置TOTP
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>想要修改数据库敏感信息加密的key怎么办?</summary>
|
||||
首先需要进入程序所在目录,使用docker安装的程序目录为:/usr/local/next-terminal
|
||||
|
||||
执行命令
|
||||
|
||||
```shell
|
||||
./next-terminal --encryption-key 旧的加密key new-encryption-key 新的的加密key
|
||||
```
|
||||
|
||||
成功之后会输出
|
||||
|
||||
``` shell
|
||||
|
||||
_______ __ ___________ .__ .__
|
||||
\ \ ____ ___ ____/ |_ \__ ___/__________ _____ |__| ____ _____ | |
|
||||
/ | \_/ __ \\ \/ /\ __\ | |_/ __ \_ __ \/ \| |/ \\__ \ | |
|
||||
/ | \ ___/ > < | | | |\ ___/| | \/ Y Y \ | | \/ __ \| |__
|
||||
\____|__ /\___ >__/\_ \ |__| |____| \___ >__| |__|_| /__|___| (____ /____/
|
||||
\/ \/ \/ \/ \/ \/ \/ v0.4.0
|
||||
|
||||
当前数据库模式为:mysql
|
||||
Mar 5 20:00:16.923 [DEBU] encryption key has being changed.
|
||||
|
||||
```
|
||||
|
||||
最后重新启动程序,并且把加密key修改为新的。
|
||||
</details>
|
||||
|
||||
|
@ -104,6 +104,7 @@ docker run -d \
|
||||
| MYSQL_PASSWORD | `mysql`数据库密码 |
|
||||
| MYSQL_DATABASE | `mysql`数据库名称 |
|
||||
| SERVER_ADDR | 服务器监听地址,默认`0.0.0.0:8088` |
|
||||
| ENCRYPTION_KEY | 授权凭证和资产的密码,密钥等敏感信息加密的key,默认`next-terminal` |
|
||||
|
||||
## 其他
|
||||
|
||||
|
@ -119,10 +119,7 @@ mkfontdir
|
||||
fc-cache
|
||||
```
|
||||
### 安装 Next Terminal
|
||||
建立next-terminal目录
|
||||
```shell
|
||||
mkdir ~/next-terminal && cd ~/next-terminal
|
||||
```
|
||||
> 示例步骤安装在 `/usr/local/next-terminal`,你可以自由选择安装目录。
|
||||
|
||||
下载
|
||||
```shell
|
||||
@ -131,11 +128,10 @@ wget https://github.com/dushixiang/next-terminal/releases/latest/download/next-t
|
||||
|
||||
解压
|
||||
```shell
|
||||
tar -xvf next-terminal.tgz
|
||||
cd next-terminal
|
||||
tar -zxvf next-terminal.tgz -C /usr/local/
|
||||
```
|
||||
|
||||
在当前目录下创建或修改配置文件`config.yml`
|
||||
在`/usr/local/next-terminal`或`/etc/next-terminal`下创建或修改配置文件`config.yml`
|
||||
```shell
|
||||
db: sqlite
|
||||
# 当db为sqlite时mysql的配置无效
|
||||
@ -151,12 +147,41 @@ sqlite:
|
||||
file: 'next-terminal.db'
|
||||
server:
|
||||
addr: 0.0.0.0:8088
|
||||
# 当设置下面两个参数时会自动开启https模式
|
||||
# 当设置下面两个参数时会自动开启https模式(前提是证书文件存在)
|
||||
# cert: /root/next-terminal/cert.pem
|
||||
# key: /root/next-terminal/key.pem
|
||||
|
||||
# 授权凭证和资产的密码,密钥等敏感信息加密的key,默认`next-terminal`
|
||||
#encryption-key: next-terminal
|
||||
```
|
||||
|
||||
启动
|
||||
```shell
|
||||
./next-terminal
|
||||
```
|
||||
|
||||
使用系统服务方式启动
|
||||
|
||||
在 `/etc/systemd/system/` 目录创建 `next-terminal.service` 文件并写入以下内容
|
||||
```shell
|
||||
[Unit]
|
||||
Description=next-terminal service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=root
|
||||
WorkingDirectory=/usr/local/next-terminal
|
||||
ExecStart=/usr/local/next-terminal/next-terminal
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
重载系统服务&&设置开机启动&&启动服务&&查看状态
|
||||
```shell
|
||||
systemctl daemon-reload
|
||||
systemctl enable next-terminal
|
||||
systemctl start next-terminal
|
||||
systemctl status next-terminal
|
||||
```
|
19
main.go
19
main.go
@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
|
||||
"next-terminal/pkg/config"
|
||||
@ -12,7 +13,7 @@ import (
|
||||
"github.com/labstack/gommon/log"
|
||||
)
|
||||
|
||||
const Version = "v0.3.4"
|
||||
const Version = "v0.4.0"
|
||||
|
||||
func main() {
|
||||
err := Run()
|
||||
@ -34,13 +35,27 @@ func Run() error {
|
||||
// 为了兼容之前调用global包的代码 后期预期会改为调用pgk/config
|
||||
global.Config = config.GlobalCfg
|
||||
|
||||
if global.Config.EncryptionKey == "" {
|
||||
global.Config.EncryptionKey = "next-terminal"
|
||||
}
|
||||
md5Sum := fmt.Sprintf("%x", md5.Sum([]byte(global.Config.EncryptionKey)))
|
||||
global.Config.EncryptionPassword = []byte(md5Sum)
|
||||
|
||||
global.Cache = api.SetupCache()
|
||||
db := api.SetupDB()
|
||||
e := api.SetupRoutes(db)
|
||||
|
||||
if global.Config.ResetPassword != "" {
|
||||
return api.ResetPassword()
|
||||
return api.ResetPassword(global.Config.ResetPassword)
|
||||
}
|
||||
if global.Config.ResetTotp != "" {
|
||||
return api.ResetTotp(global.Config.ResetTotp)
|
||||
}
|
||||
|
||||
if global.Config.NewEncryptionKey != "" {
|
||||
return api.ChangeEncryptionKey(global.Config.EncryptionKey, global.Config.NewEncryptionKey)
|
||||
}
|
||||
|
||||
sessionRepo := repository.NewSessionRepository(db)
|
||||
propertyRepo := repository.NewPropertyRepository(db)
|
||||
ticker := task.NewTicker(sessionRepo, propertyRepo)
|
||||
|
@ -18,6 +18,10 @@ type Config struct {
|
||||
Mysql *Mysql
|
||||
Sqlite *Sqlite
|
||||
ResetPassword string
|
||||
ResetTotp string
|
||||
EncryptionKey string
|
||||
EncryptionPassword []byte
|
||||
NewEncryptionKey string
|
||||
}
|
||||
|
||||
type Mysql struct {
|
||||
@ -83,8 +87,11 @@ func SetupConfig() *Config {
|
||||
Key: viper.GetString("server.key"),
|
||||
},
|
||||
ResetPassword: viper.GetString("reset-password"),
|
||||
ResetTotp: viper.GetString("reset-totp"),
|
||||
Debug: viper.GetBool("debug"),
|
||||
Demo: viper.GetBool("demo"),
|
||||
EncryptionKey: viper.GetString("encryption-key"),
|
||||
NewEncryptionKey: viper.GetString("new-encryption-key"),
|
||||
}
|
||||
GlobalCfg = config
|
||||
return config
|
||||
|
34
pkg/service/asset.go
Normal file
34
pkg/service/asset.go
Normal file
@ -0,0 +1,34 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"next-terminal/pkg/global"
|
||||
"next-terminal/server/repository"
|
||||
)
|
||||
|
||||
type AssetService struct {
|
||||
assetRepository *repository.AssetRepository
|
||||
}
|
||||
|
||||
func NewAssetService(assetRepository *repository.AssetRepository) *AssetService {
|
||||
return &AssetService{assetRepository: assetRepository}
|
||||
}
|
||||
|
||||
func (r AssetService) Encrypt() error {
|
||||
items, err := r.assetRepository.FindAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range items {
|
||||
item := items[i]
|
||||
if item.Encrypted {
|
||||
continue
|
||||
}
|
||||
if err := r.assetRepository.Encrypt(&item, global.Config.EncryptionPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.assetRepository.UpdateById(&item, item.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
34
pkg/service/credential.go
Normal file
34
pkg/service/credential.go
Normal file
@ -0,0 +1,34 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"next-terminal/pkg/global"
|
||||
"next-terminal/server/repository"
|
||||
)
|
||||
|
||||
type CredentialService struct {
|
||||
credentialRepository *repository.CredentialRepository
|
||||
}
|
||||
|
||||
func NewCredentialService(credentialRepository *repository.CredentialRepository) *CredentialService {
|
||||
return &CredentialService{credentialRepository: credentialRepository}
|
||||
}
|
||||
|
||||
func (r CredentialService) Encrypt() error {
|
||||
items, err := r.credentialRepository.FindAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range items {
|
||||
item := items[i]
|
||||
if item.Encrypted {
|
||||
continue
|
||||
}
|
||||
if err := r.credentialRepository.Encrypt(&item, global.Config.EncryptionPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.credentialRepository.UpdateById(&item, item.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -160,7 +160,7 @@ func (r ShellJob) Run() {
|
||||
|
||||
msgChan := make(chan string)
|
||||
for i := range assets {
|
||||
asset, err := r.jobService.assetRepository.FindById(assets[i].ID)
|
||||
asset, err := r.jobService.assetRepository.FindByIdAndDecrypt(assets[i].ID)
|
||||
if err != nil {
|
||||
msgChan <- fmt.Sprintf("资产「%v」Shell执行失败,查询数据异常「%v」", assets[i].Name, err.Error())
|
||||
return
|
||||
@ -176,7 +176,7 @@ func (r ShellJob) Run() {
|
||||
)
|
||||
|
||||
if asset.AccountType == "credential" {
|
||||
credential, err := r.jobService.credentialRepository.FindById(asset.CredentialId)
|
||||
credential, err := r.jobService.credentialRepository.FindByIdAndDecrypt(asset.CredentialId)
|
||||
if err != nil {
|
||||
msgChan <- fmt.Sprintf("资产「%v」Shell执行失败,查询授权凭证数据异常「%v」", assets[i].Name, err.Error())
|
||||
return
|
||||
|
@ -33,3 +33,7 @@ func (r SessionService) FixSessionState() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r SessionService) EmptyPassword() error {
|
||||
return r.sessionRepository.EmptyPassword()
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ func (r UserService) InitUser() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r UserService) FixedUserOnlineState() error {
|
||||
func (r UserService) FixUserOnlineState() error {
|
||||
// 修正用户登录状态
|
||||
onlineUsers, err := r.userRepository.FindOnlineUsers()
|
||||
if err != nil {
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"next-terminal/pkg/constant"
|
||||
"next-terminal/pkg/global"
|
||||
"next-terminal/server/model"
|
||||
"next-terminal/server/utils"
|
||||
|
||||
@ -199,6 +200,9 @@ func AssetUpdateEndpoint(c echo.Context) error {
|
||||
item.Description = "-"
|
||||
}
|
||||
|
||||
if err := assetRepository.Encrypt(&item, global.Config.EncryptionPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := assetRepository.UpdateById(&item, id); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -264,7 +268,7 @@ func AssetGetEndpoint(c echo.Context) (err error) {
|
||||
}
|
||||
|
||||
var item model.Asset
|
||||
if item, err = assetRepository.FindById(id); err != nil {
|
||||
if item, err = assetRepository.FindByIdAndDecrypt(id); err != nil {
|
||||
return err
|
||||
}
|
||||
attributeMap, err := assetRepository.FindAssetAttrMapByAssetId(id)
|
||||
@ -289,9 +293,12 @@ func AssetTcpingEndpoint(c echo.Context) (err error) {
|
||||
|
||||
active := utils.Tcping(item.IP, item.Port)
|
||||
|
||||
if item.Active != active {
|
||||
if err := assetRepository.UpdateActiveById(active, item.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return Success(c, active)
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,13 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"next-terminal/pkg/constant"
|
||||
"next-terminal/pkg/global"
|
||||
"next-terminal/server/model"
|
||||
"next-terminal/server/utils"
|
||||
|
||||
@ -32,27 +34,28 @@ func CredentialCreateEndpoint(c echo.Context) error {
|
||||
case constant.Custom:
|
||||
item.PrivateKey = "-"
|
||||
item.Passphrase = "-"
|
||||
if len(item.Username) == 0 {
|
||||
if item.Username == "" {
|
||||
item.Username = "-"
|
||||
}
|
||||
if len(item.Password) == 0 {
|
||||
if item.Password == "" {
|
||||
item.Password = "-"
|
||||
}
|
||||
case constant.PrivateKey:
|
||||
item.Password = "-"
|
||||
if len(item.Username) == 0 {
|
||||
if item.Username == "" {
|
||||
item.Username = "-"
|
||||
}
|
||||
if len(item.PrivateKey) == 0 {
|
||||
if item.PrivateKey == "" {
|
||||
item.PrivateKey = "-"
|
||||
}
|
||||
if len(item.Passphrase) == 0 {
|
||||
if item.Passphrase == "" {
|
||||
item.Passphrase = "-"
|
||||
}
|
||||
default:
|
||||
return Fail(c, -1, "类型错误")
|
||||
}
|
||||
|
||||
item.Encrypted = true
|
||||
if err := credentialRepository.Create(&item); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -96,26 +99,48 @@ func CredentialUpdateEndpoint(c echo.Context) error {
|
||||
case constant.Custom:
|
||||
item.PrivateKey = "-"
|
||||
item.Passphrase = "-"
|
||||
if len(item.Username) == 0 {
|
||||
if item.Username == "" {
|
||||
item.Username = "-"
|
||||
}
|
||||
if len(item.Password) == 0 {
|
||||
if item.Password == "" {
|
||||
item.Password = "-"
|
||||
}
|
||||
if item.Password != "-" {
|
||||
encryptedCBC, err := utils.AesEncryptCBC([]byte(item.Password), global.Config.EncryptionPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item.Password = base64.StdEncoding.EncodeToString(encryptedCBC)
|
||||
}
|
||||
case constant.PrivateKey:
|
||||
item.Password = "-"
|
||||
if len(item.Username) == 0 {
|
||||
if item.Username == "" {
|
||||
item.Username = "-"
|
||||
}
|
||||
if len(item.PrivateKey) == 0 {
|
||||
if item.PrivateKey == "" {
|
||||
item.PrivateKey = "-"
|
||||
}
|
||||
if len(item.Passphrase) == 0 {
|
||||
if item.PrivateKey != "-" {
|
||||
encryptedCBC, err := utils.AesEncryptCBC([]byte(item.PrivateKey), global.Config.EncryptionPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item.PrivateKey = base64.StdEncoding.EncodeToString(encryptedCBC)
|
||||
}
|
||||
if item.Passphrase == "" {
|
||||
item.Passphrase = "-"
|
||||
}
|
||||
if item.Passphrase != "-" {
|
||||
encryptedCBC, err := utils.AesEncryptCBC([]byte(item.Passphrase), global.Config.EncryptionPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item.Passphrase = base64.StdEncoding.EncodeToString(encryptedCBC)
|
||||
}
|
||||
default:
|
||||
return Fail(c, -1, "类型错误")
|
||||
}
|
||||
item.Encrypted = true
|
||||
|
||||
if err := credentialRepository.UpdateById(&item, id); err != nil {
|
||||
return err
|
||||
@ -149,7 +174,7 @@ func CredentialGetEndpoint(c echo.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
item, err := credentialRepository.FindById(id)
|
||||
item, err := credentialRepository.FindByIdAndDecrypt(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -45,6 +47,8 @@ var (
|
||||
sessionService *service.SessionService
|
||||
mailService *service.MailService
|
||||
numService *service.NumService
|
||||
assetService *service.AssetService
|
||||
credentialService *service.CredentialService
|
||||
)
|
||||
|
||||
func SetupRoutes(db *gorm.DB) *echo.Echo {
|
||||
@ -54,6 +58,7 @@ func SetupRoutes(db *gorm.DB) *echo.Echo {
|
||||
|
||||
if err := InitDBData(); err != nil {
|
||||
log.WithError(err).Error("初始化数据异常")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if err := ReloadData(); err != nil {
|
||||
@ -251,6 +256,8 @@ func InitService() {
|
||||
sessionService = service.NewSessionService(sessionRepository)
|
||||
mailService = service.NewMailService(propertyRepository)
|
||||
numService = service.NewNumService(numRepository)
|
||||
assetService = service.NewAssetService(assetRepository)
|
||||
credentialService = service.NewCredentialService(credentialRepository)
|
||||
}
|
||||
|
||||
func InitDBData() (err error) {
|
||||
@ -266,17 +273,26 @@ func InitDBData() (err error) {
|
||||
if err := jobService.InitJob(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := userService.FixedUserOnlineState(); err != nil {
|
||||
if err := userService.FixUserOnlineState(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := sessionService.FixSessionState(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := sessionService.EmptyPassword(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := credentialService.Encrypt(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := assetService.Encrypt(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ResetPassword() error {
|
||||
user, err := userRepository.FindByUsername(global.Config.ResetPassword)
|
||||
func ResetPassword(username string) error {
|
||||
user, err := userRepository.FindByUsername(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -296,6 +312,63 @@ func ResetPassword() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ResetTotp(username string) error {
|
||||
user, err := userRepository.FindByUsername(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u := &model.User{
|
||||
TOTPSecret: "-",
|
||||
ID: user.ID,
|
||||
}
|
||||
if err := userRepository.Update(u); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debugf("用户「%v」已重置TOTP", user.Username)
|
||||
return nil
|
||||
}
|
||||
|
||||
func ChangeEncryptionKey(oldEncryptionKey, newEncryptionKey string) error {
|
||||
|
||||
oldPassword := []byte(fmt.Sprintf("%x", md5.Sum([]byte(oldEncryptionKey))))
|
||||
newPassword := []byte(fmt.Sprintf("%x", md5.Sum([]byte(newEncryptionKey))))
|
||||
|
||||
credentials, err := credentialRepository.FindAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range credentials {
|
||||
credential := credentials[i]
|
||||
if err := credentialRepository.Decrypt(&credential, oldPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := credentialRepository.Encrypt(&credential, newPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := credentialRepository.UpdateById(&credential, credential.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
assets, err := assetRepository.FindAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range assets {
|
||||
asset := assets[i]
|
||||
if err := assetRepository.Decrypt(&asset, oldPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := assetRepository.Encrypt(&asset, newPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := assetRepository.UpdateById(&asset, asset.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
log.Infof("encryption key has being changed.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetupCache() *cache.Cache {
|
||||
// 配置缓存器
|
||||
mCache := cache.New(5*time.Minute, 10*time.Minute)
|
||||
|
@ -137,6 +137,9 @@ func CloseSessionById(sessionId string, code int, reason string) {
|
||||
session.DisconnectedTime = utils.NowJsonTime()
|
||||
session.Code = code
|
||||
session.Message = reason
|
||||
session.Password = "-"
|
||||
session.PrivateKey = "-"
|
||||
session.Passphrase = "-"
|
||||
|
||||
_ = sessionRepository.UpdateById(&session, sessionId)
|
||||
}
|
||||
@ -359,7 +362,7 @@ type File struct {
|
||||
|
||||
func SessionLsEndpoint(c echo.Context) error {
|
||||
sessionId := c.Param("id")
|
||||
session, err := sessionRepository.FindById(sessionId)
|
||||
session, err := sessionRepository.FindByIdAndDecrypt(sessionId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ func SSHEndpoint(c echo.Context) (err error) {
|
||||
cols, _ := strconv.Atoi(c.QueryParam("cols"))
|
||||
rows, _ := strconv.Atoi(c.QueryParam("rows"))
|
||||
|
||||
session, err := sessionRepository.FindById(sessionId)
|
||||
session, err := sessionRepository.FindByIdAndDecrypt(sessionId)
|
||||
if err != nil {
|
||||
msg := Message{
|
||||
Type: Closed,
|
||||
|
@ -65,7 +65,7 @@ func TunEndpoint(c echo.Context) error {
|
||||
configuration.SetParameter("width", width)
|
||||
configuration.SetParameter("height", height)
|
||||
configuration.SetParameter("dpi", dpi)
|
||||
session, err = sessionRepository.FindById(sessionId)
|
||||
session, err = sessionRepository.FindByIdAndDecrypt(sessionId)
|
||||
if err != nil {
|
||||
CloseSessionById(sessionId, NotFoundSession, "会话不存在")
|
||||
return err
|
||||
|
@ -21,6 +21,7 @@ type Asset struct {
|
||||
Created utils.JsonTime `json:"created"`
|
||||
Tags string `json:"tags"`
|
||||
Owner string `gorm:"index" json:"owner"`
|
||||
Encrypted bool `json:"encrypted"`
|
||||
}
|
||||
|
||||
type AssetForPage struct {
|
||||
|
@ -14,6 +14,7 @@ type Credential struct {
|
||||
Passphrase string `json:"passphrase"`
|
||||
Created utils.JsonTime `json:"created"`
|
||||
Owner string `gorm:"index" json:"owner"`
|
||||
Encrypted bool `json:"encrypted"`
|
||||
}
|
||||
|
||||
func (r *Credential) TableName() string {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@ -145,7 +146,36 @@ func (r AssetRepository) Find(pageIndex, pageSize int, name, protocol, tags stri
|
||||
return
|
||||
}
|
||||
|
||||
func (r AssetRepository) Encrypt(item *model.Asset, password []byte) error {
|
||||
if item.Password != "" && item.Password != "-" {
|
||||
encryptedCBC, err := utils.AesEncryptCBC([]byte(item.Password), password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item.Password = base64.StdEncoding.EncodeToString(encryptedCBC)
|
||||
}
|
||||
if item.PrivateKey != "" && item.PrivateKey != "-" {
|
||||
encryptedCBC, err := utils.AesEncryptCBC([]byte(item.PrivateKey), password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item.PrivateKey = base64.StdEncoding.EncodeToString(encryptedCBC)
|
||||
}
|
||||
if item.Passphrase != "" && item.Passphrase != "-" {
|
||||
encryptedCBC, err := utils.AesEncryptCBC([]byte(item.Passphrase), password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item.Passphrase = base64.StdEncoding.EncodeToString(encryptedCBC)
|
||||
}
|
||||
item.Encrypted = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r AssetRepository) Create(o *model.Asset) (err error) {
|
||||
if err := r.Encrypt(o, global.Config.EncryptionPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = r.DB.Create(o).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@ -157,8 +187,54 @@ func (r AssetRepository) FindById(id string) (o model.Asset, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (r AssetRepository) Decrypt(item *model.Asset, password []byte) error {
|
||||
if item.Encrypted {
|
||||
if item.Password != "" && item.Password != "-" {
|
||||
origData, err := base64.StdEncoding.DecodeString(item.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decryptedCBC, err := utils.AesDecryptCBC(origData, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item.Password = string(decryptedCBC)
|
||||
}
|
||||
if item.PrivateKey != "" && item.PrivateKey != "-" {
|
||||
origData, err := base64.StdEncoding.DecodeString(item.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decryptedCBC, err := utils.AesDecryptCBC(origData, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item.PrivateKey = string(decryptedCBC)
|
||||
}
|
||||
if item.Passphrase != "" && item.Passphrase != "-" {
|
||||
origData, err := base64.StdEncoding.DecodeString(item.Passphrase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decryptedCBC, err := utils.AesDecryptCBC(origData, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item.Passphrase = string(decryptedCBC)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r AssetRepository) FindByIdAndDecrypt(id string) (o model.Asset, err error) {
|
||||
err = r.DB.Where("id = ?", id).First(&o).Error
|
||||
if err == nil {
|
||||
err = r.Decrypt(&o, global.Config.EncryptionPassword)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r AssetRepository) UpdateById(o *model.Asset, id string) error {
|
||||
o.ID = id
|
||||
return r.DB.Updates(o).Error
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,12 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
|
||||
"next-terminal/pkg/constant"
|
||||
"next-terminal/pkg/global"
|
||||
"next-terminal/server/model"
|
||||
"next-terminal/server/utils"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@ -65,6 +69,9 @@ func (r CredentialRepository) Find(pageIndex, pageSize int, name, order, field s
|
||||
}
|
||||
|
||||
func (r CredentialRepository) Create(o *model.Credential) (err error) {
|
||||
if err := r.Encrypt(o, global.Config.EncryptionPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = r.DB.Create(o).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@ -76,6 +83,79 @@ func (r CredentialRepository) FindById(id string) (o model.Credential, err error
|
||||
return
|
||||
}
|
||||
|
||||
func (r CredentialRepository) Encrypt(item *model.Credential, password []byte) error {
|
||||
if item.Password != "-" {
|
||||
encryptedCBC, err := utils.AesEncryptCBC([]byte(item.Password), password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item.Password = base64.StdEncoding.EncodeToString(encryptedCBC)
|
||||
}
|
||||
if item.PrivateKey != "-" {
|
||||
encryptedCBC, err := utils.AesEncryptCBC([]byte(item.PrivateKey), password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item.PrivateKey = base64.StdEncoding.EncodeToString(encryptedCBC)
|
||||
}
|
||||
if item.Passphrase != "-" {
|
||||
encryptedCBC, err := utils.AesEncryptCBC([]byte(item.Passphrase), password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item.Passphrase = base64.StdEncoding.EncodeToString(encryptedCBC)
|
||||
}
|
||||
item.Encrypted = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r CredentialRepository) Decrypt(item *model.Credential, password []byte) error {
|
||||
if item.Encrypted {
|
||||
if item.Password != "" && item.Password != "-" {
|
||||
origData, err := base64.StdEncoding.DecodeString(item.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decryptedCBC, err := utils.AesDecryptCBC(origData, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item.Password = string(decryptedCBC)
|
||||
}
|
||||
if item.PrivateKey != "" && item.PrivateKey != "-" {
|
||||
origData, err := base64.StdEncoding.DecodeString(item.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decryptedCBC, err := utils.AesDecryptCBC(origData, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item.PrivateKey = string(decryptedCBC)
|
||||
}
|
||||
if item.Passphrase != "" && item.Passphrase != "-" {
|
||||
origData, err := base64.StdEncoding.DecodeString(item.Passphrase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decryptedCBC, err := utils.AesDecryptCBC(origData, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item.Passphrase = string(decryptedCBC)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r CredentialRepository) FindByIdAndDecrypt(id string) (o model.Credential, err error) {
|
||||
err = r.DB.Where("id = ?", id).First(&o).Error
|
||||
if err == nil {
|
||||
err = r.Decrypt(&o, global.Config.EncryptionPassword)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r CredentialRepository) UpdateById(o *model.Credential, id string) error {
|
||||
o.ID = id
|
||||
return r.DB.Updates(o).Error
|
||||
@ -107,3 +187,8 @@ func (r CredentialRepository) CountByUserId(userId string) (total int64, err err
|
||||
err = db.Find(&model.Credential{}).Count(&total).Error
|
||||
return
|
||||
}
|
||||
|
||||
func (r CredentialRepository) FindAll() (o []model.Credential, err error) {
|
||||
err = r.DB.Find(&o).Error
|
||||
return
|
||||
}
|
||||
|
@ -1,12 +1,15 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"next-terminal/pkg/constant"
|
||||
"next-terminal/pkg/global"
|
||||
"next-terminal/server/model"
|
||||
"next-terminal/server/utils"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@ -93,6 +96,51 @@ func (r SessionRepository) FindById(id string) (o model.Session, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (r SessionRepository) FindByIdAndDecrypt(id string) (o model.Session, err error) {
|
||||
err = r.DB.Where("id = ?", id).First(&o).Error
|
||||
if err == nil {
|
||||
err = r.Decrypt(&o)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r SessionRepository) Decrypt(item *model.Session) error {
|
||||
if item.Password != "" && item.Password != "-" {
|
||||
origData, err := base64.StdEncoding.DecodeString(item.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decryptedCBC, err := utils.AesDecryptCBC(origData, global.Config.EncryptionPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item.Password = string(decryptedCBC)
|
||||
}
|
||||
if item.PrivateKey != "" && item.PrivateKey != "-" {
|
||||
origData, err := base64.StdEncoding.DecodeString(item.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decryptedCBC, err := utils.AesDecryptCBC(origData, global.Config.EncryptionPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item.PrivateKey = string(decryptedCBC)
|
||||
}
|
||||
if item.Passphrase != "" && item.Passphrase != "-" {
|
||||
origData, err := base64.StdEncoding.DecodeString(item.Passphrase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decryptedCBC, err := utils.AesDecryptCBC(origData, global.Config.EncryptionPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item.Passphrase = string(decryptedCBC)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r SessionRepository) FindByConnectionId(connectionId string) (o model.Session, err error) {
|
||||
err = r.DB.Where("connection_id = ?", connectionId).First(&o).Error
|
||||
return
|
||||
@ -167,3 +215,8 @@ func (r SessionRepository) CountSessionByDay(day int) (results []D, err error) {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r SessionRepository) EmptyPassword() error {
|
||||
sql := "update sessions set password = '-',private_key = '-', passphrase = '-' where 1=1"
|
||||
return r.DB.Exec(sql).Error
|
||||
}
|
||||
|
@ -1,6 +1,10 @@
|
||||
package utils_test
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
@ -33,3 +37,43 @@ func TestTcping(t *testing.T) {
|
||||
_ = conn.Close()
|
||||
}()
|
||||
}
|
||||
|
||||
func TestAesEncryptCBC(t *testing.T) {
|
||||
origData := []byte("Hello Next Terminal") // 待加密的数据
|
||||
key := []byte("qwertyuiopasdfgh") // 加密的密钥
|
||||
encryptedCBC, err := utils.AesEncryptCBC(origData, key)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "s2xvMRPfZjmttpt+x0MzG9dsWcf1X+h9nt7waLvXpNM=", base64.StdEncoding.EncodeToString(encryptedCBC))
|
||||
}
|
||||
|
||||
func TestAesDecryptCBC(t *testing.T) {
|
||||
origData, err := base64.StdEncoding.DecodeString("s2xvMRPfZjmttpt+x0MzG9dsWcf1X+h9nt7waLvXpNM=") // 待解密的数据
|
||||
assert.NoError(t, err)
|
||||
key := []byte("qwertyuiopasdfgh") // 解密的密钥
|
||||
decryptCBC, err := utils.AesDecryptCBC(origData, key)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Hello Next Terminal", string(decryptCBC))
|
||||
}
|
||||
|
||||
func TestPbkdf2(t *testing.T) {
|
||||
pbkdf2, err := utils.Pbkdf2("1234")
|
||||
assert.NoError(t, err)
|
||||
println(hex.EncodeToString(pbkdf2))
|
||||
}
|
||||
|
||||
func TestAesEncryptCBCWithAnyKey(t *testing.T) {
|
||||
origData := []byte("admin") // 待加密的数据
|
||||
key := []byte(fmt.Sprintf("%x", md5.Sum([]byte("next-terminal")))) // 加密的密钥
|
||||
encryptedCBC, err := utils.AesEncryptCBC(origData, key)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "3qwawlPxghyiLS5hdr/p0g==", base64.StdEncoding.EncodeToString(encryptedCBC))
|
||||
}
|
||||
|
||||
func TestAesDecryptCBCWithAnyKey(t *testing.T) {
|
||||
origData, err := base64.StdEncoding.DecodeString("3qwawlPxghyiLS5hdr/p0g==") // 待解密的数据
|
||||
assert.NoError(t, err)
|
||||
key := []byte(fmt.Sprintf("%x", md5.Sum([]byte("next-terminal")))) // 加密的密钥
|
||||
decryptCBC, err := utils.AesDecryptCBC(origData, key)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "admin", string(decryptCBC))
|
||||
}
|
||||
|
@ -2,7 +2,11 @@ package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"database/sql/driver"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
@ -17,6 +21,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@ -224,3 +230,56 @@ func Check(f func() error) {
|
||||
logrus.Error("Received error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(ciphertext)%blockSize
|
||||
padText := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(ciphertext, padText...)
|
||||
}
|
||||
|
||||
func PKCS5UnPadding(origData []byte) []byte {
|
||||
length := len(origData)
|
||||
unPadding := int(origData[length-1])
|
||||
return origData[:(length - unPadding)]
|
||||
}
|
||||
|
||||
// AesEncryptCBC /*
|
||||
func AesEncryptCBC(origData, key []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blockSize := block.BlockSize()
|
||||
origData = PKCS5Padding(origData, blockSize)
|
||||
blockMode := cipher.NewCBCEncrypter(block, key[:blockSize])
|
||||
encrypted := make([]byte, len(origData))
|
||||
blockMode.CryptBlocks(encrypted, origData)
|
||||
return encrypted, nil
|
||||
}
|
||||
|
||||
func AesDecryptCBC(encrypted, key []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blockSize := block.BlockSize()
|
||||
blockMode := cipher.NewCBCDecrypter(block, key[:blockSize])
|
||||
origData := make([]byte, len(encrypted))
|
||||
blockMode.CryptBlocks(origData, encrypted)
|
||||
origData = PKCS5UnPadding(origData)
|
||||
return origData, nil
|
||||
}
|
||||
|
||||
func Pbkdf2(password string) ([]byte, error) {
|
||||
//生成随机盐
|
||||
salt := make([]byte, 32)
|
||||
_, err := rand.Read(salt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//生成密文
|
||||
dk := pbkdf2.Key([]byte(password), salt, 1, 32, sha256.New)
|
||||
return dk, nil
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "next-terminal",
|
||||
"version": "0.3.4",
|
||||
"version": "0.4.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.3.0",
|
||||
|
@ -132,7 +132,7 @@ class LoginForm extends Component {
|
||||
|
||||
<Modal title="双因素认证" visible={this.state.totpModalVisible} confirmLoading={this.state.confirmLoading}
|
||||
maskClosable={false}
|
||||
centered={true}
|
||||
|
||||
okButtonProps={{form:'totp-form', key: 'submit', htmlType: 'submit'}}
|
||||
onOk={() => {
|
||||
this.formRef.current
|
||||
|
@ -20,7 +20,8 @@ class Term extends Component {
|
||||
term: undefined,
|
||||
webSocket: undefined,
|
||||
fitAddon: undefined,
|
||||
sessionId: undefined
|
||||
sessionId: undefined,
|
||||
enterBtnIndex: 1001
|
||||
};
|
||||
|
||||
componentDidMount = async () => {
|
||||
@ -215,17 +216,17 @@ class Term extends Component {
|
||||
}}/>
|
||||
|
||||
<Draggable>
|
||||
<Affix style={{position: 'absolute', top: 50, right: 50, zIndex: 9999}}>
|
||||
<Affix style={{position: 'absolute', top: 50, right: 50, zIndex: this.state.enterBtnIndex}}>
|
||||
<Button icon={<AppstoreTwoTone/>} onClick={() => {
|
||||
this.setState({
|
||||
fileSystemVisible: true,
|
||||
enterBtnIndex: 999, // xterm.js 输入框的zIndex是1000,在弹出文件管理页面后要隐藏此按钮
|
||||
});
|
||||
}}/>
|
||||
</Affix>
|
||||
</Draggable>
|
||||
|
||||
<Drawer
|
||||
style={{zIndex: 10000}}
|
||||
title={'会话详情'}
|
||||
placement="right"
|
||||
width={window.innerWidth * 0.8}
|
||||
@ -233,7 +234,8 @@ class Term extends Component {
|
||||
// maskClosable={false}
|
||||
onClose={() => {
|
||||
this.setState({
|
||||
fileSystemVisible: false
|
||||
fileSystemVisible: false,
|
||||
enterBtnIndex: 1001, // xterm.js 输入框的zIndex是1000,在隐藏文件管理页面后要显示此按钮
|
||||
});
|
||||
}}
|
||||
visible={this.state.fileSystemVisible}
|
||||
|
@ -355,10 +355,10 @@ class Asset extends Component {
|
||||
window.open(`#/access?assetId=${id}&assetName=${name}&protocol=${protocol}`);
|
||||
}
|
||||
} else {
|
||||
message.warn('您访问的资产未在线,请确认网络状态。', 10);
|
||||
message.warn({content: '您访问的资产未在线,请确认网络状态。', key: id, duration: 10});
|
||||
}
|
||||
} else {
|
||||
message.error('操作失败 :( ' + result.message, 10);
|
||||
message.error({content: result.message, key: id, duration: 10});
|
||||
}
|
||||
|
||||
}
|
||||
|
427
web/src/components/command/ChooseAsset.js
Normal file
427
web/src/components/command/ChooseAsset.js
Normal file
@ -0,0 +1,427 @@
|
||||
import React, {Component} from 'react';
|
||||
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Col,
|
||||
Divider,
|
||||
Input,
|
||||
Layout,
|
||||
Modal,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from "antd";
|
||||
import qs from "qs";
|
||||
import request from "../../common/request";
|
||||
import {message} from "antd/es";
|
||||
|
||||
|
||||
import {PlusOutlined, SyncOutlined, UndoOutlined} from '@ant-design/icons';
|
||||
import {PROTOCOL_COLORS} from "../../common/constants";
|
||||
import {isEmpty} from "../../utils/utils";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const confirm = Modal.confirm;
|
||||
const {Search} = Input;
|
||||
const {Content} = Layout;
|
||||
const {Title, Text} = Typography;
|
||||
|
||||
class ChooseAsset extends Component {
|
||||
|
||||
inputRefOfName = React.createRef();
|
||||
inputRefOfIp = React.createRef();
|
||||
changeOwnerFormRef = React.createRef();
|
||||
|
||||
state = {
|
||||
items: [],
|
||||
total: 0,
|
||||
queryParams: {
|
||||
pageIndex: 1,
|
||||
pageSize: 10,
|
||||
protocol: 'ssh'
|
||||
},
|
||||
loading: false,
|
||||
tags: [],
|
||||
model: {},
|
||||
selectedRowKeys: [],
|
||||
selectedRows: [],
|
||||
delBtnLoading: false,
|
||||
changeOwnerModalVisible: false,
|
||||
changeSharerModalVisible: false,
|
||||
changeOwnerConfirmLoading: false,
|
||||
changeSharerConfirmLoading: false,
|
||||
users: [],
|
||||
selected: {},
|
||||
totalSelectedRows: [],
|
||||
};
|
||||
|
||||
checkedAssets = undefined
|
||||
|
||||
async componentDidMount() {
|
||||
this.checkedAssets = this.props.setCheckedAssets;
|
||||
this.loadTableData();
|
||||
let result = await request.get('/tags');
|
||||
if (result['code'] === 1) {
|
||||
this.setState({
|
||||
tags: result['data']
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async loadTableData(queryParams) {
|
||||
this.setState({
|
||||
loading: true
|
||||
});
|
||||
|
||||
queryParams = queryParams || this.state.queryParams;
|
||||
|
||||
// queryParams
|
||||
let paramsStr = qs.stringify(queryParams);
|
||||
|
||||
let data = {
|
||||
items: [],
|
||||
total: 0
|
||||
};
|
||||
|
||||
try {
|
||||
let result = await request.get('/assets/paging?' + paramsStr);
|
||||
if (result['code'] === 1) {
|
||||
data = result['data'];
|
||||
} else {
|
||||
message.error(result['message']);
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
} finally {
|
||||
let sharer = this.state.sharer;
|
||||
const items = data.items.map(item => {
|
||||
let disabled = false;
|
||||
if (sharer === item['owner']) {
|
||||
disabled = true;
|
||||
}
|
||||
return {...item, 'key': item['id'], 'disabled': disabled}
|
||||
})
|
||||
let totalSelectedRows = this.state.totalSelectedRows;
|
||||
let selectedRowKeys = totalSelectedRows.map(item => item['id']);
|
||||
this.setState({
|
||||
items: items,
|
||||
total: data.total,
|
||||
queryParams: queryParams,
|
||||
loading: false,
|
||||
selectedRowKeys: selectedRowKeys
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleChangPage = async (pageIndex, pageSize) => {
|
||||
let queryParams = this.state.queryParams;
|
||||
queryParams.pageIndex = pageIndex;
|
||||
queryParams.pageSize = pageSize;
|
||||
|
||||
this.setState({
|
||||
queryParams: queryParams
|
||||
});
|
||||
|
||||
await this.loadTableData(queryParams)
|
||||
};
|
||||
|
||||
handleSearchByName = name => {
|
||||
let query = {
|
||||
...this.state.queryParams,
|
||||
'pageIndex': 1,
|
||||
'pageSize': this.state.queryParams.pageSize,
|
||||
'name': name,
|
||||
}
|
||||
|
||||
this.loadTableData(query);
|
||||
};
|
||||
|
||||
handleSearchByIp = ip => {
|
||||
let query = {
|
||||
...this.state.queryParams,
|
||||
'pageIndex': 1,
|
||||
'pageSize': this.state.queryParams.pageSize,
|
||||
'ip': ip,
|
||||
}
|
||||
|
||||
this.loadTableData(query);
|
||||
};
|
||||
|
||||
handleTagsChange = tags => {
|
||||
console.log(tags)
|
||||
// this.setState({
|
||||
// tags: tags
|
||||
// })
|
||||
let query = {
|
||||
...this.state.queryParams,
|
||||
'pageIndex': 1,
|
||||
'pageSize': this.state.queryParams.pageSize,
|
||||
'tags': tags.join(','),
|
||||
}
|
||||
|
||||
this.loadTableData(query);
|
||||
}
|
||||
|
||||
unSelectRow = async (assetId) => {
|
||||
const selectedRowKeys = this.state.selectedRowKeys.filter(key => key !== assetId);
|
||||
const totalSelectedRows = this.state.totalSelectedRows.filter(item => item['id'] !== assetId);
|
||||
this.setState({
|
||||
selectedRowKeys: selectedRowKeys,
|
||||
totalSelectedRows: totalSelectedRows
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const columns = [{
|
||||
title: '序号',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
render: (id, record, index) => {
|
||||
return index + 1;
|
||||
}
|
||||
}, {
|
||||
title: '资产名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (name, record) => {
|
||||
let short = name;
|
||||
if (short && short.length > 20) {
|
||||
short = short.substring(0, 20) + " ...";
|
||||
}
|
||||
return (
|
||||
<Tooltip placement="topLeft" title={name}>
|
||||
{short}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}, {
|
||||
title: '连接协议',
|
||||
dataIndex: 'protocol',
|
||||
key: 'protocol',
|
||||
render: (text, record) => {
|
||||
const title = `${record['ip'] + ':' + record['port']}`
|
||||
return (
|
||||
<Tooltip title={title}>
|
||||
<Tag color={PROTOCOL_COLORS[text]}>{text}</Tag>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
}, {
|
||||
title: '标签',
|
||||
dataIndex: 'tags',
|
||||
key: 'tags',
|
||||
render: tags => {
|
||||
if (!isEmpty(tags)) {
|
||||
let tagDocuments = []
|
||||
let tagArr = tags.split(',');
|
||||
for (let i = 0; i < tagArr.length; i++) {
|
||||
if (tags[i] === '-') {
|
||||
continue;
|
||||
}
|
||||
tagDocuments.push(<Tag>{tagArr[i]}</Tag>)
|
||||
}
|
||||
return tagDocuments;
|
||||
}
|
||||
}
|
||||
}, {
|
||||
title: '状态',
|
||||
dataIndex: 'active',
|
||||
key: 'active',
|
||||
render: text => {
|
||||
|
||||
if (text) {
|
||||
return (
|
||||
<Tooltip title='运行中'>
|
||||
<Badge status="processing"/>
|
||||
</Tooltip>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Tooltip title='不可用'>
|
||||
<Badge status="error"/>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
}
|
||||
}, {
|
||||
title: '所有者',
|
||||
dataIndex: 'ownerName',
|
||||
key: 'ownerName'
|
||||
}, {
|
||||
title: '创建日期',
|
||||
dataIndex: 'created',
|
||||
key: 'created',
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Tooltip title={text}>
|
||||
{dayjs(text).fromNow()}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
const selectedRowKeys = this.state.selectedRowKeys;
|
||||
const rowSelection = {
|
||||
selectedRowKeys: this.state.selectedRowKeys,
|
||||
onChange: (selectedRowKeys, selectedRows) => {
|
||||
this.setState({selectedRowKeys, selectedRows});
|
||||
},
|
||||
getCheckboxProps: (record) => ({
|
||||
disabled: record['disabled'],
|
||||
}),
|
||||
};
|
||||
let hasSelected = false;
|
||||
if (selectedRowKeys.length > 0) {
|
||||
let totalSelectedRows = this.state.totalSelectedRows;
|
||||
let allSelectedRowKeys = totalSelectedRows.map(item => item['id']);
|
||||
for (let i = 0; i < selectedRowKeys.length; i++) {
|
||||
let selectedRowKey = selectedRowKeys[i];
|
||||
if (!allSelectedRowKeys.includes(selectedRowKey)) {
|
||||
hasSelected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title level={3}>已选择资产列表</Title>
|
||||
<div>
|
||||
{
|
||||
this.state.totalSelectedRows.map(item => {
|
||||
return <Tag color={PROTOCOL_COLORS[item['protocol']]} closable
|
||||
onClose={() => this.unSelectRow(item['id'])}
|
||||
key={item['id']}>{item['name']}</Tag>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
<Divider/>
|
||||
<Content key='page-content' className="site-layout-background">
|
||||
<div style={{marginBottom: 20}}>
|
||||
<Row justify="space-around" align="middle" gutter={24}>
|
||||
<Col span={4} key={1}>
|
||||
<Title level={3}>全部资产列表</Title>
|
||||
</Col>
|
||||
<Col span={20} key={2} style={{textAlign: 'right'}}>
|
||||
<Space>
|
||||
|
||||
<Search
|
||||
ref={this.inputRefOfName}
|
||||
placeholder="资产名称"
|
||||
allowClear
|
||||
onSearch={this.handleSearchByName}
|
||||
style={{width: 200}}
|
||||
/>
|
||||
|
||||
<Search
|
||||
ref={this.inputRefOfIp}
|
||||
placeholder="资产IP"
|
||||
allowClear
|
||||
onSearch={this.handleSearchByIp}
|
||||
style={{width: 200}}
|
||||
/>
|
||||
|
||||
<Select mode="multiple"
|
||||
allowClear
|
||||
placeholder="资产标签" onChange={this.handleTagsChange}
|
||||
style={{minWidth: 150}}>
|
||||
{this.state.tags.map(tag => {
|
||||
if (tag === '-') {
|
||||
return undefined;
|
||||
}
|
||||
return (<Select.Option key={tag}>{tag}</Select.Option>)
|
||||
})}
|
||||
</Select>
|
||||
|
||||
<Tooltip title='重置查询'>
|
||||
|
||||
<Button icon={<UndoOutlined/>} onClick={() => {
|
||||
this.inputRefOfName.current.setValue('');
|
||||
this.inputRefOfIp.current.setValue('');
|
||||
this.loadTableData({
|
||||
...this.state.queryParams,
|
||||
pageIndex: 1,
|
||||
pageSize: 10,
|
||||
protocol: 'ssh',
|
||||
name: '',
|
||||
ip: ''
|
||||
})
|
||||
}}>
|
||||
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Divider type="vertical"/>
|
||||
|
||||
<Tooltip title="刷新列表">
|
||||
<Button icon={<SyncOutlined/>} onClick={() => {
|
||||
this.loadTableData(this.state.queryParams)
|
||||
}}>
|
||||
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="添加选择">
|
||||
<Button type="primary" disabled={!hasSelected} icon={<PlusOutlined/>}
|
||||
onClick={async () => {
|
||||
console.log(this.state.selectedRows)
|
||||
let totalSelectedRows = this.state.totalSelectedRows;
|
||||
let totalSelectedRowKeys = totalSelectedRows.map(item => item['id']);
|
||||
|
||||
let selectedRows = this.state.selectedRows;
|
||||
let newRowKeys = []
|
||||
for (let i = 0; i < selectedRows.length; i++) {
|
||||
let selectedRow = selectedRows[i];
|
||||
if (totalSelectedRowKeys.includes(selectedRow['id'])) {
|
||||
continue;
|
||||
}
|
||||
totalSelectedRows.push(selectedRow);
|
||||
newRowKeys.push(selectedRow['id']);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
totalSelectedRows: totalSelectedRows
|
||||
})
|
||||
if (this.checkedAssets) {
|
||||
this.checkedAssets(totalSelectedRows);
|
||||
}
|
||||
}}>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<Table key='assets-table'
|
||||
rowSelection={rowSelection}
|
||||
dataSource={this.state.items}
|
||||
columns={columns}
|
||||
position={'both'}
|
||||
size="middle"
|
||||
pagination={{
|
||||
showSizeChanger: true,
|
||||
current: this.state.queryParams.pageIndex,
|
||||
pageSize: this.state.queryParams.pageSize,
|
||||
onChange: this.handleChangPage,
|
||||
onShowSizeChange: this.handleChangPage,
|
||||
total: this.state.total,
|
||||
showTotal: total => `总计 ${total} 条`
|
||||
}}
|
||||
loading={this.state.loading}
|
||||
/>
|
||||
</Content>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ChooseAsset;
|
@ -32,10 +32,10 @@ import {
|
||||
SyncOutlined,
|
||||
UndoOutlined
|
||||
} from '@ant-design/icons';
|
||||
import {compare} from "../../utils/utils";
|
||||
|
||||
import {hasPermission, isAdmin} from "../../service/permission";
|
||||
import dayjs from "dayjs";
|
||||
import ChooseAsset from "./ChooseAsset";
|
||||
|
||||
const confirm = Modal.confirm;
|
||||
const {Content} = Layout;
|
||||
@ -191,6 +191,12 @@ class DynamicCommand extends Component {
|
||||
});
|
||||
};
|
||||
|
||||
setCheckedAssets = (checkedAssets) => {
|
||||
this.setState({
|
||||
checkedAssets: checkedAssets
|
||||
})
|
||||
}
|
||||
|
||||
executeCommand = e => {
|
||||
let checkedAssets = this.state.checkedAssets;
|
||||
if (checkedAssets.length === 0) {
|
||||
@ -198,18 +204,10 @@ class DynamicCommand extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
let assets = this.state.assets;
|
||||
let cAssets = checkedAssets.map(item => {
|
||||
let name = '';
|
||||
for (let i = 0; i < assets.length; i++) {
|
||||
if (assets[i]['id'] === item) {
|
||||
name = assets[i]['name'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: item,
|
||||
name: name
|
||||
id: item['id'],
|
||||
name: item['name']
|
||||
}
|
||||
});
|
||||
|
||||
@ -474,17 +472,6 @@ class DynamicCommand extends Component {
|
||||
assetsVisible: true,
|
||||
commandId: record['id']
|
||||
});
|
||||
|
||||
let result = await request.get('/assets?protocol=ssh');
|
||||
if (result.code === 1) {
|
||||
let assets = result.data;
|
||||
assets.sort(compare('name'));
|
||||
this.setState({
|
||||
assets: assets
|
||||
});
|
||||
} else {
|
||||
message.error(result.message);
|
||||
}
|
||||
}}>执行</Button>
|
||||
|
||||
<Dropdown overlay={menu}>
|
||||
@ -640,7 +627,8 @@ class DynamicCommand extends Component {
|
||||
<Modal
|
||||
title="选择资产"
|
||||
visible={this.state.assetsVisible}
|
||||
|
||||
width={window.innerWidth * 0.8}
|
||||
centered={true}
|
||||
onOk={this.executeCommand}
|
||||
onCancel={() => {
|
||||
this.setState({
|
||||
@ -648,19 +636,11 @@ class DynamicCommand extends Component {
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Checkbox indeterminate={this.state.indeterminate} onChange={this.onCheckAllChange}
|
||||
checked={this.state.checkAllChecked}>
|
||||
全选
|
||||
</Checkbox>
|
||||
<Divider/>
|
||||
<ChooseAsset
|
||||
setCheckedAssets={this.setCheckedAssets}
|
||||
>
|
||||
|
||||
<CheckboxGroup options={this.state.assets.map((item) => {
|
||||
return {
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
key: item.id,
|
||||
}
|
||||
})} value={this.state.checkedAssets} onChange={this.onChange}/>
|
||||
</ChooseAsset>
|
||||
</Modal>
|
||||
|
||||
|
||||
|
@ -21,6 +21,7 @@ const DynamicCommandModal = ({title, visible, handleOk, handleCancel, confirmLoa
|
||||
title={title}
|
||||
visible={visible}
|
||||
maskClosable={false}
|
||||
|
||||
onOk={() => {
|
||||
form
|
||||
.validateFields()
|
||||
|
@ -47,6 +47,7 @@ const CredentialModal = ({title, visible, handleOk, handleCancel, confirmLoading
|
||||
title={title}
|
||||
visible={visible}
|
||||
maskClosable={false}
|
||||
|
||||
onOk={() => {
|
||||
form
|
||||
.validateFields()
|
||||
|
@ -47,6 +47,7 @@ const JobModal = ({title, visible, handleOk, handleCancel, confirmLoading, model
|
||||
title={title}
|
||||
visible={visible}
|
||||
maskClosable={false}
|
||||
|
||||
onOk={() => {
|
||||
form
|
||||
.validateFields()
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import {Form, Input, InputNumber, Modal, Radio, Tooltip} from "antd/lib/index";
|
||||
import {ExclamationCircleOutlined} from "@ant-design/icons";
|
||||
import {Form, Input, InputNumber, Modal, Radio} from "antd/lib/index";
|
||||
|
||||
const formItemLayout = {
|
||||
labelCol: {span: 6},
|
||||
@ -20,6 +19,7 @@ const SecurityModal = ({title, visible, handleOk, handleCancel, confirmLoading,
|
||||
title={title}
|
||||
visible={visible}
|
||||
maskClosable={false}
|
||||
|
||||
onOk={() => {
|
||||
form
|
||||
.validateFields()
|
||||
@ -54,9 +54,7 @@ const SecurityModal = ({title, visible, handleOk, handleCancel, confirmLoading,
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={<Tooltip
|
||||
title="数字越小代表优先级越高。">优先级
|
||||
<ExclamationCircleOutlined/></Tooltip>} name='priority' rules={[{required: true, message: '请输入优先级'}]}>
|
||||
<Form.Item label="优先级" name='priority' rules={[{required: true, message: '请输入优先级'}]}>
|
||||
<InputNumber min={0} max={100}/>
|
||||
</Form.Item>
|
||||
|
||||
|
@ -628,7 +628,6 @@ class User extends Component {
|
||||
width={window.innerWidth * 0.8}
|
||||
title='已授权资产'
|
||||
visible={this.state.assetVisible}
|
||||
|
||||
maskClosable={false}
|
||||
destroyOnClose={true}
|
||||
onOk={() => {
|
||||
@ -649,7 +648,6 @@ class User extends Component {
|
||||
<Modal title="修改密码" visible={this.state.changePasswordVisible}
|
||||
confirmLoading={this.state.changePasswordConfirmLoading}
|
||||
maskClosable={false}
|
||||
|
||||
onOk={() => {
|
||||
this.changePasswordFormRef.current
|
||||
.validateFields()
|
||||
|
@ -429,7 +429,6 @@ class UserGroup extends Component {
|
||||
title='已授权资产'
|
||||
visible={this.state.assetVisible}
|
||||
maskClosable={false}
|
||||
|
||||
destroyOnClose={true}
|
||||
onOk={() => {
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user