增加批量导入资产功能
This commit is contained in:
parent
b48f650f7e
commit
dc9934bc9e
@ -1,3 +1,4 @@
|
||||
debug: true
|
||||
db: mysql
|
||||
mysql:
|
||||
hostname: 172.16.101.32
|
||||
|
40
main.go
40
main.go
@ -11,6 +11,7 @@ import (
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"io"
|
||||
"next-terminal/pkg/api"
|
||||
"next-terminal/pkg/config"
|
||||
@ -24,7 +25,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const Version = "v0.3.2"
|
||||
const Version = "v0.3.3"
|
||||
|
||||
func main() {
|
||||
err := Run()
|
||||
@ -65,6 +66,13 @@ func Run() error {
|
||||
return err
|
||||
}
|
||||
|
||||
var logMode logger.Interface
|
||||
if global.Config.Debug {
|
||||
logMode = logger.Default.LogMode(logger.Info)
|
||||
} else {
|
||||
logMode = logger.Default.LogMode(logger.Silent)
|
||||
}
|
||||
|
||||
fmt.Printf("当前数据库模式为:%v\n", global.Config.DB)
|
||||
if global.Config.DB == "mysql" {
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
@ -75,11 +83,11 @@ func Run() error {
|
||||
global.Config.Mysql.Database,
|
||||
)
|
||||
global.DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||||
//Logger: logger.Default.LogMode(logger.Info),
|
||||
Logger: logMode,
|
||||
})
|
||||
} else {
|
||||
global.DB, err = gorm.Open(sqlite.Open(global.Config.Sqlite.File), &gorm.Config{
|
||||
//Logger: logger.Default.LogMode(logger.Info),
|
||||
Logger: logMode,
|
||||
})
|
||||
}
|
||||
|
||||
@ -196,9 +204,12 @@ func Run() error {
|
||||
global.Cache = cache.New(5*time.Minute, 10*time.Minute)
|
||||
global.Cache.OnEvicted(func(key string, value interface{}) {
|
||||
if strings.HasPrefix(key, api.Token) {
|
||||
token := strings.Split(key, ":")[1]
|
||||
token := api.GetTokenFormCacheKey(key)
|
||||
logrus.Debugf("用户Token「%v」过期", token)
|
||||
model.Logout(token)
|
||||
err := model.Logout(token)
|
||||
if err != nil {
|
||||
logrus.Errorf("退出登录失败 %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
global.Store = global.NewStore()
|
||||
@ -256,7 +267,7 @@ func Run() error {
|
||||
User: user,
|
||||
}
|
||||
|
||||
cacheKey := strings.Join([]string{api.Token, token}, ":")
|
||||
cacheKey := api.BuildCacheKeyByToken(token)
|
||||
|
||||
if authorization.Remember {
|
||||
// 记住登录有效期两周
|
||||
@ -267,6 +278,23 @@ func Run() error {
|
||||
logrus.Debugf("重新加载用户「%v」授权Token「%v」到缓存", user.Nickname, token)
|
||||
}
|
||||
|
||||
// 修正用户登录状态
|
||||
onlineUsers, err := model.FindOnlineUsers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range onlineUsers {
|
||||
logs, err := model.FindAliveLoginLogsByUserId(onlineUsers[i].ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(logs) == 0 {
|
||||
if err := model.UpdateUserOnline(false, onlineUsers[i].ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
e := api.SetupRoutes()
|
||||
if err := handle.InitProperties(); err != nil {
|
||||
return err
|
||||
|
@ -91,7 +91,7 @@ func LoginSuccess(c echo.Context, loginAccount LoginAccount, user model.User) (t
|
||||
User: user,
|
||||
}
|
||||
|
||||
cacheKey := strings.Join([]string{Token, token}, ":")
|
||||
cacheKey := BuildCacheKeyByToken(token)
|
||||
|
||||
if authorization.Remember {
|
||||
// 记住登录有效期两周
|
||||
@ -119,6 +119,16 @@ func LoginSuccess(c echo.Context, loginAccount LoginAccount, user model.User) (t
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func BuildCacheKeyByToken(token string) string {
|
||||
cacheKey := strings.Join([]string{Token, token}, ":")
|
||||
return cacheKey
|
||||
}
|
||||
|
||||
func GetTokenFormCacheKey(cacheKey string) string {
|
||||
token := strings.Split(cacheKey, ":")[1]
|
||||
return token
|
||||
}
|
||||
|
||||
func loginWithTotpEndpoint(c echo.Context) error {
|
||||
var loginAccount LoginAccount
|
||||
if err := c.Bind(&loginAccount); err != nil {
|
||||
@ -165,7 +175,7 @@ func loginWithTotpEndpoint(c echo.Context) error {
|
||||
|
||||
func LogoutEndpoint(c echo.Context) error {
|
||||
token := GetToken(c)
|
||||
cacheKey := strings.Join([]string{Token, token}, ":")
|
||||
cacheKey := BuildCacheKeyByToken(token)
|
||||
global.Cache.Delete(cacheKey)
|
||||
model.Logout(token)
|
||||
return Success(c, nil)
|
||||
|
@ -1,6 +1,8 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/labstack/echo/v4"
|
||||
@ -44,6 +46,77 @@ func AssetCreateEndpoint(c echo.Context) error {
|
||||
return Success(c, item)
|
||||
}
|
||||
|
||||
func AssetImportEndpoint(c echo.Context) error {
|
||||
account, _ := GetCurrentAccount(c)
|
||||
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer src.Close()
|
||||
reader := csv.NewReader(bufio.NewReader(src))
|
||||
records, err := reader.ReadAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
total := len(records)
|
||||
if total == 0 {
|
||||
return errors.New("csv数据为空")
|
||||
}
|
||||
|
||||
var successCount = 0
|
||||
var errorCount = 0
|
||||
m := echo.Map{}
|
||||
|
||||
for i := 0; i < total; i++ {
|
||||
record := records[i]
|
||||
if len(record) >= 9 {
|
||||
port, _ := strconv.Atoi(record[3])
|
||||
asset := model.Asset{
|
||||
ID: utils.UUID(),
|
||||
Name: record[0],
|
||||
Protocol: record[1],
|
||||
IP: record[2],
|
||||
Port: port,
|
||||
AccountType: model.Custom,
|
||||
Username: record[4],
|
||||
Password: record[5],
|
||||
PrivateKey: record[6],
|
||||
Passphrase: record[7],
|
||||
Description: record[8],
|
||||
Created: utils.NowJsonTime(),
|
||||
Owner: account.ID,
|
||||
}
|
||||
|
||||
err := model.CreateNewAsset(&asset)
|
||||
if err != nil {
|
||||
errorCount++
|
||||
m[strconv.Itoa(i)] = err.Error()
|
||||
} else {
|
||||
successCount++
|
||||
// 创建后自动检测资产是否存活
|
||||
go func() {
|
||||
active := utils.Tcping(asset.IP, asset.Port)
|
||||
model.UpdateAssetActiveById(active, asset.ID)
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Success(c, echo.Map{
|
||||
"successCount": successCount,
|
||||
"errorCount": errorCount,
|
||||
"data": m,
|
||||
})
|
||||
}
|
||||
|
||||
func AssetPagingEndpoint(c echo.Context) error {
|
||||
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
|
||||
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
|
||||
@ -134,6 +207,10 @@ func AssetUpdateEndpoint(c echo.Context) error {
|
||||
func AssetGetAttributeEndpoint(c echo.Context) error {
|
||||
|
||||
assetId := c.Param("id")
|
||||
if err := PreCheckAssetPermission(c, assetId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attributeMap, err := model.FindAssetAttrMapByAssetId(assetId)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -177,6 +254,9 @@ func AssetDeleteEndpoint(c echo.Context) error {
|
||||
|
||||
func AssetGetEndpoint(c echo.Context) (err error) {
|
||||
id := c.Param("id")
|
||||
if err := PreCheckAssetPermission(c, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var item model.Asset
|
||||
if item, err = model.FindAssetById(id); err != nil {
|
||||
|
@ -84,6 +84,11 @@ func CommandDeleteEndpoint(c echo.Context) error {
|
||||
|
||||
func CommandGetEndpoint(c echo.Context) (err error) {
|
||||
id := c.Param("id")
|
||||
|
||||
if err := PreCheckCommandPermission(c, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var item model.Command
|
||||
if item, err = model.FindCommandById(id); err != nil {
|
||||
return err
|
||||
|
@ -140,6 +140,9 @@ func CredentialDeleteEndpoint(c echo.Context) error {
|
||||
|
||||
func CredentialGetEndpoint(c echo.Context) error {
|
||||
id := c.Param("id")
|
||||
if err := PreCheckCredentialPermission(c, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
item, err := model.FindCredentialById(id)
|
||||
if err != nil {
|
||||
|
@ -55,7 +55,7 @@ func Auth(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
}
|
||||
|
||||
token := GetToken(c)
|
||||
cacheKey := strings.Join([]string{Token, token}, ":")
|
||||
cacheKey := BuildCacheKeyByToken(token)
|
||||
authorization, found := global.Cache.Get(cacheKey)
|
||||
if !found {
|
||||
return Fail(c, 401, "您的登录信息已失效,请重新登录后再试。")
|
||||
@ -63,9 +63,9 @@ func Auth(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
|
||||
if authorization.(Authorization).Remember {
|
||||
// 记住登录有效期两周
|
||||
global.Cache.Set(token, authorization, time.Hour*time.Duration(24*14))
|
||||
global.Cache.Set(cacheKey, authorization, time.Hour*time.Duration(24*14))
|
||||
} else {
|
||||
global.Cache.Set(token, authorization, time.Hour*time.Duration(2))
|
||||
global.Cache.Set(cacheKey, authorization, time.Hour*time.Duration(2))
|
||||
}
|
||||
|
||||
return next(c)
|
||||
|
@ -71,10 +71,11 @@ func SetupRoutes() *echo.Echo {
|
||||
//userGroups.DELETE("/:id/members/:memberId", UserGroupDelMembersEndpoint)
|
||||
}
|
||||
|
||||
assets := e.Group("/assets", Auth)
|
||||
assets := e.Group("/assets")
|
||||
{
|
||||
assets.GET("", AssetAllEndpoint)
|
||||
assets.POST("", AssetCreateEndpoint)
|
||||
assets.POST("/import", Admin(AssetImportEndpoint))
|
||||
assets.GET("/paging", AssetPagingEndpoint)
|
||||
assets.POST("/:id/tcping", AssetTcpingEndpoint)
|
||||
assets.PUT("/:id", AssetUpdateEndpoint)
|
||||
@ -110,7 +111,7 @@ func SetupRoutes() *echo.Echo {
|
||||
sessions := e.Group("/sessions")
|
||||
{
|
||||
sessions.POST("", SessionCreateEndpoint)
|
||||
sessions.GET("/paging", SessionPagingEndpoint)
|
||||
sessions.GET("/paging", Admin(SessionPagingEndpoint))
|
||||
sessions.POST("/:id/connect", SessionConnectEndpoint)
|
||||
sessions.POST("/:id/disconnect", Admin(SessionDisconnectEndpoint))
|
||||
sessions.POST("/:id/resize", SessionResizeEndpoint)
|
||||
@ -138,7 +139,7 @@ func SetupRoutes() *echo.Echo {
|
||||
loginLogs.DELETE("/:id", LoginLogDeleteEndpoint)
|
||||
}
|
||||
|
||||
e.GET("/properties", PropertyGetEndpoint)
|
||||
e.GET("/properties", Admin(PropertyGetEndpoint))
|
||||
e.PUT("/properties", Admin(PropertyUpdateEndpoint))
|
||||
|
||||
e.GET("/overview/counter", OverviewCounterEndPoint)
|
||||
@ -202,7 +203,8 @@ func GetToken(c echo.Context) string {
|
||||
|
||||
func GetCurrentAccount(c echo.Context) (model.User, bool) {
|
||||
token := GetToken(c)
|
||||
get, b := global.Cache.Get(token)
|
||||
cacheKey := BuildCacheKeyByToken(token)
|
||||
get, b := global.Cache.Get(cacheKey)
|
||||
if b {
|
||||
return get.(Authorization).User, true
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Debug bool
|
||||
DB string
|
||||
Server *Server
|
||||
Mysql *Mysql
|
||||
@ -78,6 +79,7 @@ func SetupConfig() (*Config, error) {
|
||||
Key: viper.GetString("server.key"),
|
||||
},
|
||||
ResetPassword: viper.GetString("reset-password"),
|
||||
Debug: viper.GetBool("debug"),
|
||||
}
|
||||
|
||||
return config, nil
|
||||
|
@ -9,8 +9,8 @@ import (
|
||||
type Asset struct {
|
||||
ID string `gorm:"primary_key " json:"id"`
|
||||
Name string `json:"name"`
|
||||
IP string `json:"ip"`
|
||||
Protocol string `json:"protocol"`
|
||||
IP string `json:"ip"`
|
||||
Port int `json:"port"`
|
||||
AccountType string `json:"accountType"`
|
||||
Username string `json:"username"`
|
||||
|
@ -81,7 +81,7 @@ func FindLoginLogById(id string) (o LoginLog, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func Logout(token string) {
|
||||
func Logout(token string) (err error) {
|
||||
|
||||
loginLog, err := FindLoginLogById(token)
|
||||
if err != nil {
|
||||
@ -89,7 +89,10 @@ func Logout(token string) {
|
||||
return
|
||||
}
|
||||
|
||||
global.DB.Table("login_logs").Where("id = ?", token).Update("logout_time", utils.NowJsonTime())
|
||||
err = global.DB.Updates(&LoginLog{LogoutTime: utils.NowJsonTime(), ID: token}).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
loginLogs, err := FindAliveLoginLogsByUserId(loginLog.UserId)
|
||||
if err != nil {
|
||||
@ -97,6 +100,7 @@ func Logout(token string) {
|
||||
}
|
||||
|
||||
if len(loginLogs) == 0 {
|
||||
UpdateUserById(&User{Online: false}, loginLog.UserId)
|
||||
err = UpdateUserOnline(false, loginLog.UserId)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -129,6 +129,17 @@ func UpdateUserById(o *User, id string) {
|
||||
global.DB.Updates(o)
|
||||
}
|
||||
|
||||
func UpdateUserOnline(online bool, id string) (err error) {
|
||||
sql := "update users set online = ? where id = ?"
|
||||
err = global.DB.Exec(sql, online, id).Error
|
||||
return
|
||||
}
|
||||
|
||||
func FindOnlineUsers() (o []User, err error) {
|
||||
err = global.DB.Where("online = ?", true).Find(&o).Error
|
||||
return
|
||||
}
|
||||
|
||||
func DeleteUserById(id string) {
|
||||
global.DB.Where("id = ?", id).Delete(&User{})
|
||||
// 删除用户组中的用户关系
|
||||
|
1
sample.csv
Normal file
1
sample.csv
Normal file
@ -0,0 +1 @@
|
||||
测试阿里云,ssh,10.1.1.2,22,username,password,privateKey,passphrase,description
|
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "next-terminal",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3-beta",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.3.0",
|
||||
|
@ -2,5 +2,6 @@ export const PROTOCOL_COLORS = {
|
||||
'rdp': 'cyan',
|
||||
'ssh': 'blue',
|
||||
'telnet': 'geekblue',
|
||||
'vnc': 'purple'
|
||||
'vnc': 'purple',
|
||||
'kubernetes': 'volcano'
|
||||
}
|
@ -12,6 +12,7 @@ import {
|
||||
Layout,
|
||||
Menu,
|
||||
Modal,
|
||||
notification,
|
||||
PageHeader,
|
||||
Row,
|
||||
Select,
|
||||
@ -26,19 +27,24 @@ import qs from "qs";
|
||||
import AssetModal from "./AssetModal";
|
||||
import request from "../../common/request";
|
||||
import {message} from "antd/es";
|
||||
import {isEmpty, itemRender} from "../../utils/utils";
|
||||
import {getHeaders, isEmpty, itemRender} from "../../utils/utils";
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
DeleteOutlined,
|
||||
DownOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
ImportOutlined,
|
||||
PlusOutlined,
|
||||
SyncOutlined,
|
||||
UndoOutlined
|
||||
UndoOutlined,
|
||||
UploadOutlined
|
||||
} from '@ant-design/icons';
|
||||
import {PROTOCOL_COLORS} from "../../common/constants";
|
||||
import Logout from "../user/Logout";
|
||||
import {hasPermission, isAdmin} from "../../service/permission";
|
||||
import Upload from "antd/es/upload";
|
||||
import axios from "axios";
|
||||
import {server} from "../../common/env";
|
||||
|
||||
|
||||
const confirm = Modal.confirm;
|
||||
@ -88,6 +94,9 @@ class Asset extends Component {
|
||||
users: [],
|
||||
selected: {},
|
||||
selectedSharers: [],
|
||||
importModalVisible: false,
|
||||
fileList: [],
|
||||
uploading: false,
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
@ -726,6 +735,20 @@ class Asset extends Component {
|
||||
|
||||
<Divider type="vertical"/>
|
||||
|
||||
{isAdmin() ?
|
||||
<Tooltip title="批量导入">
|
||||
<Button type="dashed" icon={<ImportOutlined/>}
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
importModalVisible: true
|
||||
})
|
||||
}}>
|
||||
|
||||
</Button>
|
||||
</Tooltip> : undefined
|
||||
}
|
||||
|
||||
|
||||
<Tooltip title="新增">
|
||||
<Button type="dashed" icon={<PlusOutlined/>}
|
||||
onClick={() => this.showModal('新增资产', {})}>
|
||||
@ -803,6 +826,85 @@ class Asset extends Component {
|
||||
: null
|
||||
}
|
||||
|
||||
{
|
||||
this.state.importModalVisible ?
|
||||
<Modal title="资产导入" visible={true}
|
||||
onOk={() => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", this.state.fileList[0]);
|
||||
|
||||
let headers = getHeaders();
|
||||
headers['Content-Type'] = 'multipart/form-data';
|
||||
|
||||
axios
|
||||
.post(server + "/assets/import", formData, {
|
||||
headers: headers
|
||||
})
|
||||
.then((resp) => {
|
||||
console.log("上传成功", resp);
|
||||
this.setState({
|
||||
importModalVisible: false
|
||||
})
|
||||
let result = resp.data;
|
||||
if (result['code'] === 1) {
|
||||
let data = result['data'];
|
||||
let successCount = data['successCount'];
|
||||
let errorCount = data['errorCount'];
|
||||
if (errorCount === 0) {
|
||||
notification['success']({
|
||||
message: '导入资产成功',
|
||||
description: '共导入成功' + successCount + '条资产。',
|
||||
});
|
||||
} else {
|
||||
notification['info']({
|
||||
message: '导入资产完成',
|
||||
description: `共导入成功${successCount}条资产,失败${errorCount}条资产。`,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
notification['error']({
|
||||
message: '导入资产失败',
|
||||
description: result['message'],
|
||||
});
|
||||
}
|
||||
this.loadTableData();
|
||||
});
|
||||
}}
|
||||
onCancel={() => {
|
||||
this.setState({
|
||||
importModalVisible: false
|
||||
})
|
||||
}}
|
||||
okButtonProps={{
|
||||
disabled: this.state.fileList.length === 0
|
||||
}}
|
||||
>
|
||||
<Upload
|
||||
maxCount={1}
|
||||
onRemove={file => {
|
||||
this.setState(state => {
|
||||
const index = state.fileList.indexOf(file);
|
||||
const newFileList = state.fileList.slice();
|
||||
newFileList.splice(index, 1);
|
||||
return {
|
||||
fileList: newFileList,
|
||||
};
|
||||
});
|
||||
}}
|
||||
beforeUpload={(file) => {
|
||||
this.setState(state => ({
|
||||
fileList: [file],
|
||||
}));
|
||||
return false;
|
||||
}}
|
||||
fileList={this.state.fileList}
|
||||
>
|
||||
<Button icon={<UploadOutlined/>}>选择csv文件</Button>
|
||||
</Upload>
|
||||
</Modal>
|
||||
: undefined
|
||||
}
|
||||
|
||||
<Modal title={<Text>更换资源「<strong style={{color: '#1890ff'}}>{this.state.selected['name']}</strong>」的所有者
|
||||
</Text>}
|
||||
visible={this.state.changeOwnerModalVisible}
|
||||
|
Loading…
Reference in New Issue
Block a user