diff --git a/config.yml b/config.yml index 62aa885..d4292cc 100644 --- a/config.yml +++ b/config.yml @@ -1,3 +1,4 @@ +debug: true db: mysql mysql: hostname: 172.16.101.32 diff --git a/main.go b/main.go index 92b513f..fcfa9b9 100644 --- a/main.go +++ b/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 diff --git a/pkg/api/account.go b/pkg/api/account.go index c673de1..29e7a10 100644 --- a/pkg/api/account.go +++ b/pkg/api/account.go @@ -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) diff --git a/pkg/api/asset.go b/pkg/api/asset.go index ca4e734..4e2f30d 100644 --- a/pkg/api/asset.go +++ b/pkg/api/asset.go @@ -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 { diff --git a/pkg/api/command.go b/pkg/api/command.go index 2d71058..78ba421 100644 --- a/pkg/api/command.go +++ b/pkg/api/command.go @@ -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 diff --git a/pkg/api/credential.go b/pkg/api/credential.go index a091557..aa2848e 100644 --- a/pkg/api/credential.go +++ b/pkg/api/credential.go @@ -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 { diff --git a/pkg/api/middleware.go b/pkg/api/middleware.go index 775b21c..acb8173 100644 --- a/pkg/api/middleware.go +++ b/pkg/api/middleware.go @@ -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) diff --git a/pkg/api/routes.go b/pkg/api/routes.go index bb33423..2805deb 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -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 } diff --git a/pkg/config/config.go b/pkg/config/config.go index fe9cceb..4b1bc47 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -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 diff --git a/pkg/model/asset.go b/pkg/model/asset.go index c58f2f3..9e6fe52 100644 --- a/pkg/model/asset.go +++ b/pkg/model/asset.go @@ -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"` diff --git a/pkg/model/login-log.go b/pkg/model/login-log.go index 295f4fc..44f845a 100644 --- a/pkg/model/login-log.go +++ b/pkg/model/login-log.go @@ -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 } diff --git a/pkg/model/user.go b/pkg/model/user.go index 14825f0..226f43f 100644 --- a/pkg/model/user.go +++ b/pkg/model/user.go @@ -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{}) // 删除用户组中的用户关系 diff --git a/sample.csv b/sample.csv new file mode 100644 index 0000000..ccdea43 --- /dev/null +++ b/sample.csv @@ -0,0 +1 @@ +测试阿里云,ssh,10.1.1.2,22,username,password,privateKey,passphrase,description \ No newline at end of file diff --git a/web/package.json b/web/package.json index 8b0614f..ec06b94 100644 --- a/web/package.json +++ b/web/package.json @@ -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", diff --git a/web/src/common/constants.js b/web/src/common/constants.js index 2c22a59..7bead42 100644 --- a/web/src/common/constants.js +++ b/web/src/common/constants.js @@ -2,5 +2,6 @@ export const PROTOCOL_COLORS = { 'rdp': 'cyan', 'ssh': 'blue', 'telnet': 'geekblue', - 'vnc': 'purple' + 'vnc': 'purple', + 'kubernetes': 'volcano' } \ No newline at end of file diff --git a/web/src/components/asset/Asset.js b/web/src/components/asset/Asset.js index 8c316e9..5d443ba 100644 --- a/web/src/components/asset/Asset.js +++ b/web/src/components/asset/Asset.js @@ -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 { + {isAdmin() ? + + + : undefined + } + + + + + : undefined + } + 更换资源「{this.state.selected['name']}」的所有者 } visible={this.state.changeOwnerModalVisible}