1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-01-31 14:08:06 +08:00

feat: 增加 mfa 设置

This commit is contained in:
ssongliu 2022-09-14 23:27:17 +08:00 committed by ssongliu
parent d28697b63a
commit e1bf00cb34
23 changed files with 549 additions and 234 deletions

View File

@ -1,10 +1,13 @@
package v1 package v1
import ( import (
"errors"
"github.com/1Panel-dev/1Panel/app/api/v1/helper" "github.com/1Panel-dev/1Panel/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/app/dto" "github.com/1Panel-dev/1Panel/app/dto"
"github.com/1Panel-dev/1Panel/constant" "github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/global" "github.com/1Panel-dev/1Panel/global"
"github.com/1Panel-dev/1Panel/utils/mfa"
"github.com/1Panel-dev/1Panel/utils/ntp" "github.com/1Panel-dev/1Panel/utils/ntp"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -70,3 +73,55 @@ func (b *BaseApi) SyncTime(c *gin.Context) {
helper.SuccessWithData(c, ts) helper.SuccessWithData(c, ts)
} }
func (b *BaseApi) CleanMonitor(c *gin.Context) {
if err := global.DB.Exec("DELETE FROM monitor_bases").Error; err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
if err := global.DB.Exec("DELETE FROM monitor_ios").Error; err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
if err := global.DB.Exec("DELETE FROM monitor_networks").Error; err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) GetMFA(c *gin.Context) {
otp, err := mfa.GetOtp("admin")
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, otp)
}
func (b *BaseApi) MFABind(c *gin.Context) {
var req dto.MfaCredential
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
success := mfa.ValidCode(req.Code, req.Secret)
if !success {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, errors.New("code is not valid"))
return
}
if err := settingService.Update(c, "MFAStatus", "enable"); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
if err := settingService.Update(c, "MFASecret", req.Secret); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}

View File

@ -16,6 +16,7 @@ type SettingInfo struct {
PasswordTimeOut string `json:"passwordTimeOut"` PasswordTimeOut string `json:"passwordTimeOut"`
ComplexityVerification string `json:"complexityVerification"` ComplexityVerification string `json:"complexityVerification"`
MFAStatus string `json:"mfaStatus"` MFAStatus string `json:"mfaStatus"`
MFASecret string `json:"mfaSecret"`
MonitorStatus string `json:"monitorStatus"` MonitorStatus string `json:"monitorStatus"`
MonitorStoreDays string `json:"monitorStoreDays"` MonitorStoreDays string `json:"monitorStoreDays"`

View File

@ -9,3 +9,8 @@ type UserLoginInfo struct {
Name string `json:"name"` Name string `json:"name"`
Token string `json:"token"` Token string `json:"token"`
} }
type MfaCredential struct {
Secret string `json:"secret"`
Code string `json:"code"`
}

View File

@ -1,9 +1,7 @@
package model package model
import "gorm.io/gorm"
type Setting struct { type Setting struct {
gorm.Model BaseModel
Key string `json:"key" gorm:"type:varchar(256);not null;"` Key string `json:"key" gorm:"type:varchar(256);not null;"`
Value string `json:"value" gorm:"type:varchar(256)"` Value string `json:"value" gorm:"type:varchar(256)"`
About string `json:"about" gorm:"type:longText"` About string `json:"about" gorm:"type:longText"`

View File

@ -1,9 +1,11 @@
package job package job
import ( import (
"strconv"
"time" "time"
"github.com/1Panel-dev/1Panel/app/model" "github.com/1Panel-dev/1Panel/app/model"
"github.com/1Panel-dev/1Panel/app/repo"
"github.com/1Panel-dev/1Panel/global" "github.com/1Panel-dev/1Panel/global"
"github.com/shirou/gopsutil/cpu" "github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/disk" "github.com/shirou/gopsutil/disk"
@ -19,6 +21,11 @@ func NewMonitorJob() *monitor {
} }
func (m *monitor) Run() { func (m *monitor) Run() {
settingRepo := repo.NewISettingRepo()
monitorStatus, _ := settingRepo.Get(settingRepo.WithByKey("MonitorStatus"))
if monitorStatus.Value == "disable" {
return
}
var itemModel model.MonitorBase var itemModel model.MonitorBase
totalPercent, _ := cpu.Percent(3*time.Second, false) totalPercent, _ := cpu.Percent(3*time.Second, false)
if len(totalPercent) == 1 { if len(totalPercent) == 1 {
@ -35,7 +42,9 @@ func (m *monitor) Run() {
memoryInfo, _ := mem.VirtualMemory() memoryInfo, _ := mem.VirtualMemory()
itemModel.Memory = memoryInfo.UsedPercent itemModel.Memory = memoryInfo.UsedPercent
_ = global.DB.Create(&itemModel) if err := global.DB.Create(&itemModel).Error; err != nil {
global.LOG.Debug("create monitor base failed, err: %v", err)
}
ioStat, _ := disk.IOCounters() ioStat, _ := disk.IOCounters()
for _, v := range ioStat { for _, v := range ioStat {
@ -67,7 +76,9 @@ func (m *monitor) Run() {
if writeTime > itemIO.Time { if writeTime > itemIO.Time {
itemIO.Time = writeTime itemIO.Time = writeTime
} }
_ = global.DB.Create(&itemIO) if err := global.DB.Create(&itemIO).Error; err != nil {
global.LOG.Debug("create monitor io failed, err: %v", err)
}
} }
netStat, _ := net.IOCounters(true) netStat, _ := net.IOCounters(true)
@ -84,7 +95,9 @@ func (m *monitor) Run() {
stime := time.Since(aheadData.CreatedAt).Seconds() stime := time.Since(aheadData.CreatedAt).Seconds()
itemNet.Up = float64(v.BytesSent-aheadData.BytesSent) / 1024 / stime itemNet.Up = float64(v.BytesSent-aheadData.BytesSent) / 1024 / stime
itemNet.Down = float64(v.BytesRecv-aheadData.BytesRecv) / 1024 / stime itemNet.Down = float64(v.BytesRecv-aheadData.BytesRecv) / 1024 / stime
_ = global.DB.Create(&itemNet) if err := global.DB.Create(&itemNet).Error; err != nil {
global.LOG.Debug("create monitor network failed, err: %v", err)
}
} }
netStatAll, _ := net.IOCounters(false) netStatAll, _ := net.IOCounters(false)
if len(netStatAll) != 0 { if len(netStatAll) != 0 {
@ -100,6 +113,18 @@ func (m *monitor) Run() {
stime := time.Since(aheadData.CreatedAt).Seconds() stime := time.Since(aheadData.CreatedAt).Seconds()
itemNet.Up = float64(netStatAll[0].BytesSent-aheadData.BytesSent) / 1024 / stime itemNet.Up = float64(netStatAll[0].BytesSent-aheadData.BytesSent) / 1024 / stime
itemNet.Down = float64(netStatAll[0].BytesRecv-aheadData.BytesRecv) / 1024 / stime itemNet.Down = float64(netStatAll[0].BytesRecv-aheadData.BytesRecv) / 1024 / stime
_ = global.DB.Create(&itemNet) if err := global.DB.Create(&itemNet).Error; err != nil {
global.LOG.Debug("create monitor network all failed, err: %v", err)
}
} }
MonitorStoreDays, err := settingRepo.Get(settingRepo.WithByKey("MonitorStoreDays"))
if err != nil {
return
}
storeDays, _ := strconv.Atoi(MonitorStoreDays.Value)
timeForDelete := time.Now().AddDate(0, 0, -storeDays)
_ = global.DB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorBase{}).Error
_ = global.DB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorIO{}).Error
_ = global.DB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorNetwork{}).Error
} }

View File

@ -27,11 +27,13 @@ require (
github.com/satori/go.uuid v1.2.0 github.com/satori/go.uuid v1.2.0
github.com/shirou/gopsutil v3.21.11+incompatible github.com/shirou/gopsutil v3.21.11+incompatible
github.com/sirupsen/logrus v1.9.0 github.com/sirupsen/logrus v1.9.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/spf13/afero v1.8.2 github.com/spf13/afero v1.8.2
github.com/spf13/viper v1.12.0 github.com/spf13/viper v1.12.0
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a
github.com/swaggo/gin-swagger v1.5.1 github.com/swaggo/gin-swagger v1.5.1
github.com/swaggo/swag v1.8.4 github.com/swaggo/swag v1.8.4
github.com/xlzd/gotp v0.0.0-20220817083547-a63b9d03d72f
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
golang.org/x/text v0.3.7 golang.org/x/text v0.3.7
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0

View File

@ -371,6 +371,8 @@ github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMT
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
@ -430,6 +432,8 @@ github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4A
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/xlzd/gotp v0.0.0-20220817083547-a63b9d03d72f h1:C8De+7emQKojPBC+mXA0fr39XN5mKjRm9IUzdxI4whI=
github.com/xlzd/gotp v0.0.0-20220817083547-a63b9d03d72f/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

View File

@ -93,6 +93,9 @@ var AddTableSetting = &gormigrate.Migration{
if err := tx.Create(&model.Setting{Key: "MFAStatus", Value: "disable"}).Error; err != nil { if err := tx.Create(&model.Setting{Key: "MFAStatus", Value: "disable"}).Error; err != nil {
return err return err
} }
if err := tx.Create(&model.Setting{Key: "MFASecret", Value: ""}).Error; err != nil {
return err
}
if err := tx.Create(&model.Setting{Key: "MonitorStatus", Value: "enable"}).Error; err != nil { if err := tx.Create(&model.Setting{Key: "MonitorStatus", Value: "enable"}).Error; err != nil {
return err return err

View File

@ -17,5 +17,8 @@ func (s *SettingRouter) InitSettingRouter(Router *gin.RouterGroup) {
withRecordRouter.PUT("", baseApi.UpdateSetting) withRecordRouter.PUT("", baseApi.UpdateSetting)
settingRouter.PUT("/password", baseApi.UpdatePassword) settingRouter.PUT("/password", baseApi.UpdatePassword)
settingRouter.POST("/time/sync", baseApi.SyncTime) settingRouter.POST("/time/sync", baseApi.SyncTime)
settingRouter.POST("/monitor/clean", baseApi.CleanMonitor)
settingRouter.GET("/mfa", baseApi.GetMFA)
settingRouter.POST("/mfa/bind", baseApi.MFABind)
} }
} }

40
backend/utils/mfa/mfa.go Normal file
View File

@ -0,0 +1,40 @@
package mfa
import (
"bytes"
"encoding/base64"
"strconv"
"time"
"github.com/skip2/go-qrcode"
"github.com/xlzd/gotp"
)
const secretLength = 16
type Otp struct {
Secret string `json:"secret"`
QrImage string `json:"qrImage"`
}
func GetOtp(username string) (otp Otp, err error) {
secret := gotp.RandomSecret(secretLength)
otp.Secret = secret
totp := gotp.NewDefaultTOTP(secret)
uri := totp.ProvisioningUri(username, "1Panel")
subImg, err := qrcode.Encode(uri, qrcode.Medium, 256)
dist := make([]byte, 3000)
base64.StdEncoding.Encode(dist, subImg)
index := bytes.IndexByte(dist, 0)
baseImage := dist[0:index]
otp.QrImage = "data:image/png;base64," + string(baseImage)
return
}
func ValidCode(code string, secret string) bool {
totp := gotp.NewDefaultTOTP(secret)
now := time.Now().Unix()
strInt64 := strconv.FormatInt(now, 10)
id16, _ := strconv.Atoi(strInt64)
return totp.Verify(code, int64(id16))
}

View File

@ -16,6 +16,7 @@ export namespace Setting {
passwordTimeOut: string; passwordTimeOut: string;
complexityVerification: string; complexityVerification: string;
mfaStatus: string; mfaStatus: string;
mfaSecret: string;
monitorStatus: string; monitorStatus: string;
monitorStoreDays: string; monitorStoreDays: string;
@ -34,4 +35,12 @@ export namespace Setting {
newPassword: string; newPassword: string;
retryPassword: string; retryPassword: string;
} }
export interface MFAInfo {
secret: string;
qrImage: string;
}
export interface MFABind {
secret: string;
code: string;
}
} }

View File

@ -16,3 +16,15 @@ export const updatePassword = (param: Setting.PasswordUpdate) => {
export const syncTime = () => { export const syncTime = () => {
return http.post(`/settings/time/sync`, {}); return http.post(`/settings/time/sync`, {});
}; };
export const cleanMonitors = () => {
return http.post(`/settings/monitor/clean`, {});
};
export const getMFA = () => {
return http.get<Setting.MFAInfo>(`/settings/mfa`, {});
};
export const bindMFA = (param: Setting.MFABind) => {
return http.post(`/settings/mfa/bind`, param);
};

View File

@ -20,6 +20,7 @@ interface CommonRule {
requiredSelect: FormItemRule; requiredSelect: FormItemRule;
name: FormItemRule; name: FormItemRule;
email: FormItemRule; email: FormItemRule;
number: FormItemRule;
ip: FormItemRule; ip: FormItemRule;
port: FormItemRule; port: FormItemRule;
} }
@ -48,6 +49,13 @@ export const Rules: CommonRule = {
message: i18n.global.t('commons.rule.email'), message: i18n.global.t('commons.rule.email'),
trigger: 'blur', trigger: 'blur',
}, },
number: {
required: true,
trigger: 'blur',
min: 0,
type: 'number',
message: i18n.global.t('commons.rule.number'),
},
ip: { ip: {
validator: checkIp, validator: checkIp,
required: true, required: true,

View File

@ -11,8 +11,11 @@ export default {
confirm: 'Confirm', confirm: 'Confirm',
cancel: 'Cancel', cancel: 'Cancel',
reset: 'Reset', reset: 'Reset',
login: 'Login',
conn: 'Connect', conn: 'Connect',
clean: 'Clean',
login: 'Login',
close: 'Close',
saveAndEnable: 'Save and enable',
}, },
search: { search: {
timeStart: 'Time start', timeStart: 'Time start',
@ -268,5 +271,35 @@ export default {
oldPassword: 'Original password', oldPassword: 'Original password',
newPassword: 'New password', newPassword: 'New password',
retryPassword: 'Confirm password', retryPassword: 'Confirm password',
safe: 'Safe',
panelPort: 'Panel port',
portHelper:
'The recommended port range is 8888 to 65535. Note: If the server has a security group, permit the new port from the security group in advance',
safeEntrance: 'Security entrance',
safeEntranceHelper:
'Panel management portal. You can log in to the panel only through a specified security portal, for example, / 89DC6AE8',
passwordTimeout: 'Password expiration Time',
timeoutHelper:
'[ {0} days ] The panel password is about to expire. After the expiration, you need to reset the password',
complexity: 'Password complexity verification',
complexityHelper:
'The password must contain at least eight characters and contain at least three uppercase letters, lowercase letters, digits, and special characters',
mfa: 'MFA',
mfaHelper1: 'Download a MFA verification mobile app such as:',
mfaHelper2: 'Scan the following QR code using the mobile app to obtain the 6-digit verification code',
mfaHelper3: 'Enter six digits from the app',
enableMonitor: 'Enable',
storeDays: 'Expiration time (day)',
cleanMonitor: 'Clearing monitoring records',
message: 'Message',
messageType: 'Message type',
email: 'Email',
wechat: 'WeChat',
dingding: 'DingDing',
closeMessage: 'Turning off Message Notification',
emailServer: 'Service name',
emailAddr: 'Service address',
emailSMTP: 'SMTP code',
secret: 'Secret',
}, },
}; };

View File

@ -8,11 +8,16 @@ export default {
sync: '同步', sync: '同步',
delete: '删除', delete: '删除',
edit: '编辑', edit: '编辑',
enable: '启用',
disable: '禁用',
confirm: '确认', confirm: '确认',
cancel: '取消', cancel: '取消',
reset: '重置', reset: '重置',
conn: '连接', conn: '连接',
clean: '清空',
login: '登录', login: '登录',
close: '关闭',
saveAndEnable: '保存并启用',
}, },
search: { search: {
timeStart: '开始时间', timeStart: '开始时间',
@ -64,7 +69,8 @@ export default {
requiredInput: '请填写必填项', requiredInput: '请填写必填项',
requiredSelect: '请选择必选项', requiredSelect: '请选择必选项',
commonName: '支持英文中文数字.-_,长度1-30', commonName: '支持英文中文数字.-_,长度1-30',
email: '邮箱格式错误', email: '请输入正确的邮箱',
number: '请输入正确的数字',
ip: '请输入正确的 IP 地址', ip: '请输入正确的 IP 地址',
port: '请输入正确的端口', port: '请输入正确的端口',
}, },
@ -277,5 +283,31 @@ export default {
oldPassword: '原密码', oldPassword: '原密码',
newPassword: '新密码', newPassword: '新密码',
retryPassword: '确认密码', retryPassword: '确认密码',
safe: '安全',
panelPort: '面板端口',
portHelper: '建议端口范围8888 - 65535注意有安全组的服务器请提前在安全组放行新端口',
safeEntrance: '安全入口',
safeEntranceHelper: '面板管理入口设置后只能通过指定安全入口登录面板,: /89dc6ae8',
passwordTimeout: '密码过期时间',
timeoutHelper: ' {0} 天后 面板密码即将过期过期后需要重新设置密码',
complexity: '密码复杂度验证',
complexityHelper: '密码必须满足密码长度大于8位且大写字母小写字母数字特殊字符至少3项组合',
mfa: '两步验证',
mfaHelper1: '下载两步验证手机应用 :',
mfaHelper2: '使用手机应用扫描以下二维码获取 6 位验证码',
mfaHelper3: '输入手机应用上的 6 位数字',
enableMonitor: '监控状态',
storeDays: '过期时间 ()',
cleanMonitor: '清空监控记录',
message: '通知',
messageType: '通知方式',
email: '邮箱',
wechat: '企业微信',
dingding: '钉钉',
closeMessage: '关闭消息通知',
emailServer: '邮箱服务名称',
emailAddr: '邮箱地址',
emailSMTP: '邮箱 SMTP 授权码',
secret: '密钥',
}, },
}; };

View File

@ -29,7 +29,7 @@ const hostRouter = {
{ {
path: '/hosts/monitor', path: '/hosts/monitor',
name: 'Monitor', name: 'Monitor',
component: () => import('@/views/monitor/index.vue'), component: () => import('@/views/host/monitor/index.vue'),
meta: { meta: {
title: 'menu.monitor', title: 'menu.monitor',
}, },

View File

@ -9,7 +9,6 @@
@change="search('load')" @change="search('load')"
v-model="timeRangeLoad" v-model="timeRangeLoad"
type="datetimerange" type="datetimerange"
size="small"
:range-separator="$t('commons.search.timeRange')" :range-separator="$t('commons.search.timeRange')"
:start-placeholder="$t('commons.search.timeStart')" :start-placeholder="$t('commons.search.timeStart')"
:end-placeholder="$t('commons.search.timeEnd')" :end-placeholder="$t('commons.search.timeEnd')"
@ -30,7 +29,6 @@
@change="search('cpu')" @change="search('cpu')"
v-model="timeRangeCpu" v-model="timeRangeCpu"
type="datetimerange" type="datetimerange"
size="small"
:range-separator="$t('commons.search.timeRange')" :range-separator="$t('commons.search.timeRange')"
:start-placeholder="$t('commons.search.timeStart')" :start-placeholder="$t('commons.search.timeStart')"
:end-placeholder="$t('commons.search.timeEnd')" :end-placeholder="$t('commons.search.timeEnd')"
@ -49,7 +47,6 @@
@change="search('memory')" @change="search('memory')"
v-model="timeRangeMemory" v-model="timeRangeMemory"
type="datetimerange" type="datetimerange"
size="small"
:range-separator="$t('commons.search.timeRange')" :range-separator="$t('commons.search.timeRange')"
:start-placeholder="$t('commons.search.timeStart')" :start-placeholder="$t('commons.search.timeStart')"
:end-placeholder="$t('commons.search.timeEnd')" :end-placeholder="$t('commons.search.timeEnd')"
@ -70,7 +67,6 @@
@change="search('io')" @change="search('io')"
v-model="timeRangeIO" v-model="timeRangeIO"
type="datetimerange" type="datetimerange"
size="small"
:range-separator="$t('commons.search.timeRange')" :range-separator="$t('commons.search.timeRange')"
:start-placeholder="$t('commons.search.timeStart')" :start-placeholder="$t('commons.search.timeStart')"
:end-placeholder="$t('commons.search.timeEnd')" :end-placeholder="$t('commons.search.timeEnd')"
@ -92,7 +88,6 @@
@change="search('network')" @change="search('network')"
style="margin-left: 20px" style="margin-left: 20px"
placeholder="Select" placeholder="Select"
size="small"
> >
<el-option v-for="item in netOptions" :key="item" :label="item" :value="item" /> <el-option v-for="item in netOptions" :key="item" :label="item" :value="item" />
</el-select> </el-select>
@ -100,7 +95,6 @@
@change="search('network')" @change="search('network')"
v-model="timeRangeNetwork" v-model="timeRangeNetwork"
type="datetimerange" type="datetimerange"
size="small"
:range-separator="$t('commons.search.timeRange')" :range-separator="$t('commons.search.timeRange')"
:start-placeholder="$t('commons.search.timeStart')" :start-placeholder="$t('commons.search.timeStart')"
:end-placeholder="$t('commons.search.timeEnd')" :end-placeholder="$t('commons.search.timeEnd')"

View File

@ -3,33 +3,21 @@
<el-col :span="8"> <el-col :span="8">
<el-card class="el-card"> <el-card class="el-card">
<el-tooltip class="box-item" effect="dark" :content="$t('terminal.createConn')" placement="top-start"> <el-tooltip class="box-item" effect="dark" :content="$t('terminal.createConn')" placement="top-start">
<el-button icon="Plus" @click="restHostForm" size="small" /> <el-button icon="Plus" @click="restHostForm" />
</el-tooltip> </el-tooltip>
<el-tooltip class="box-item" effect="dark" :content="$t('terminal.createGroup')" placement="top-start"> <el-tooltip class="box-item" effect="dark" :content="$t('terminal.createGroup')" placement="top-start">
<el-button icon="FolderAdd" @click="onGroupCreate" size="small" /> <el-button icon="FolderAdd" @click="onGroupCreate" />
</el-tooltip> </el-tooltip>
<el-tooltip class="box-item" effect="dark" :content="$t('terminal.expand')" placement="top-start"> <el-tooltip class="box-item" effect="dark" :content="$t('terminal.expand')" placement="top-start">
<el-button icon="Expand" @click="setTreeStatus(true)" size="small" /> <el-button icon="Expand" @click="setTreeStatus(true)" />
</el-tooltip> </el-tooltip>
<el-tooltip class="box-item" effect="dark" :content="$t('terminal.fold')" placement="top-start"> <el-tooltip class="box-item" effect="dark" :content="$t('terminal.fold')" placement="top-start">
<el-button icon="Fold" @click="setTreeStatus(false)" size="small" /> <el-button icon="Fold" @click="setTreeStatus(false)" />
</el-tooltip> </el-tooltip>
<el-input <el-input @input="loadHostTree" clearable style="margin-top: 5px" v-model="searcConfig.info">
size="small"
@input="loadHostTree"
clearable
style="margin-top: 5px"
v-model="searcConfig.info"
>
<template #append><el-button icon="search" @click="loadHostTree" /></template> <template #append><el-button icon="search" @click="loadHostTree" /></template>
</el-input> </el-input>
<el-input <el-input v-if="groupInputShow" clearable style="margin-top: 5px" v-model="groupInputValue">
size="small"
v-if="groupInputShow"
clearable
style="margin-top: 5px"
v-model="groupInputValue"
>
<template #append> <template #append>
<el-button-group> <el-button-group>
<el-button icon="Check" @click="onCreateGroup" /> <el-button icon="Check" @click="onCreateGroup" />
@ -53,8 +41,8 @@
<el-button-group <el-button-group
v-if="!(node.level === 1 && data.label === 'default') && data.id === hover" v-if="!(node.level === 1 && data.label === 'default') && data.id === hover"
> >
<el-button icon="Edit" size="small" @click="onEdit(node, data)" /> <el-button icon="Edit" @click="onEdit(node, data)" />
<el-button icon="Delete" size="small" @click="onDelete(node, data)" /> <el-button icon="Delete" @click="onDelete(node, data)" />
</el-button-group> </el-button-group>
</span> </span>
</template> </template>

View File

@ -47,7 +47,7 @@
@change="quickInput" @change="quickInput"
style="width: 25%" style="width: 25%"
:placeholder="$t('terminal.quickCommand')" :placeholder="$t('terminal.quickCommand')"
size="small"
> >
<el-option <el-option
v-for="cmd in commandList" v-for="cmd in commandList"
@ -61,10 +61,10 @@
v-model="batchVal" v-model="batchVal"
@keyup.enter="batchInput" @keyup.enter="batchInput"
style="width: 75%" style="width: 75%"
size="small"
> >
<template #append> <template #append>
<el-switch size="small" v-model="isBatch" class="ml-2" /> <el-switch v-model="isBatch" class="ml-2" />
</template> </template>
</el-input> </el-input>
</div> </div>
@ -81,7 +81,7 @@
<el-button @click="onNewSsh">New ssh</el-button> <el-button @click="onNewSsh">New ssh</el-button>
<el-button @click="onNewTab">New tab</el-button> <el-button @click="onNewTab">New tab</el-button>
</el-button-group> </el-button-group>
<el-input size="small" clearable style="margin-top: 5px" v-model="hostfilterInfo"> <el-input clearable style="margin-top: 5px" v-model="hostfilterInfo">
<template #append><el-button icon="search" /></template> <template #append><el-button icon="search" /></template>
</el-input> </el-input>
<el-tree <el-tree

View File

@ -1,70 +1,67 @@
<template> <template>
<el-form size="small" :model="form" label-position="left" label-width="120px"> <el-form :model="mesForm" label-position="left" label-width="160px">
<el-card style="margin-top: 10px"> <el-card style="margin-top: 10px">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span>通知</span> <span>{{ $t('setting.message') }}</span>
</div> </div>
</template> </template>
<el-row> <el-row>
<el-col :span="1"><br /></el-col> <el-col :span="1"><br /></el-col>
<el-col :span="8"> <el-col :span="10">
<el-form-item label="通知方式"> <el-form-item :label="$t('setting.messageType')">
<el-radio-group v-model="form.settingInfo.messageType"> <el-radio-group v-model="mesForm.messageType">
<el-radio-button label="none">关闭</el-radio-button> <el-radio-button label="none">{{ $t('commons.button.close') }}</el-radio-button>
<el-radio-button label="email">email</el-radio-button> <el-radio-button label="email">{{ $t('setting.email') }}</el-radio-button>
<el-radio-button label="wechat">企业微信</el-radio-button> <el-radio-button label="wechat">{{ $t('setting.wechat') }}</el-radio-button>
<el-radio-button label="dingding">钉钉</el-radio-button> <el-radio-button label="dingding">{{ $t('setting.dingding') }}</el-radio-button>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<div v-if="form.settingInfo.messageType === 'none'"> <div v-if="mesForm.messageType === 'none'">
<el-form-item> <el-form-item>
<el-button @click="SaveSetting()">关闭消息通知</el-button> <el-button @click="SaveSetting()">{{ $t('setting.closeMessage') }}</el-button>
</el-form-item> </el-form-item>
</div> </div>
<div v-if="form.settingInfo.messageType === 'email'"> <div v-if="mesForm.messageType === 'email'">
<el-form-item label="邮箱服务名称"> <el-form-item :label="$t('setting.emailServer')">
<el-input clearable v-model="emailVars.serverName" /> <el-input clearable v-model="mesForm.emailVars.serverName" />
</el-form-item> </el-form-item>
<el-form-item label="邮箱地址"> <el-form-item :label="$t('setting.emailAddr')">
<el-input clearable v-model="emailVars.serverAddr" /> <el-input clearable v-model="mesForm.emailVars.serverAddr" />
</el-form-item> </el-form-item>
<el-form-item label="邮箱SMTP授权码"> <el-form-item :label="$t('setting.emailSMTP')">
<el-input clearable v-model="emailVars.serverSMTP" /> <el-input clearable v-model="mesForm.emailVars.serverSMTP" />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button @click="SaveSetting()">保存并启用</el-button> <el-button @click="SaveSetting()">{{ $t('commons.button.saveAndEnable') }}</el-button>
</el-form-item> </el-form-item>
</div> </div>
<div v-if="form.settingInfo.messageType === 'wechat'"> <div v-if="mesForm.messageType === 'wechat'">
<el-form-item label="orpid"> <el-form-item label="orpid">
<el-input clearable v-model="weChatVars.orpid" /> <el-input clearable v-model="mesForm.weChatVars.orpid" />
</el-form-item> </el-form-item>
<el-form-item label="corpsecret"> <el-form-item label="corpsecret">
<el-input clearable v-model="weChatVars.corpsecret" /> <el-input clearable v-model="mesForm.weChatVars.corpsecret" />
</el-form-item> </el-form-item>
<el-form-item label="touser"> <el-form-item label="touser">
<el-input clearable v-model="weChatVars.touser" /> <el-input clearable v-model="mesForm.weChatVars.touser" />
</el-form-item> </el-form-item>
<el-form-item label="agentid"> <el-form-item label="agentid">
<el-input clearable v-model="weChatVars.agentid" /> <el-input clearable v-model="mesForm.weChatVars.agentid" />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button @click="SaveSetting()">保存并启用</el-button> <el-button @click="SaveSetting()">{{ $t('commons.button.saveAndEnable') }}</el-button>
</el-form-item> </el-form-item>
</div> </div>
<div v-if="form.settingInfo.messageType === 'dingding'"> <div v-if="mesForm.messageType === 'dingding'">
<el-form-item label="webhook token"> <el-form-item label="webhook token">
<el-input clearable v-model="dingVars.webhookToken" /> <el-input clearable v-model="mesForm.dingVars.webhookToken" />
</el-form-item> </el-form-item>
<el-form-item label="密钥"> <el-form-item :label="$t('setting.secret')">
<el-input clearable v-model="dingVars.secret" /> <el-input clearable v-model="mesForm.dingVars.secret" />
</el-form-item>
<el-form-item label="邮箱 SMTP 授权码">
<el-input clearable v-model="emailVars.serverSMTP" />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button @click="SaveSetting()">保存并启用</el-button> <el-button @click="SaveSetting()">{{ $t('commons.button.saveAndEnable') }}</el-button>
</el-form-item> </el-form-item>
</div> </div>
</el-col> </el-col>
@ -74,7 +71,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted } from 'vue'; import { reactive, watch } from 'vue';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { updateSetting } from '@/api/modules/setting'; import { updateSetting } from '@/api/modules/setting';
import i18n from '@/lang'; import i18n from '@/lang';
@ -91,39 +88,57 @@ const form = withDefaults(defineProps<Props>(), {
}, },
}); });
const emailVars = ref({ const mesForm = reactive({
serverName: '', messageType: '',
serverAddr: '', emailVars: {
serverSMTP: '', serverName: '',
serverAddr: '',
serverSMTP: '',
},
weChatVars: {
orpid: '',
corpsecret: '',
touser: '',
agentid: '',
},
dingVars: {
webhookToken: '',
secret: '',
},
}); });
const weChatVars = ref({
orpid: '', watch(form, (val: any) => {
corpsecret: '', if (val.settingInfo.messageType) {
touser: '', mesForm.messageType = form.settingInfo.messageType;
agentid: '', mesForm.emailVars = val.settingInfo.emailVars
}); ? JSON.parse(val.settingInfo.emailVars)
const dingVars = ref({ : { serverName: '', serverAddr: '', serverSMTP: '' };
webhookToken: '', mesForm.weChatVars = val.settingInfo.weChatVars
secret: '', ? JSON.parse(val.settingInfo.weChatVars)
: { orpid: '', corpsecret: '', touser: '', agentid: '' };
mesForm.dingVars = val.settingInfo.dingVars
? JSON.parse(val.settingInfo.dingVars)
: { webhookToken: '', secret: '' };
}
}); });
const SaveSetting = async () => { const SaveSetting = async () => {
let settingKey = ''; let settingKey = '';
let settingVal = ''; let settingVal = '';
switch (form.settingInfo.messageType) { switch (mesForm.messageType) {
case 'none': case 'none':
settingVal = ''; settingVal = '';
break; break;
case 'email': case 'email':
settingVal = JSON.stringify(emailVars.value); settingVal = JSON.stringify(mesForm.emailVars);
settingKey = 'EmailVars'; settingKey = 'EmailVars';
break; break;
case 'wechat': case 'wechat':
settingVal = JSON.stringify(emailVars.value); settingVal = JSON.stringify(mesForm.weChatVars);
settingKey = 'WeChatVars'; settingKey = 'WeChatVars';
break; break;
case 'dingding': case 'dingding':
settingVal = JSON.stringify(emailVars.value); settingVal = JSON.stringify(mesForm.dingVars);
settingKey = 'DingVars'; settingKey = 'DingVars';
break; break;
} }
@ -131,22 +146,8 @@ const SaveSetting = async () => {
key: settingKey, key: settingKey,
value: settingVal, value: settingVal,
}; };
await updateSetting({ key: 'MessageType', value: form.settingInfo.messageType }); await updateSetting({ key: 'MessageType', value: mesForm.messageType });
await updateSetting(param); await updateSetting(param);
ElMessage.success(i18n.global.t('commons.msg.operationSuccess')); ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
}; };
onMounted(() => {
switch (form.settingInfo.messageType) {
case 'email':
emailVars.value = JSON.parse(form.settingInfo.emailVars);
console.log(emailVars.value);
break;
case 'wechat':
weChatVars.value = JSON.parse(form.settingInfo.weChatVars);
break;
case 'dingding':
dingVars.value = JSON.parse(form.settingInfo.dingVars);
break;
}
});
</script> </script>

View File

@ -1,36 +1,37 @@
<template> <template>
<el-form size="small" :model="form" label-position="left" label-width="120px"> <el-form :model="form" label-position="left" label-width="160px">
<el-card style="margin-top: 10px"> <el-card style="margin-top: 10px">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span>监控</span> <span>{{ $t('menu.monitor') }}</span>
</div> </div>
</template> </template>
<el-row> <el-row>
<el-col :span="1"><br /></el-col> <el-col :span="1"><br /></el-col>
<el-col :span="8"> <el-col :span="10">
<el-form-item label="开启监控"> <el-form-item :label="$t('setting.enableMonitor')">
<el-switch <el-radio-group
v-model="form.settingInfo.monitorStatus"
active-value="enable"
inactive-value="disable"
@change="SaveSetting('MonitorStatus', form.settingInfo.monitorStatus)" @change="SaveSetting('MonitorStatus', form.settingInfo.monitorStatus)"
/> v-model="form.settingInfo.monitorStatus"
>
<el-radio-button label="enable">{{ $t('commons.button.enable') }}</el-radio-button>
<el-radio-button label="disable">{{ $t('commons.button.disable') }}</el-radio-button>
</el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="过期时间"> <el-form-item :label="$t('setting.storeDays')">
<el-input clearable v-model="form.settingInfo.monitorStoreDays"> <el-input clearable v-model="form.settingInfo.monitorStoreDays">
<template #append> <template #append>
<el-button <el-button
@click="SaveSetting('MonitorStoreDays', form.settingInfo.monitorStoreDays)" @click="SaveSetting('MonitorStoreDays', form.settingInfo.monitorStoreDays)"
icon="Collection" icon="Collection"
> >
保存 {{ $t('commons.button.save') }}
</el-button> </el-button>
</template> </template>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button icon="Delete">清空监控记录</el-button> <el-button @click="onClean()" icon="Delete">{{ $t('setting.cleanMonitor') }}</el-button>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
@ -40,8 +41,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { updateSetting } from '@/api/modules/setting'; import { updateSetting, cleanMonitors } from '@/api/modules/setting';
import i18n from '@/lang'; import i18n from '@/lang';
import { useDeleteData } from '@/hooks/use-delete-data';
interface Props { interface Props {
settingInfo: any; settingInfo: any;
@ -61,4 +63,8 @@ const SaveSetting = async (key: string, val: string) => {
await updateSetting(param); await updateSetting(param);
ElMessage.success(i18n.global.t('commons.msg.operationSuccess')); ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
}; };
const onClean = async () => {
await useDeleteData(cleanMonitors, {}, 'commons.msg.delete', true);
};
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<el-form size="small" :model="form" label-position="left" label-width="160px"> <el-form :model="form" label-position="left" label-width="160px">
<el-card style="margin-top: 20px"> <el-card style="margin-top: 20px">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
@ -9,7 +9,7 @@
<el-row> <el-row>
<el-col :span="1"><br /></el-col> <el-col :span="1"><br /></el-col>
<el-col :span="10"> <el-col :span="10">
<el-form-item :label="$t('auth.username')"> <el-form-item :label="$t('auth.username')" prop="settingInfo.userName">
<el-input clearable v-model="form.settingInfo.userName"> <el-input clearable v-model="form.settingInfo.userName">
<template #append> <template #append>
<el-button <el-button
@ -21,7 +21,7 @@
</template> </template>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item :label="$t('auth.password')"> <el-form-item :label="$t('auth.password')" prop="settingInfo.password">
<el-input type="password" clearable disabled v-model="form.settingInfo.password"> <el-input type="password" clearable disabled v-model="form.settingInfo.password">
<template #append> <template #append>
<el-button icon="Setting" @click="passwordVisiable = true"> <el-button icon="Setting" @click="passwordVisiable = true">
@ -30,7 +30,7 @@
</template> </template>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item :label="$t('auth.email')"> <el-form-item :label="$t('auth.email')" prop="settingInfo.email">
<el-input clearable v-model="form.settingInfo.email"> <el-input clearable v-model="form.settingInfo.email">
<template #append> <template #append>
<el-button @click="SaveSetting('Email', form.settingInfo.email)" icon="Collection"> <el-button @click="SaveSetting('Email', form.settingInfo.email)" icon="Collection">
@ -42,7 +42,7 @@
<span class="input-help">{{ $t('setting.emailHelper') }}</span> <span class="input-help">{{ $t('setting.emailHelper') }}</span>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item :label="$t('setting.title')"> <el-form-item :label="$t('setting.title')" prop="settingInfo.panelName">
<el-input clearable v-model="form.settingInfo.panelName"> <el-input clearable v-model="form.settingInfo.panelName">
<template #append> <template #append>
<el-button <el-button
@ -54,7 +54,7 @@
</template> </template>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item :label="$t('setting.theme')"> <el-form-item :label="$t('setting.theme')" prop="settingInfo.theme">
<el-radio-group <el-radio-group
@change="SaveSetting('Theme', form.settingInfo.theme)" @change="SaveSetting('Theme', form.settingInfo.theme)"
v-model="form.settingInfo.theme" v-model="form.settingInfo.theme"
@ -69,7 +69,7 @@
</el-radio-button> </el-radio-button>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item :label="$t('setting.language')"> <el-form-item :label="$t('setting.language')" prop="settingInfo.language">
<el-radio-group <el-radio-group
@change="SaveSetting('Language', form.settingInfo.language)" @change="SaveSetting('Language', form.settingInfo.language)"
v-model="form.settingInfo.language" v-model="form.settingInfo.language"
@ -83,8 +83,8 @@
</span> </span>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item :label="$t('setting.sessionTimeout')"> <el-form-item :label="$t('setting.sessionTimeout')" prop="settingInfo.sessionTimeout">
<el-input v-model="form.settingInfo.sessionTimeout"> <el-input v-model.number="form.settingInfo.sessionTimeout">
<template #append> <template #append>
<el-button <el-button
@click="SaveSetting('SessionTimeout', form.settingInfo.sessionTimeout)" @click="SaveSetting('SessionTimeout', form.settingInfo.sessionTimeout)"
@ -114,14 +114,7 @@
</el-card> </el-card>
</el-form> </el-form>
<el-dialog v-model="passwordVisiable" :title="$t('setting.changePassword')" width="30%"> <el-dialog v-model="passwordVisiable" :title="$t('setting.changePassword')" width="30%">
<el-form <el-form ref="passFormRef" label-width="80px" label-position="left" :model="passForm" :rules="passRules">
size="small"
ref="passFormRef"
label-width="80px"
label-position="left"
:model="passForm"
:rules="passRules"
>
<el-form-item :label="$t('setting.oldPassword')" prop="oldPassword"> <el-form-item :label="$t('setting.oldPassword')" prop="oldPassword">
<el-input type="password" show-password clearable v-model="passForm.oldPassword" /> <el-input type="password" show-password clearable v-model="passForm.oldPassword" />
</el-form-item> </el-form-item>
@ -135,7 +128,7 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="passwordVisiable = false">{{ $t('commons.button.cancel') }}</el-button> <el-button @click="passwordVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button @click="submitChangePassword()"> <el-button @click="submitChangePassword(passFormRef)">
{{ $t('commons.button.confirm') }} {{ $t('commons.button.confirm') }}
</el-button> </el-button>
</span> </span>
@ -151,8 +144,8 @@ import { Setting } from '@/api/interface/setting';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { GlobalStore } from '@/store'; import { GlobalStore } from '@/store';
import { useTheme } from '@/hooks/use-theme'; import { useTheme } from '@/hooks/use-theme';
import { Rules } from '@/global/form-rues';
import router from '@/routers/router'; import router from '@/routers/router';
import { Rules } from '@/global/form-rues';
const i18n = useI18n(); const i18n = useI18n();
const globalStore = GlobalStore(); const globalStore = GlobalStore();
@ -191,6 +184,9 @@ const form = withDefaults(defineProps<Props>(), {
const { switchDark } = useTheme(); const { switchDark } = useTheme();
const SaveSetting = async (key: string, val: string) => { const SaveSetting = async (key: string, val: string) => {
if (val === '') {
return;
}
switch (key) { switch (key) {
case 'Language': case 'Language':
i18n.locale.value = val; i18n.locale.value = val;
@ -218,12 +214,16 @@ function checkPassword(rule: any, value: any, callback: any) {
} }
callback(); callback();
} }
const submitChangePassword = async () => { const submitChangePassword = async (formEl: FormInstance | undefined) => {
await updatePassword(passForm); if (!formEl) return;
passwordVisiable.value = false; formEl.validate(async (valid) => {
ElMessage.success(i18n.t('commons.msg.operationSuccess')); if (!valid) return;
router.push({ name: 'login' }); await updatePassword(passForm);
globalStore.setLogStatus(false); passwordVisiable.value = false;
ElMessage.success(i18n.t('commons.msg.operationSuccess'));
router.push({ name: 'login' });
globalStore.setLogStatus(false);
});
}; };
const onSyncTime = async () => { const onSyncTime = async () => {
const res = await syncTime(); const res = await syncTime();

View File

@ -1,102 +1,150 @@
<template> <template>
<el-form size="small" :model="form" label-position="left" label-width="120px"> <div>
<el-card style="margin-top: 10px"> <el-form :model="form" label-position="left" label-width="160px">
<template #header> <el-card style="margin-top: 10px">
<div class="card-header"> <template #header>
<span>安全</span> <div class="card-header">
</div> <span>{{ $t('setting.safe') }}</span>
</template> </div>
<el-row> </template>
<el-col :span="1"><br /></el-col> <el-row>
<el-col :span="8"> <el-col :span="1"><br /></el-col>
<el-form-item label="面板端口"> <el-col :span="10">
<el-input clearable v-model="form.settingInfo.serverPort"> <el-form-item :label="$t('setting.panelPort')">
<template #append> <el-input clearable v-model="form.settingInfo.serverPort">
<el-button <template #append>
@click="SaveSetting('ServerPort', form.settingInfo.serverPort)" <el-button
icon="Collection" @click="SaveSetting('ServerPort', form.settingInfo.serverPort)"
icon="Collection"
>
{{ $t('commons.button.save') }}
</el-button>
</template>
<el-tooltip
class="box-item"
effect="dark"
content="Top Left prompts info"
placement="top-start"
> >
保存 <el-icon style="font-size: 14px; margin-top: 8px"><WarningFilled /></el-icon>
</el-button> </el-tooltip>
</template> </el-input>
<el-tooltip <div>
class="box-item" <span class="input-help">
effect="dark" {{ $t('setting.portHelper') }}
content="Top Left prompts info" </span>
placement="top-start" </div>
</el-form-item>
<el-form-item label="$t('setting.safeEntrance')">
<el-input clearable v-model="form.settingInfo.securityEntrance">
<template #append>
<el-button
@click="SaveSetting('SecurityEntrance', form.settingInfo.securityEntrance)"
icon="Collection"
>
{{ $t('commons.button.save') }}
</el-button>
</template>
</el-input>
<div>
<span class="input-help">
{{ $t('setting.safeEntranceHelper') }}
</span>
</div>
</el-form-item>
<el-form-item :label="$t('setting.passwordTimeout')">
<el-input clearable v-model="form.settingInfo.passwordTimeOut">
<template #append>
<el-button @click="timeoutVisiable = true" icon="Collection">
{{ $t('commons.button.set') }}
</el-button>
</template>
</el-input>
<div>
<span class="input-help">
{{ $t('setting.timeoutHelper', [loadTimeOut()]) }}
</span>
</div>
</el-form-item>
<el-form-item :label="$t('setting.complexity')">
<el-radio-group
@change="SaveSetting('ComplexityVerification', form.settingInfo.complexityVerification)"
v-model="form.settingInfo.complexityVerification"
> >
<el-icon style="font-size: 14px; margin-top: 8px"><WarningFilled /></el-icon> <el-radio-button label="enable">{{ $t('commons.button.enable') }}</el-radio-button>
</el-tooltip> <el-radio-button label="disable">{{ $t('commons.button.disable') }}</el-radio-button>
</el-input> </el-radio-group>
<div> <div>
<span class="input-help"> <span class="input-help">
建议端口范围8888 - 65535注意有安全组的服务器请提前在安全组放行新端口 {{ $t('setting.complexityHelper') }}
</span> </span>
</div>
</el-form-item>
<el-form-item :label="$t('setting.mfa')">
<el-radio-group @change="handleMFA()" v-model="form.settingInfo.mfaStatus">
<el-radio-button label="enable">{{ $t('commons.button.enable') }}</el-radio-button>
<el-radio-button label="disable">{{ $t('commons.button.disable') }}</el-radio-button>
</el-radio-group>
</el-form-item>
<div v-if="isMFAShow">
<el-card>
<ul style="margin-left: 120px; line-height: 24px">
<li>
{{ $t('setting.mfaHelper1') }}
<ul>
<li>Google Authenticator</li>
<li>Microsoft Authenticator</li>
<li>1Password</li>
<li>LastPass</li>
<li>Authenticator</li>
</ul>
</li>
<li>{{ $t('setting.mfaHelper2') }}</li>
<el-image
style="margin-left: 15px; width: 100px; height: 100px"
:src="otp.qrImage"
/>
<li>{{ $t('setting.mfaHelper3') }}</li>
<el-input v-model="mfaCode"></el-input>
<div style="margin-top: 10px; margin-bottom: 10px; float: right">
<el-button @click="form.settingInfo.mfaStatus = 'disable'">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button @click="onBind">{{ $t('commons.button.saveAndEnable') }}</el-button>
</div>
</ul>
</el-card>
</div> </div>
</el-form-item> </el-col>
<el-form-item label="安全入口"> </el-row>
<el-input clearable v-model="form.settingInfo.securityEntrance"> </el-card>
<template #append> </el-form>
<el-button <el-dialog v-model="timeoutVisiable" :title="$t('setting.changePassword')" width="30%">
@click="SaveSetting('SecurityEntrance', form.settingInfo.securityEntrance)" <el-form ref="timeoutFormRef" label-width="80px" label-position="left" :model="timeoutForm">
icon="Collection" <el-form-item :label="$t('setting.oldPassword')" prop="days" :rules="Rules.number">
> <el-input clearable v-model.number="timeoutForm.days" />
保存 </el-form-item>
</el-button> </el-form>
</template> <template #footer>
</el-input> <span class="dialog-footer">
<div> <el-button @click="timeoutVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
<span class="input-help"> <el-button @click="submitTimeout(timeoutFormRef)">
面板管理入口设置后只能通过指定安全入口登录面板,: /89dc6ae8 {{ $t('commons.button.confirm') }}
</span> </el-button>
</div> </span>
</el-form-item> </template>
<el-form-item label="密码过期时间"> </el-dialog>
<el-input clearable v-model="form.settingInfo.passwordTimeOut"> </div>
<template #append>
<el-button
@click="SaveSetting('Password', form.settingInfo.passwordTimeOut)"
icon="Collection"
>
保存
</el-button>
</template>
</el-input>
<div>
<span class="input-help">为面板密码设置过期时间过期后需要重新设置密码</span>
</div>
</el-form-item>
<el-form-item label="密码复杂度验证">
<el-switch
v-model="form.settingInfo.complexityVerification"
active-value="enable"
inactive-value="disable"
@change="SaveSetting('ComplexityVerification', form.settingInfo.complexityVerification)"
/>
<div>
<span class="input-help">
密码必须满足密码长度大于8位且大写字母小写字母数字特殊字符至少3项组合
</span>
</div>
</el-form-item>
<el-form-item label="两步验证">
<el-switch
v-model="form.settingInfo.mfaStatus"
active-value="enable"
inactive-value="disable"
@change="SaveSetting('MFAStatus', form.settingInfo.mfaStatus)"
/>
</el-form-item>
</el-col>
</el-row>
</el-card>
</el-form>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive } from 'vue';
import { ElMessage, ElForm } from 'element-plus'; import { ElMessage, ElForm } from 'element-plus';
import { updateSetting } from '@/api/modules/setting'; import { Setting } from '@/api/interface/setting';
import { updateSetting, getMFA, bindMFA } from '@/api/modules/setting';
import i18n from '@/lang'; import i18n from '@/lang';
import { Rules } from '@/global/form-rues';
import { dateFromat } from '@/utils/util';
interface Props { interface Props {
settingInfo: any; settingInfo: any;
@ -108,8 +156,22 @@ const form = withDefaults(defineProps<Props>(), {
passwordTimeOut: '', passwordTimeOut: '',
complexityVerification: '', complexityVerification: '',
mfaStatus: '', mfaStatus: '',
mfaSecret: '',
}, },
}); });
type FormInstance = InstanceType<typeof ElForm>;
const timeoutFormRef = ref<FormInstance>();
const timeoutVisiable = ref<boolean>(false);
const timeoutForm = reactive({
days: 10,
});
const isMFAShow = ref<boolean>(false);
const otp = reactive<Setting.MFAInfo>({
secret: '',
qrImage: '',
});
const mfaCode = ref();
const SaveSetting = async (key: string, val: string) => { const SaveSetting = async (key: string, val: string) => {
let param = { let param = {
@ -119,4 +181,38 @@ const SaveSetting = async (key: string, val: string) => {
await updateSetting(param); await updateSetting(param);
ElMessage.success(i18n.global.t('commons.msg.operationSuccess')); ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
}; };
const handleMFA = async () => {
if (form.settingInfo.mfaStatus === 'enable') {
const res = await getMFA();
otp.secret = res.data.secret;
otp.qrImage = res.data.qrImage;
isMFAShow.value = true;
} else {
await updateSetting({ key: 'MFAStatus', value: 'disable' });
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
}
};
const onBind = async () => {
await bindMFA({ code: mfaCode.value, secret: otp.secret });
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
isMFAShow.value = false;
};
const submitTimeout = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
let time = new Date(new Date().getTime() + 3600 * 1000 * 24 * timeoutForm.days);
SaveSetting('PasswordTimeOut', dateFromat(0, 0, time));
form.settingInfo.passwordTimeOut = dateFromat(0, 0, time);
timeoutVisiable.value = false;
});
};
function loadTimeOut() {
let staytimeGap = new Date(form.settingInfo.passwordTimeOut).getTime() - new Date().getTime();
return Math.floor(staytimeGap / (3600 * 1000 * 24));
}
</script> </script>