- 增加登录日志
- 增加数据库索引 - 修改原生安装文档
This commit is contained in:
parent
1d4653a561
commit
f0157dbaeb
@ -126,7 +126,7 @@ mkdir ~/next-terminal & cd ~/next-terminal
|
||||
|
||||
下载
|
||||
```shell
|
||||
wget https://github.com/dushixiang/next-terminal/releases/download/v0.0.4/next-terminal.tgz
|
||||
wget https://github.com/dushixiang/next-terminal/releases/latest/download/next-terminal.tgz
|
||||
```
|
||||
|
||||
解压
|
||||
|
51
main.go
51
main.go
@ -10,7 +10,6 @@ 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"
|
||||
@ -23,6 +22,8 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const Version = "v0.0.9"
|
||||
|
||||
func main() {
|
||||
log.Fatal(Run())
|
||||
}
|
||||
@ -35,12 +36,11 @@ func Run() error {
|
||||
/ | \_/ __ \\ \/ /\ __\ | |_/ __ \_ __ \/ \| |/ \\__ \ | |
|
||||
/ | \ ___/ > < | | | |\ ___/| | \/ Y Y \ | | \/ __ \| |__
|
||||
\____|__ /\___ >__/\_ \ |__| |____| \___ >__| |__|_| /__|___| (____ /____/
|
||||
\/ \/ \/ \/ \/ \/ \/
|
||||
|
||||
`)
|
||||
\/ \/ \/ \/ \/ \/ \/ ` + Version + "\n")
|
||||
|
||||
var err error
|
||||
//logrus.SetReportCaller(true)
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
logrus.SetFormatter(&nested.Formatter{
|
||||
HideKeys: true,
|
||||
FieldsOrder: []string{"component", "category"},
|
||||
@ -70,7 +70,7 @@ func Run() error {
|
||||
global.Config.Mysql.Database,
|
||||
)
|
||||
global.DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Info),
|
||||
//Logger: logger.Default.LogMode(logger.Info),
|
||||
})
|
||||
} else {
|
||||
global.DB, err = gorm.Open(sqlite.Open(global.Config.Sqlite.File), &gorm.Config{})
|
||||
@ -88,8 +88,9 @@ func Run() error {
|
||||
users := model.FindAllUser()
|
||||
if len(users) == 0 {
|
||||
|
||||
initPassword := "admin"
|
||||
var pass []byte
|
||||
if pass, err = utils.Encoder.Encode([]byte("admin")); err != nil {
|
||||
if pass, err = utils.Encoder.Encode([]byte(initPassword)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -104,6 +105,7 @@ func Run() error {
|
||||
if err := model.CreateNewUser(&user); err != nil {
|
||||
return err
|
||||
}
|
||||
logrus.Infof("初始用户创建成功,账号:「%v」密码:「%v」", user.Username, initPassword)
|
||||
} else {
|
||||
for i := range users {
|
||||
// 修正默认用户类型为管理员
|
||||
@ -141,6 +143,9 @@ func Run() error {
|
||||
if err := global.DB.AutoMigrate(&model.UserGroupMember{}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := global.DB.AutoMigrate(&model.LoginLog{}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := global.DB.AutoMigrate(&model.Num{}); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -153,9 +158,43 @@ func Run() error {
|
||||
}
|
||||
}
|
||||
|
||||
// 配置缓存器
|
||||
global.Cache = cache.New(5*time.Minute, 10*time.Minute)
|
||||
global.Cache.OnEvicted(func(key string, value interface{}) {
|
||||
logrus.Debugf("用户Token「%v」过期", key)
|
||||
model.Logout(key)
|
||||
})
|
||||
global.Store = global.NewStore()
|
||||
|
||||
loginLogs, err := model.FindAliveLoginLogs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range loginLogs {
|
||||
loginLog := loginLogs[i]
|
||||
token := loginLog.ID
|
||||
user, err := model.FindUserById(loginLog.UserId)
|
||||
if err != nil {
|
||||
logrus.Debugf("用户「%v」获取失败,忽略", loginLog.UserId)
|
||||
continue
|
||||
}
|
||||
|
||||
authorization := api.Authorization{
|
||||
Token: token,
|
||||
Remember: loginLog.Remember,
|
||||
User: user,
|
||||
}
|
||||
|
||||
if authorization.Remember {
|
||||
// 记住登录有效期两周
|
||||
global.Cache.Set(token, authorization, api.RememberEffectiveTime)
|
||||
} else {
|
||||
global.Cache.Set(token, authorization, api.NotRememberEffectiveTime)
|
||||
}
|
||||
logrus.Debugf("重新加载用户「%v」授权Token「%v」到缓存", user.Nickname, token)
|
||||
}
|
||||
|
||||
e := api.SetupRoutes()
|
||||
if err := handle.InitProperties(); err != nil {
|
||||
return err
|
||||
|
@ -11,6 +11,11 @@ import (
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
const (
|
||||
RememberEffectiveTime = time.Hour * time.Duration(24*14)
|
||||
NotRememberEffectiveTime = time.Minute * time.Duration(2)
|
||||
)
|
||||
|
||||
type LoginAccount struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
@ -53,7 +58,16 @@ func LoginEndpoint(c echo.Context) error {
|
||||
return Fail(c, 0, "")
|
||||
}
|
||||
|
||||
token := strings.Join([]string{utils.UUID(), utils.UUID(), utils.UUID(), utils.UUID()}, "")
|
||||
token, err := Login(c, loginAccount, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return Success(c, token)
|
||||
}
|
||||
|
||||
func Login(c echo.Context, loginAccount LoginAccount, user model.User) (token string, err error) {
|
||||
token = strings.Join([]string{utils.UUID(), utils.UUID(), utils.UUID(), utils.UUID()}, "")
|
||||
|
||||
authorization := Authorization{
|
||||
Token: token,
|
||||
@ -63,14 +77,28 @@ func LoginEndpoint(c echo.Context) error {
|
||||
|
||||
if authorization.Remember {
|
||||
// 记住登录有效期两周
|
||||
global.Cache.Set(token, authorization, time.Hour*time.Duration(24*14))
|
||||
global.Cache.Set(token, authorization, RememberEffectiveTime)
|
||||
} else {
|
||||
global.Cache.Set(token, authorization, time.Hour*time.Duration(2))
|
||||
global.Cache.Set(token, authorization, NotRememberEffectiveTime)
|
||||
}
|
||||
|
||||
model.UpdateUserById(&model.User{Online: true}, user.ID)
|
||||
// 保存登录日志
|
||||
loginLog := model.LoginLog{
|
||||
ID: token,
|
||||
UserId: user.ID,
|
||||
ClientIP: c.RealIP(),
|
||||
ClientUserAgent: c.Request().UserAgent(),
|
||||
LoginTime: utils.NowJsonTime(),
|
||||
Remember: authorization.Remember,
|
||||
}
|
||||
|
||||
return Success(c, token)
|
||||
if model.CreateNewLoginLog(&loginLog) != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 修改登录状态
|
||||
model.UpdateUserById(&model.User{Online: true}, user.ID)
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func loginWithTotpEndpoint(c echo.Context) error {
|
||||
@ -92,29 +120,18 @@ func loginWithTotpEndpoint(c echo.Context) error {
|
||||
return Fail(c, -2, "您的TOTP不匹配")
|
||||
}
|
||||
|
||||
token := strings.Join([]string{utils.UUID(), utils.UUID(), utils.UUID(), utils.UUID()}, "")
|
||||
|
||||
authorization := Authorization{
|
||||
Token: token,
|
||||
Remember: loginAccount.Remember,
|
||||
User: user,
|
||||
token, err := Login(c, loginAccount, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if authorization.Remember {
|
||||
// 记住登录有效期两周
|
||||
global.Cache.Set(token, authorization, time.Hour*time.Duration(24*14))
|
||||
} else {
|
||||
global.Cache.Set(token, authorization, time.Hour*time.Duration(2))
|
||||
}
|
||||
|
||||
model.UpdateUserById(&model.User{Online: true}, user.ID)
|
||||
|
||||
return Success(c, token)
|
||||
}
|
||||
|
||||
func LogoutEndpoint(c echo.Context) error {
|
||||
token := GetToken(c)
|
||||
global.Cache.Delete(token)
|
||||
model.Logout(token)
|
||||
return Success(c, nil)
|
||||
}
|
||||
|
||||
|
42
pkg/api/login-log.go
Normal file
42
pkg/api/login-log.go
Normal file
@ -0,0 +1,42 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"next-terminal/pkg/global"
|
||||
"next-terminal/pkg/model"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func LoginLogPagingEndpoint(c echo.Context) error {
|
||||
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
|
||||
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
|
||||
userId := c.QueryParam("userId")
|
||||
clientIp := c.QueryParam("clientIp")
|
||||
|
||||
items, total, err := model.FindPageLoginLog(pageIndex, pageSize, userId, clientIp)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return Success(c, H{
|
||||
"total": total,
|
||||
"items": items,
|
||||
})
|
||||
}
|
||||
|
||||
func LoginLogDeleteEndpoint(c echo.Context) error {
|
||||
ids := c.Param("id")
|
||||
split := strings.Split(ids, ",")
|
||||
for i := range split {
|
||||
token := split[i]
|
||||
global.Cache.Delete(token)
|
||||
model.Logout(token)
|
||||
}
|
||||
if err := model.DeleteLoginLogByIdIn(split); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return Success(c, nil)
|
||||
}
|
@ -3,7 +3,6 @@ package api
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/sirupsen/logrus"
|
||||
"next-terminal/pkg/global"
|
||||
"next-terminal/pkg/model"
|
||||
"strings"
|
||||
@ -44,7 +43,6 @@ func Auth(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
token := GetToken(c)
|
||||
authorization, found := global.Cache.Get(token)
|
||||
if !found {
|
||||
logrus.Debugf("您的登录信息已失效,请重新登录后再试。")
|
||||
return Fail(c, 401, "您的登录信息已失效,请重新登录后再试。")
|
||||
}
|
||||
|
||||
@ -62,7 +60,10 @@ func Auth(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
func Admin(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
|
||||
account, _ := GetCurrentAccount(c)
|
||||
account, found := GetCurrentAccount(c)
|
||||
if !found {
|
||||
return Fail(c, 401, "您的登录信息已失效,请重新登录后再试。")
|
||||
}
|
||||
|
||||
if account.Type != model.TypeAdmin {
|
||||
return Fail(c, 403, "permission denied")
|
||||
|
@ -53,13 +53,13 @@ func SetupRoutes() *echo.Echo {
|
||||
users.POST("/:id/reset-totp", Admin(UserResetTotpEndpoint))
|
||||
}
|
||||
|
||||
userGroups := e.Group("/user-groups")
|
||||
userGroups := e.Group("/user-groups", Admin)
|
||||
{
|
||||
userGroups.POST("", Admin(UserGroupCreateEndpoint))
|
||||
userGroups.GET("/paging", Admin(UserGroupPagingEndpoint))
|
||||
userGroups.PUT("/:id", Admin(UserGroupUpdateEndpoint))
|
||||
userGroups.DELETE("/:id", Admin(UserGroupDeleteEndpoint))
|
||||
userGroups.GET("/:id", Admin(UserGroupGetEndpoint))
|
||||
userGroups.POST("", UserGroupCreateEndpoint)
|
||||
userGroups.GET("/paging", UserGroupPagingEndpoint)
|
||||
userGroups.PUT("/:id", UserGroupUpdateEndpoint)
|
||||
userGroups.DELETE("/:id", UserGroupDeleteEndpoint)
|
||||
userGroups.GET("/:id", UserGroupGetEndpoint)
|
||||
//userGroups.POST("/:id/members", UserGroupAddMembersEndpoint)
|
||||
//userGroups.DELETE("/:id/members/:memberId", UserGroupDelMembersEndpoint)
|
||||
}
|
||||
@ -125,6 +125,12 @@ func SetupRoutes() *echo.Echo {
|
||||
resourceSharers.POST("/add-resources", Admin(ResourceAddByUserIdAssignEndPoint))
|
||||
}
|
||||
|
||||
loginLogs := e.Group("login-logs", Admin)
|
||||
{
|
||||
loginLogs.GET("/paging", LoginLogPagingEndpoint)
|
||||
loginLogs.DELETE("/:id", LoginLogDeleteEndpoint)
|
||||
}
|
||||
|
||||
e.GET("/properties", PropertyGetEndpoint)
|
||||
e.PUT("/properties", Admin(PropertyUpdateEndpoint))
|
||||
|
||||
|
@ -15,14 +15,14 @@ type Asset struct {
|
||||
AccountType string `json:"accountType"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
CredentialId string `json:"credentialId"`
|
||||
CredentialId string `gorm:"index" json:"credentialId"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
Passphrase string `json:"passphrase"`
|
||||
Description string `json:"description"`
|
||||
Active bool `json:"active"`
|
||||
Created utils.JsonTime `json:"created"`
|
||||
Tags string `json:"tags"`
|
||||
Owner string `json:"owner"`
|
||||
Owner string `gorm:"index" json:"owner"`
|
||||
}
|
||||
|
||||
type AssetVo struct {
|
||||
|
@ -10,7 +10,7 @@ type Command struct {
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
Created utils.JsonTime `json:"created"`
|
||||
Owner string `json:"owner"`
|
||||
Owner string `gorm:"index" json:"owner"`
|
||||
}
|
||||
|
||||
type CommandVo struct {
|
||||
|
@ -20,7 +20,7 @@ type Credential struct {
|
||||
PrivateKey string `json:"privateKey"`
|
||||
Passphrase string `json:"passphrase"`
|
||||
Created utils.JsonTime `json:"created"`
|
||||
Owner string `json:"owner"`
|
||||
Owner string `gorm:"index" json:"owner"`
|
||||
}
|
||||
|
||||
func (r *Credential) TableName() string {
|
||||
|
102
pkg/model/login-log.go
Normal file
102
pkg/model/login-log.go
Normal file
@ -0,0 +1,102 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"next-terminal/pkg/global"
|
||||
"next-terminal/pkg/utils"
|
||||
)
|
||||
|
||||
type LoginLog struct {
|
||||
ID string `gorm:"primary_key" json:"id"`
|
||||
UserId string `gorm:"index" json:"userId"`
|
||||
ClientIP string `json:"clientIp"`
|
||||
ClientUserAgent string `json:"clientUserAgent"`
|
||||
LoginTime utils.JsonTime `json:"loginTime"`
|
||||
LogoutTime utils.JsonTime `json:"logoutTime"`
|
||||
Remember bool `json:"remember"`
|
||||
}
|
||||
|
||||
type LoginLogVo struct {
|
||||
ID string `json:"id"`
|
||||
UserId string `json:"userId"`
|
||||
UserName string `json:"userName"`
|
||||
ClientIP string `json:"clientIp"`
|
||||
ClientUserAgent string `json:"clientUserAgent"`
|
||||
LoginTime utils.JsonTime `json:"loginTime"`
|
||||
LogoutTime utils.JsonTime `json:"logoutTime"`
|
||||
Remember bool `json:"remember"`
|
||||
}
|
||||
|
||||
func (r *LoginLog) TableName() string {
|
||||
return "login_logs"
|
||||
}
|
||||
|
||||
func FindPageLoginLog(pageIndex, pageSize int, userId, clientIp string) (o []LoginLogVo, total int64, err error) {
|
||||
|
||||
db := global.DB.Table("login_logs").Select("login_logs.id,login_logs.user_id,login_logs.client_ip,login_logs.client_user_agent,login_logs.login_time, login_logs.logout_time, users.nickname as user_name").Joins("left join users on login_logs.user_id = users.id")
|
||||
dbCounter := global.DB.Table("login_logs").Select("DISTINCT login_logs.id")
|
||||
|
||||
if userId != "" {
|
||||
db = db.Where("login_logs.user_id = ?", userId)
|
||||
dbCounter = dbCounter.Where("login_logs.user_id = ?", userId)
|
||||
}
|
||||
|
||||
if clientIp != "" {
|
||||
db = db.Where("login_logs.client_ip like ?", "%"+clientIp+"%")
|
||||
dbCounter = dbCounter.Where("login_logs.client_ip like ?", "%"+clientIp+"%")
|
||||
}
|
||||
|
||||
err = dbCounter.Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
err = db.Order("login_logs.login_time desc").Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&o).Error
|
||||
if o == nil {
|
||||
o = make([]LoginLogVo, 0)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func FindAliveLoginLogs() (o []LoginLog, err error) {
|
||||
err = global.DB.Where("logout_time is null").Find(&o).Error
|
||||
return
|
||||
}
|
||||
|
||||
func FindAliveLoginLogsByUserId(userId string) (o []LoginLog, err error) {
|
||||
err = global.DB.Where("logout_time is null and user_id = ?", userId).Find(&o).Error
|
||||
return
|
||||
}
|
||||
|
||||
func CreateNewLoginLog(o *LoginLog) (err error) {
|
||||
return global.DB.Create(o).Error
|
||||
}
|
||||
|
||||
func DeleteLoginLogByIdIn(ids []string) (err error) {
|
||||
return global.DB.Where("id in ?", ids).Delete(&LoginLog{}).Error
|
||||
}
|
||||
|
||||
func FindLoginLogById(id string) (o LoginLog, err error) {
|
||||
err = global.DB.Where("id = ?", id).First(&o).Error
|
||||
return
|
||||
}
|
||||
|
||||
func Logout(id string) {
|
||||
|
||||
loginLog, err := FindLoginLogById(id)
|
||||
if err != nil {
|
||||
logrus.Warnf("登录日志「%v」获取失败", id)
|
||||
return
|
||||
}
|
||||
|
||||
global.DB.Table("login_logs").Where("id = ?", id).Update("logout_time", utils.NowJsonTime())
|
||||
|
||||
loginLogs, err := FindAliveLoginLogsByUserId(loginLog.UserId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(loginLogs) == 0 {
|
||||
UpdateUserById(&User{Online: false}, loginLog.UserId)
|
||||
}
|
||||
}
|
@ -8,10 +8,11 @@ import (
|
||||
)
|
||||
|
||||
type ResourceSharer struct {
|
||||
ID string `gorm:"primary_key" json:"name"`
|
||||
ResourceId string `json:"resourceId"`
|
||||
ResourceType string `json:"resourceType"`
|
||||
UserId string `json:"userId"`
|
||||
ID string `gorm:"primary_key" json:"id"`
|
||||
ResourceId string `gorm:"index" json:"resourceId"`
|
||||
ResourceType string `gorm:"index" json:"resourceType"`
|
||||
UserId string `gorm:"index" json:"userId"`
|
||||
UserGroupId string `gorm:"index" json:"userGroupId"`
|
||||
}
|
||||
|
||||
func (r *ResourceSharer) TableName() string {
|
||||
|
@ -19,14 +19,14 @@ type Session struct {
|
||||
IP string `json:"ip"`
|
||||
Port int `json:"port"`
|
||||
ConnectionId string `json:"connectionId"`
|
||||
AssetId string `json:"assetId"`
|
||||
AssetId string `gorm:"index" json:"assetId"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Creator string `json:"creator"`
|
||||
Creator string `gorm:"index" json:"creator"`
|
||||
ClientIP string `json:"clientIp"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Status string `json:"status"`
|
||||
Status string `gorm:"index" json:"status"`
|
||||
Recording string `json:"recording"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
Passphrase string `json:"passphrase"`
|
||||
|
@ -4,8 +4,8 @@ import "next-terminal/pkg/global"
|
||||
|
||||
type UserGroupMember struct {
|
||||
ID string `gorm:"primary_key" json:"name"`
|
||||
UserId string `json:"userId"`
|
||||
UserGroupId string `json:"userGroupId"`
|
||||
UserId string `gorm:"index" json:"userId"`
|
||||
UserGroupId string `gorm:"index" json:"userGroupId"`
|
||||
}
|
||||
|
||||
func (r *UserGroupMember) TableName() string {
|
||||
|
@ -13,7 +13,7 @@ type UserGroup struct {
|
||||
}
|
||||
|
||||
type UserGroupVo struct {
|
||||
ID string `gorm:"primary_key" json:"id"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Created utils.JsonTime `json:"created"`
|
||||
MemberCount int64 `json:"memberCount"`
|
||||
|
@ -13,7 +13,7 @@ const (
|
||||
|
||||
type User struct {
|
||||
ID string `gorm:"primary_key" json:"id"`
|
||||
Username string `json:"username"`
|
||||
Username string `gorm:"index:unique" json:"username"`
|
||||
Password string `json:"password"`
|
||||
Nickname string `json:"nickname"`
|
||||
TOTPSecret string `json:"-"`
|
||||
@ -24,7 +24,7 @@ type User struct {
|
||||
}
|
||||
|
||||
type UserVo struct {
|
||||
ID string `gorm:"primary_key" json:"id"`
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Nickname string `json:"nickname"`
|
||||
Online bool `json:"online"`
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "next-terminal",
|
||||
"version": "0.0.8",
|
||||
"version": "0.0.9",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.3.0",
|
||||
|
@ -22,11 +22,14 @@ import {
|
||||
DisconnectOutlined,
|
||||
IdcardOutlined,
|
||||
LinkOutlined,
|
||||
LoginOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
SettingOutlined,
|
||||
SolutionOutlined, TeamOutlined,
|
||||
UserOutlined, UserSwitchOutlined
|
||||
SolutionOutlined,
|
||||
TeamOutlined,
|
||||
UserOutlined,
|
||||
UserSwitchOutlined
|
||||
} from '@ant-design/icons';
|
||||
import Info from "./components/user/Info";
|
||||
import request from "./common/request";
|
||||
@ -36,6 +39,7 @@ import BatchCommand from "./components/command/BatchCommand";
|
||||
import {NT_PACKAGE} from "./utils/utils";
|
||||
import {isAdmin} from "./service/permission";
|
||||
import UserGroup from "./components/user/UserGroup";
|
||||
import LoginLog from "./components/session/LoginLog";
|
||||
|
||||
const {Footer, Sider} = Layout;
|
||||
|
||||
@ -142,17 +146,16 @@ class App extends Component {
|
||||
</Menu.Item>
|
||||
|
||||
<SubMenu key='resource' title='资源管理' icon={<CloudServerOutlined/>}>
|
||||
<Menu.Item key="idcard" icon={<IdcardOutlined/>}>
|
||||
<Link to={'/credential'}>
|
||||
授权凭证
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item key="asset" icon={<DesktopOutlined/>}>
|
||||
<Link to={'/asset'}>
|
||||
资产列表
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="idcard" icon={<IdcardOutlined/>}>
|
||||
<Link to={'/credential'}>
|
||||
授权凭证
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
</SubMenu>
|
||||
|
||||
<SubMenu key='command-manage' title='指令管理' icon={<CodeOutlined/>}>
|
||||
@ -183,15 +186,21 @@ class App extends Component {
|
||||
历史会话
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item key="login-log" icon={<LoginOutlined/>}>
|
||||
<Link to={'/login-log'}>
|
||||
登录日志
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
</SubMenu>
|
||||
|
||||
<SubMenu key='user-group' title='用户管理' icon={<UserSwitchOutlined />}>
|
||||
<SubMenu key='user-group' title='用户管理' icon={<UserSwitchOutlined/>}>
|
||||
<Menu.Item key="user" icon={<UserOutlined/>}>
|
||||
<Link to={'/user'}>
|
||||
用户管理
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="user-group" icon={<TeamOutlined />}>
|
||||
<Menu.Item key="user-group" icon={<TeamOutlined/>}>
|
||||
<Link to={'/user-group'}>
|
||||
用户组管理
|
||||
</Link>
|
||||
@ -243,6 +252,7 @@ class App extends Component {
|
||||
<Route path="/batch-command" component={BatchCommand}/>
|
||||
<Route path="/online-session" component={OnlineSession}/>
|
||||
<Route path="/offline-session" component={OfflineSession}/>
|
||||
<Route path="/login-log" component={LoginLog}/>
|
||||
<Route path="/info" component={Info}/>
|
||||
<Route path="/setting" component={Setting}/>
|
||||
|
||||
|
399
web/src/components/session/LoginLog.js
Normal file
399
web/src/components/session/LoginLog.js
Normal file
@ -0,0 +1,399 @@
|
||||
import React, {Component} from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Divider,
|
||||
Input,
|
||||
Layout,
|
||||
Modal,
|
||||
notification,
|
||||
PageHeader,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from "antd";
|
||||
import qs from "qs";
|
||||
import request from "../../common/request";
|
||||
import {formatDate, isEmpty, itemRender} from "../../utils/utils";
|
||||
import {message} from "antd/es";
|
||||
import {DeleteOutlined, ExclamationCircleOutlined, SyncOutlined, UndoOutlined} from "@ant-design/icons";
|
||||
import Logout from "../user/Logout";
|
||||
|
||||
const confirm = Modal.confirm;
|
||||
const {Content} = Layout;
|
||||
const {Search} = Input;
|
||||
const {Title, Text} = Typography;
|
||||
const routes = [
|
||||
{
|
||||
path: '',
|
||||
breadcrumbName: '首页',
|
||||
},
|
||||
{
|
||||
path: 'loginLog',
|
||||
breadcrumbName: '登录日志',
|
||||
}
|
||||
];
|
||||
|
||||
class LoginLog extends Component {
|
||||
|
||||
inputRefOfClientIp = React.createRef();
|
||||
|
||||
state = {
|
||||
items: [],
|
||||
total: 0,
|
||||
queryParams: {
|
||||
pageIndex: 1,
|
||||
pageSize: 10,
|
||||
userId: undefined,
|
||||
},
|
||||
loading: false,
|
||||
selectedRowKeys: [],
|
||||
delBtnLoading: false,
|
||||
users: [],
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.loadTableData();
|
||||
this.handleSearchByNickname('');
|
||||
}
|
||||
|
||||
async loadTableData(queryParams) {
|
||||
queryParams = queryParams || this.state.queryParams;
|
||||
|
||||
this.setState({
|
||||
queryParams: queryParams,
|
||||
loading: true
|
||||
});
|
||||
|
||||
// queryParams
|
||||
let paramsStr = qs.stringify(queryParams);
|
||||
|
||||
let data = {
|
||||
items: [],
|
||||
total: 0
|
||||
};
|
||||
|
||||
try {
|
||||
let result = await request.get('/login-logs/paging?' + paramsStr);
|
||||
if (result.code === 1) {
|
||||
data = result.data;
|
||||
} else {
|
||||
message.error(result.message);
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
} finally {
|
||||
const items = data.items.map(item => {
|
||||
return {'key': item['id'], ...item}
|
||||
})
|
||||
this.setState({
|
||||
items: items,
|
||||
total: data.total,
|
||||
queryParams: queryParams,
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleChangPage = (pageIndex, pageSize) => {
|
||||
let queryParams = this.state.queryParams;
|
||||
queryParams.pageIndex = pageIndex;
|
||||
queryParams.pageSize = pageSize;
|
||||
|
||||
this.setState({
|
||||
queryParams: queryParams
|
||||
});
|
||||
|
||||
this.loadTableData(queryParams)
|
||||
};
|
||||
|
||||
handleSearchByClientIp = clientIp => {
|
||||
let query = {
|
||||
...this.state.queryParams,
|
||||
'pageIndex': 1,
|
||||
'pageSize': this.state.queryParams.pageSize,
|
||||
'clientIp': clientIp,
|
||||
}
|
||||
this.loadTableData(query);
|
||||
}
|
||||
|
||||
handleChangeByProtocol = protocol => {
|
||||
let query = {
|
||||
...this.state.queryParams,
|
||||
'pageIndex': 1,
|
||||
'pageSize': this.state.queryParams.pageSize,
|
||||
'protocol': protocol,
|
||||
}
|
||||
this.loadTableData(query);
|
||||
}
|
||||
|
||||
handleSearchByNickname = async nickname => {
|
||||
const result = await request.get(`/users/paging?pageIndex=1&pageSize=1000&nickname=${nickname}`);
|
||||
if (result.code !== 1) {
|
||||
message.error(result.message, 10);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
users: result.data.items
|
||||
})
|
||||
}
|
||||
|
||||
handleChangeByUserId = userId => {
|
||||
let query = {
|
||||
...this.state.queryParams,
|
||||
'pageIndex': 1,
|
||||
'pageSize': this.state.queryParams.pageSize,
|
||||
'userId': userId,
|
||||
}
|
||||
this.loadTableData(query);
|
||||
}
|
||||
|
||||
batchDelete = async () => {
|
||||
this.setState({
|
||||
delBtnLoading: true
|
||||
})
|
||||
try {
|
||||
let result = await request.delete('/login-logs/' + this.state.selectedRowKeys.join(','));
|
||||
if (result.code === 1) {
|
||||
message.success('操作成功', 3);
|
||||
this.setState({
|
||||
selectedRowKeys: []
|
||||
})
|
||||
await this.loadTableData(this.state.queryParams);
|
||||
} else {
|
||||
message.error('删除失败 :( ' + result.message, 10);
|
||||
}
|
||||
} finally {
|
||||
this.setState({
|
||||
delBtnLoading: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const columns = [{
|
||||
title: '序号',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
render: (id, record, index) => {
|
||||
return index + 1;
|
||||
}
|
||||
}, {
|
||||
title: '用户昵称',
|
||||
dataIndex: 'userName',
|
||||
key: 'userName'
|
||||
}, {
|
||||
title: '来源IP',
|
||||
dataIndex: 'clientIp',
|
||||
key: 'clientIp'
|
||||
}, {
|
||||
title: '浏览器',
|
||||
dataIndex: 'clientUserAgent',
|
||||
key: 'clientUserAgent',
|
||||
render: (text, record) => {
|
||||
if (isEmpty(text)) {
|
||||
return '未知';
|
||||
}
|
||||
return (
|
||||
<Tooltip placement="topLeft" title={text}>
|
||||
{text.split(' ')[0]}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
}, {
|
||||
title: '登录时间',
|
||||
dataIndex: 'loginTime',
|
||||
key: 'loginTime',
|
||||
render: (text, record) => {
|
||||
|
||||
return formatDate(text, 'yyyy-MM-dd hh:mm:ss');
|
||||
}
|
||||
}, {
|
||||
title: '注销时间',
|
||||
dataIndex: 'logoutTime',
|
||||
key: 'logoutTime',
|
||||
render: (text, record) => {
|
||||
if (isEmpty(text) || text === '0001-01-01 00:00:00') {
|
||||
return '';
|
||||
}
|
||||
return text;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<div>
|
||||
<Button type="link" size='small' onClick={() => {
|
||||
confirm({
|
||||
title: '您确定要删除此条登录日志吗?',
|
||||
content: '删除用户未注销的登录日志将会强制用户下线',
|
||||
okText: '确定',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
onOk() {
|
||||
del(record.id)
|
||||
}
|
||||
});
|
||||
|
||||
const del = async (id) => {
|
||||
const result = await request.delete(`/login-logs/${id}`);
|
||||
if (result.code === 1) {
|
||||
notification['success']({
|
||||
message: '提示',
|
||||
description: '删除成功',
|
||||
});
|
||||
this.loadTableData();
|
||||
} else {
|
||||
notification['error']({
|
||||
message: '提示',
|
||||
description: '删除失败 :( ' + result.message,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}}>删除</Button>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
];
|
||||
|
||||
const selectedRowKeys = this.state.selectedRowKeys;
|
||||
const rowSelection = {
|
||||
selectedRowKeys: this.state.selectedRowKeys,
|
||||
onChange: (selectedRowKeys, selectedRows) => {
|
||||
this.setState({selectedRowKeys});
|
||||
},
|
||||
};
|
||||
const hasSelected = selectedRowKeys.length > 0;
|
||||
|
||||
const userOptions = this.state.users.map(d => <Select.Option key={d.id}
|
||||
value={d.id}>{d.nickname}</Select.Option>);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
className="site-page-header-ghost-wrapper page-herder"
|
||||
title="登录日志"
|
||||
breadcrumb={{
|
||||
routes: routes,
|
||||
itemRender: itemRender
|
||||
}}
|
||||
extra={[
|
||||
<Logout key='logout'/>
|
||||
]}
|
||||
subTitle="只有登录成功的才会保存日志"
|
||||
>
|
||||
</PageHeader>
|
||||
|
||||
<Content className="site-layout-background page-content">
|
||||
<div style={{marginBottom: 20}}>
|
||||
<Row justify="space-around" align="middle" gutter={24}>
|
||||
<Col span={8} key={1}>
|
||||
<Title level={3}>登录日志列表</Title>
|
||||
</Col>
|
||||
<Col span={16} key={2} style={{textAlign: 'right'}}>
|
||||
<Space>
|
||||
|
||||
<Search
|
||||
ref={this.inputRefOfClientIp}
|
||||
placeholder="来源IP"
|
||||
allowClear
|
||||
onSearch={this.handleSearchByClientIp}
|
||||
/>
|
||||
|
||||
<Select
|
||||
style={{width: 150}}
|
||||
showSearch
|
||||
value={this.state.queryParams.userId}
|
||||
placeholder='用户昵称'
|
||||
onSearch={this.handleSearchByNickname}
|
||||
onChange={this.handleChangeByUserId}
|
||||
filterOption={false}
|
||||
>
|
||||
{userOptions}
|
||||
</Select>
|
||||
|
||||
<Tooltip title='重置查询'>
|
||||
|
||||
<Button icon={<UndoOutlined/>} onClick={() => {
|
||||
this.inputRefOfClientIp.current.setValue('');
|
||||
this.loadTableData({
|
||||
pageIndex: 1,
|
||||
pageSize: 10,
|
||||
protocol: '',
|
||||
userId: undefined,
|
||||
assetId: undefined
|
||||
})
|
||||
}}>
|
||||
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Divider type="vertical"/>
|
||||
|
||||
<Tooltip title="刷新列表">
|
||||
<Button icon={<SyncOutlined/>} onClick={() => {
|
||||
this.loadTableData(this.state.queryParams)
|
||||
}}>
|
||||
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="批量删除">
|
||||
<Button type="primary" danger disabled={!hasSelected} icon={<DeleteOutlined/>}
|
||||
loading={this.state.delBtnLoading}
|
||||
onClick={() => {
|
||||
const content = <div>
|
||||
您确定要删除选中的<Text style={{color: '#1890FF'}}
|
||||
strong>{this.state.selectedRowKeys.length}</Text>条记录吗?
|
||||
</div>;
|
||||
confirm({
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
content: content,
|
||||
onOk: () => {
|
||||
this.batchDelete()
|
||||
},
|
||||
onCancel() {
|
||||
|
||||
},
|
||||
});
|
||||
}}>
|
||||
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<Table rowSelection={rowSelection}
|
||||
dataSource={this.state.items}
|
||||
columns={columns}
|
||||
position={'both'}
|
||||
pagination={{
|
||||
showSizeChanger: true,
|
||||
current: this.state.queryParams.pageIndex,
|
||||
pageSize: this.state.queryParams.pageSize,
|
||||
onChange: this.handleChangPage,
|
||||
total: this.state.total,
|
||||
showTotal: total => `总计 ${total} 条`
|
||||
}}
|
||||
loading={this.state.loading}
|
||||
/>
|
||||
</Content>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LoginLog;
|
@ -474,20 +474,24 @@ class OfflineSession extends Component {
|
||||
loading={this.state.loading}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
className='monitor'
|
||||
title="会话回放"
|
||||
centered
|
||||
visible={this.state.playbackVisible}
|
||||
onCancel={this.hidePlayback}
|
||||
{
|
||||
this.state.playbackVisible ?
|
||||
<Modal
|
||||
className='monitor'
|
||||
title="会话回放"
|
||||
centered
|
||||
visible={this.state.playbackVisible}
|
||||
onCancel={this.hidePlayback}
|
||||
|
||||
width={window.innerWidth * 0.8}
|
||||
footer={null}
|
||||
destroyOnClose
|
||||
maskClosable={false}
|
||||
>
|
||||
<Playback sessionId={this.state.playbackSessionId}/>
|
||||
</Modal> : undefined
|
||||
}
|
||||
|
||||
width={window.innerWidth * 0.8}
|
||||
footer={null}
|
||||
destroyOnClose
|
||||
maskClosable={false}
|
||||
>
|
||||
<Playback sessionId={this.state.playbackSessionId}/>
|
||||
</Modal>
|
||||
</Content>
|
||||
</>
|
||||
);
|
||||
|
@ -142,7 +142,9 @@ class Playback extends Component {
|
||||
|
||||
this.setState({
|
||||
recording: recording
|
||||
})
|
||||
}, () => {
|
||||
this.handlePlayPause();
|
||||
});
|
||||
}
|
||||
|
||||
handlePlayPause = () => {
|
||||
@ -150,7 +152,6 @@ class Playback extends Component {
|
||||
if (recording) {
|
||||
if (this.state.percent === this.state.max) {
|
||||
// 重播
|
||||
console.log('重新播放')
|
||||
this.setState({
|
||||
percent: 0
|
||||
}, () => {
|
||||
|
Loading…
Reference in New Issue
Block a user