增加批量导入资产功能

This commit is contained in:
dushixiang 2021-03-09 23:52:44 +08:00
parent b48f650f7e
commit dc9934bc9e
16 changed files with 273 additions and 23 deletions

View File

@ -1,3 +1,4 @@
debug: true
db: mysql
mysql:
hostname: 172.16.101.32

40
main.go
View File

@ -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

View File

@ -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)

View File

@ -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 {

View File

@ -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

View File

@ -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 {

View File

@ -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)

View File

@ -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
}

View File

@ -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

View File

@ -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"`

View File

@ -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
}

View File

@ -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
View File

@ -0,0 +1 @@
测试阿里云,ssh,10.1.1.2,22,username,password,privateKey,passphrase,description
1 测试阿里云 ssh 10.1.1.2 22 username password privateKey passphrase description

View File

@ -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",

View File

@ -2,5 +2,6 @@ export const PROTOCOL_COLORS = {
'rdp': 'cyan',
'ssh': 'blue',
'telnet': 'geekblue',
'vnc': 'purple'
'vnc': 'purple',
'kubernetes': 'volcano'
}

View File

@ -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}