feat: 工具箱增加 Fail2Ban 管理 (#2966)

Refs #2209  #1643  #1787  #2645
This commit is contained in:
ssongliu 2023-11-16 14:40:08 +08:00 committed by GitHub
parent d254837c12
commit 49d8582658
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 2526 additions and 35 deletions

View file

@ -33,6 +33,8 @@ var (
sshService = service.NewISSHService()
firewallService = service.NewIFirewallService()
fail2banService = service.NewIFail2BanService()
settingService = service.NewISettingService()
backupService = service.NewIBackupService()

View file

@ -0,0 +1,153 @@
package v1
import (
"os"
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/gin-gonic/gin"
)
// @Tags Fail2Ban
// @Summary Load fail2ban base info
// @Description 获取 Fail2Ban 基础信息
// @Success 200 {object} dto.Fail2BanBaseInfo
// @Security ApiKeyAuth
// @Router /toolbox/fail2ban/base [get]
func (b *BaseApi) LoadFail2BanBaseInfo(c *gin.Context) {
data, err := fail2banService.LoadBaseInfo()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Fail2Ban
// @Summary Page fail2ban ip list
// @Description 获取 Fail2Ban ip
// @Accept json
// @Param request body dto.Fail2BanSearch true "request"
// @Success 200 {Array} string
// @Security ApiKeyAuth
// @Router /toolbox/fail2ban/search [post]
func (b *BaseApi) SearchFail2Ban(c *gin.Context) {
var req dto.Fail2BanSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
list, err := fail2banService.Search(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, list)
}
// @Tags Fail2Ban
// @Summary Operate fail2ban
// @Description 修改 Fail2Ban 状态
// @Accept json
// @Param request body dto.Operate true "request"
// @Security ApiKeyAuth
// @Router /toolbox/fail2ban/operate [post]
// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] Fail2Ban","formatEN":"[operation] Fail2Ban"}
func (b *BaseApi) OperateFail2Ban(c *gin.Context) {
var req dto.Operate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := fail2banService.Operate(req.Operation); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
// @Tags Fail2Ban
// @Summary Operate sshd of fail2ban
// @Description 配置 sshd
// @Accept json
// @Param request body dto.Operate true "request"
// @Security ApiKeyAuth
// @Router /toolbox/fail2ban/operate/sshd [post]
func (b *BaseApi) OperateSSHD(c *gin.Context) {
var req dto.Fail2BanSet
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := fail2banService.OperateSSHD(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
// @Tags Fail2Ban
// @Summary Update fail2ban conf
// @Description 修改 Fail2Ban 配置
// @Accept json
// @Param request body dto.Fail2BanUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /toolbox/fail2ban/update [post]
// @x-panel-log {"bodyKeys":["key","value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改 Fail2Ban 配置 [key] => [value]","formatEN":"update fail2ban conf [key] => [value]"}
func (b *BaseApi) UpdateFail2BanConf(c *gin.Context) {
var req dto.Fail2BanUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := fail2banService.UpdateConf(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
// @Tags Fail2Ban
// @Summary Load fail2ban conf
// @Description 获取 fail2ban 配置文件
// @Accept json
// @Success 200
// @Security ApiKeyAuth
// @Router /toolbox/fail2ban/load/conf [get]
func (b *BaseApi) LoadFail2BanConf(c *gin.Context) {
path := "/etc/fail2ban/jail.local"
file, err := os.ReadFile(path)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, string(file))
}
// @Tags Fail2Ban
// @Summary Update fail2ban conf by file
// @Description 通过文件修改 fail2ban 配置
// @Accept json
// @Param request body dto.UpdateByFile true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /toolbox/fail2ban/update/byconf [post]
func (b *BaseApi) UpdateFail2BanConfByFile(c *gin.Context) {
var req dto.UpdateByFile
if err := helper.CheckBind(&req, c); err != nil {
return
}
if err := fail2banService.UpdateConfByFile(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}

View file

@ -41,6 +41,10 @@ type DeleteByName struct {
Name string `json:"name" validate:"required"`
}
type UpdateByFile struct {
File string `json:"file"`
}
type OperationWithNameAndType struct {
Name string `json:"name"`
Type string `json:"type" validate:"required"`

View file

@ -0,0 +1,27 @@
package dto
type Fail2BanBaseInfo struct {
IsEnable bool `json:"isEnable"`
IsActive bool `json:"isActive"`
IsExist bool `json:"isExist"`
Version string `json:"version"`
MaxRetry int `json:"maxRetry"`
BanTime string `json:"banTime"`
FindTime string `json:"findTime"`
BanAction string `json:"banAction"`
}
type Fail2BanSearch struct {
Status string `json:"status" validate:"required,oneof=banned ignore"`
}
type Fail2BanUpdate struct {
Key string `json:"key" validate:"required,oneof=port bantime findtime maxretry banaction"`
Value string `json:"value"`
}
type Fail2BanSet struct {
IPs []string `json:"ips"`
Operate string `json:"operate" validate:"required,oneof=banned ignore"`
}

View file

@ -0,0 +1,211 @@
package service
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/utils/toolbox"
)
const defaultFail2BanPath = "/etc/fail2ban/jail.local"
type Fail2BanService struct{}
type IFail2BanService interface {
LoadBaseInfo() (dto.Fail2BanBaseInfo, error)
Search(search dto.Fail2BanSearch) ([]string, error)
Operate(operation string) error
OperateSSHD(req dto.Fail2BanSet) error
UpdateConf(req dto.Fail2BanUpdate) error
UpdateConfByFile(req dto.UpdateByFile) error
}
func NewIFail2BanService() IFail2BanService {
return &Fail2BanService{}
}
func (u *Fail2BanService) LoadBaseInfo() (dto.Fail2BanBaseInfo, error) {
var baseInfo dto.Fail2BanBaseInfo
client, err := toolbox.NewFail2Ban()
if err != nil {
return baseInfo, err
}
baseInfo.IsEnable, baseInfo.IsActive, baseInfo.IsExist = client.Status()
if !baseInfo.IsExist {
baseInfo.Version = "-"
return baseInfo, nil
}
baseInfo.Version = client.Version()
conf, err := os.ReadFile(defaultFail2BanPath)
if err != nil {
return baseInfo, fmt.Errorf("read fail2ban conf of %s failed, err: %v", defaultFail2BanPath, err)
}
lines := strings.Split(string(conf), "\n")
block := ""
for _, line := range lines {
if strings.HasPrefix(strings.ToLower(line), "[default]") {
block = "default"
continue
}
if strings.HasPrefix(line, "[sshd]") {
block = "sshd"
continue
}
if strings.HasPrefix(line, "[") {
block = ""
continue
}
if block != "default" && block != "sshd" {
continue
}
loadFailValue(line, &baseInfo)
}
return baseInfo, nil
}
func (u *Fail2BanService) Search(req dto.Fail2BanSearch) ([]string, error) {
var list []string
client, err := toolbox.NewFail2Ban()
if err != nil {
return nil, err
}
if req.Status == "banned" {
list, err = client.ListBanned()
} else {
list, err = client.ListIgnore()
}
if err != nil {
return nil, err
}
return list, nil
}
func (u *Fail2BanService) Operate(operation string) error {
client, err := toolbox.NewFail2Ban()
if err != nil {
return err
}
return client.Operate(operation)
}
func (u *Fail2BanService) UpdateConf(req dto.Fail2BanUpdate) error {
conf, err := os.ReadFile(defaultFail2BanPath)
if err != nil {
return fmt.Errorf("read fail2ban conf of %s failed, err: %v", defaultFail2BanPath, err)
}
lines := strings.Split(string(conf), "\n")
isStart, isEnd, hasKey := false, false, false
newFile := ""
for index, line := range lines {
if !isStart && strings.HasPrefix(line, "[sshd]") {
isStart = true
newFile += fmt.Sprintf("%s\n", line)
continue
}
if !isStart || isEnd {
newFile += fmt.Sprintf("%s\n", line)
continue
}
if strings.HasPrefix(line, req.Key) {
hasKey = true
newFile += fmt.Sprintf("%s = %s\n", req.Key, req.Value)
continue
}
if strings.HasPrefix(line, "[") || index != len(lines)-1 {
isEnd = true
if !hasKey {
newFile += fmt.Sprintf("%s = %s\n", req.Key, req.Value)
}
}
newFile += line
if index != len(lines)-1 {
newFile += "\n"
}
}
file, err := os.OpenFile(defaultFail2BanPath, os.O_WRONLY|os.O_TRUNC, 0640)
if err != nil {
return err
}
defer file.Close()
write := bufio.NewWriter(file)
_, _ = write.WriteString(newFile)
write.Flush()
client, err := toolbox.NewFail2Ban()
if err != nil {
return err
}
if err := client.Operate("reload"); err != nil {
return err
}
return nil
}
func (u *Fail2BanService) UpdateConfByFile(req dto.UpdateByFile) error {
file, err := os.OpenFile(defaultFail2BanPath, os.O_WRONLY|os.O_TRUNC, 0640)
if err != nil {
return err
}
defer file.Close()
write := bufio.NewWriter(file)
_, _ = write.WriteString(req.File)
write.Flush()
client, err := toolbox.NewFail2Ban()
if err != nil {
return err
}
if err := client.Operate("reload"); err != nil {
return err
}
return nil
}
func (u *Fail2BanService) OperateSSHD(req dto.Fail2BanSet) error {
if req.Operate == "ignore" {
if err := u.UpdateConf(dto.Fail2BanUpdate{Key: "ignoreip", Value: strings.Join(req.IPs, ",")}); err != nil {
return err
}
return nil
}
client, err := toolbox.NewFail2Ban()
if err != nil {
return err
}
if err := client.ReBanIPs(req.IPs); err != nil {
return err
}
return nil
}
func loadFailValue(line string, baseInfo *dto.Fail2BanBaseInfo) {
if strings.HasPrefix(line, "maxretry") {
itemValue := strings.ReplaceAll(line, "maxretry", "")
itemValue = strings.ReplaceAll(itemValue, "=", "")
baseInfo.MaxRetry, _ = strconv.Atoi(strings.TrimSpace(itemValue))
}
if strings.HasPrefix(line, "findtime") {
itemValue := strings.ReplaceAll(line, "findtime", "")
itemValue = strings.ReplaceAll(itemValue, "=", "")
baseInfo.FindTime = strings.TrimSpace(itemValue)
}
if strings.HasPrefix(line, "bantime") {
itemValue := strings.ReplaceAll(line, "bantime", "")
itemValue = strings.ReplaceAll(itemValue, "=", "")
baseInfo.BanTime = strings.TrimSpace(itemValue)
}
if strings.HasPrefix(line, "banaction") {
itemValue := strings.ReplaceAll(line, "banaction", "")
itemValue = strings.ReplaceAll(itemValue, "=", "")
baseInfo.BanAction = strings.TrimSpace(itemValue)
}
}

View file

@ -1,11 +1,12 @@
package router
import (
"github.com/gin-contrib/gzip"
"html/template"
"net/http"
"strings"
"github.com/gin-contrib/gzip"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/i18n"
"github.com/1Panel-dev/1Panel/backend/middleware"
@ -94,6 +95,7 @@ func Routers() *gin.Engine {
systemRouter.InitNginxRouter(PrivateGroup)
systemRouter.InitRuntimeRouter(PrivateGroup)
systemRouter.InitProcessRouter(PrivateGroup)
systemRouter.InitToolboxRouter(PrivateGroup)
}
return Router

View file

@ -8,6 +8,7 @@ type RouterGroup struct {
MonitorRouter
LogRouter
FileRouter
ToolboxRouter
TerminalRouter
CronjobRouter
SettingRouter

View file

@ -0,0 +1,27 @@
package router
import (
v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1"
"github.com/1Panel-dev/1Panel/backend/middleware"
"github.com/gin-gonic/gin"
)
type ToolboxRouter struct{}
func (s *ToolboxRouter) InitToolboxRouter(Router *gin.RouterGroup) {
toolboxRouter := Router.Group("toolbox").
Use(middleware.JwtAuth()).
Use(middleware.SessionAuth()).
Use(middleware.PasswordExpired())
baseApi := v1.ApiGroupApp.BaseApi
{
toolboxRouter.GET("/fail2ban/base", baseApi.LoadFail2BanBaseInfo)
toolboxRouter.GET("/fail2ban/load/conf", baseApi.LoadFail2BanConf)
toolboxRouter.POST("/fail2ban/search", baseApi.SearchFail2Ban)
toolboxRouter.POST("/fail2ban/operate", baseApi.OperateFail2Ban)
toolboxRouter.POST("/fail2ban/operate/sshd", baseApi.OperateSSHD)
toolboxRouter.POST("/fail2ban/update", baseApi.UpdateFail2BanConf)
toolboxRouter.POST("/fail2ban/update/byconf", baseApi.UpdateFail2BanConfByFile)
}
}

View file

@ -0,0 +1,155 @@
package toolbox
import (
"encoding/json"
"fmt"
"os"
"strings"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
)
type Fail2Ban struct{}
const defaultPath = "/etc/fail2ban/jail.local"
type FirewallClient interface {
Status() (bool, bool, bool, error)
Version() (string, error)
Operate(operate string) error
OperateSSHD(operate, ip string) error
}
func NewFail2Ban() (*Fail2Ban, error) {
isExist, _ := systemctl.IsExist("fail2ban.service")
if isExist {
if _, err := os.Stat(defaultPath); err != nil {
if err := initLocalFile(); err != nil {
return nil, err
}
stdout, err := cmd.Exec("fail2ban-client reload")
if err != nil {
global.LOG.Errorf("reload fail2ban failed, err: %s", stdout)
return nil, err
}
}
}
return &Fail2Ban{}, nil
}
func (f *Fail2Ban) Status() (bool, bool, bool) {
isEnable, _ := systemctl.IsEnable("fail2ban.service")
isActive, _ := systemctl.IsActive("fail2ban.service")
isExist, _ := systemctl.IsExist("fail2ban.service")
return isEnable, isActive, isExist
}
func (f *Fail2Ban) Version() string {
stdout, err := cmd.Exec("fail2ban-client version")
if err != nil {
global.LOG.Errorf("load the fail2ban version failed, err: %s", stdout)
return "-"
}
return strings.ReplaceAll(stdout, "\n", "")
}
func (f *Fail2Ban) Operate(operate string) error {
switch operate {
case "start", "restart", "stop", "enable", "disable":
stdout, err := cmd.Execf("systemctl %s fail2ban.service", operate)
if err != nil {
return fmt.Errorf("%s the fail2ban.service failed, err: %s", operate, stdout)
}
return nil
case "reload":
stdout, err := cmd.Exec("fail2ban-client reload")
if err != nil {
return fmt.Errorf("fail2ban-client reload, err: %s", stdout)
}
return nil
default:
return fmt.Errorf("not support such operation: %v", operate)
}
}
func (f *Fail2Ban) ReBanIPs(ips []string) error {
ipItems, _ := f.ListBanned()
stdout, err := cmd.Execf("fail2ban-client unban --all")
if err != nil {
stdout1, err := cmd.Execf("fail2ban-client set sshd banip %s", strings.Join(ipItems, " "))
if err != nil {
global.LOG.Errorf("rebanip after fail2ban-client unban --all failed, err: %s", stdout1)
}
return fmt.Errorf("fail2ban-client unban --all failed, err: %s", stdout)
}
stdout1, err := cmd.Execf("fail2ban-client set sshd banip %s", strings.Join(ips, " "))
if err != nil {
return fmt.Errorf("handle `fail2ban-client set sshd banip %s` failed, err: %s", strings.Join(ips, " "), stdout1)
}
return nil
}
func (f *Fail2Ban) ListBanned() ([]string, error) {
var lists []string
stdout, err := cmd.Exec("fail2ban-client get sshd banned")
if err != nil {
return lists, err
}
stdout = strings.ReplaceAll(stdout, "\n", "")
stdout = strings.ReplaceAll(stdout, "'", "\"")
if err := json.Unmarshal([]byte(stdout), &lists); err != nil {
return lists, fmt.Errorf("handle json unmarshal (%s) failed, err: %v", stdout, err)
}
return lists, nil
}
func (f *Fail2Ban) ListIgnore() ([]string, error) {
var lists []string
stdout, err := cmd.Exec("fail2ban-client get sshd ignoreip")
if err != nil {
return lists, err
}
stdout = strings.ReplaceAll(stdout, "|", "")
stdout = strings.ReplaceAll(stdout, "`", "")
stdout = strings.ReplaceAll(stdout, "\n", "")
addrs := strings.Split(stdout, "-")
for _, addr := range addrs {
if !strings.HasPrefix(addr, " ") {
continue
}
lists = append(lists, strings.ReplaceAll(addr, " ", ""))
}
return lists, nil
}
func initLocalFile() error {
if _, err := os.Create(defaultPath); err != nil {
return err
}
initFile := `#DEFAULT-START
[DEFAULT]
bantime = 600
findtime = 300
maxretry = 5
banaction = firewallcmd-ipset
action = %(action_mwl)s
#DEFAULT-END
[sshd]
ignoreip = 127.0.0.1/8
enabled = true
filter = sshd
port = 22
maxretry = 5
findtime = 300
bantime = 600
action = %(action_mwl)s
logpath = /var/log/secure`
if err := os.WriteFile(defaultPath, []byte(initFile), 0640); err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,61 @@
package toolbox
import (
"fmt"
"os"
"strings"
"testing"
"github.com/1Panel-dev/1Panel/backend/utils/ssh"
)
func TestCds(t *testing.T) {
kk := ssh.ConnInfo{
Port: 22,
AuthMode: "password",
User: "root",
}
sd, err := kk.Run("fail2ban-client get sshd ignoreip")
if err != nil {
fmt.Println(err)
}
sd = strings.ReplaceAll(sd, "|", "")
sd = strings.ReplaceAll(sd, "`", "")
sd = strings.ReplaceAll(sd, "\n", "")
addrs := strings.Split(sd, "-")
for _, addr := range addrs {
if !strings.HasPrefix(addr, " ") {
continue
}
fmt.Println(strings.TrimPrefix(addr, " "))
}
}
func TestCdsxx(t *testing.T) {
initFile := `#DEFAULT-START
[DEFAULT]
ignoreip = 127.0.0.1/8,172.16.10.114,172.16.10.116
bantime = 600
findtime = 300
maxretry = 5
banaction = firewallcmd-ipset
action = %(action_mwl)s
#DEFAULT-END
#sshd-START
[sshd]
enabled = true
filter = sshd
port = 22
maxretry = 5
findtime = 300
bantime = 86400
action = %(action_mwl)s
logpath = /var/log/secure
#sshd-END`
if err := os.WriteFile("/Users/slooop/Downloads/tex.txt", []byte(initFile), 0640); err != nil {
fmt.Println(err)
}
}

View file

@ -10086,6 +10086,229 @@ const docTemplate = `{
}
}
},
"/toolbox/fail2ban/base": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取 Fail2Ban 基础信息",
"tags": [
"Fail2Ban"
],
"summary": "Load fail2ban base info",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.Fail2BanBaseInfo"
}
}
}
}
},
"/toolbox/fail2ban/load/conf": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取 fail2ban 配置文件",
"consumes": [
"application/json"
],
"tags": [
"Fail2Ban"
],
"summary": "Load fail2ban conf",
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/toolbox/fail2ban/operate": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改 Fail2Ban 状态",
"consumes": [
"application/json"
],
"tags": [
"Fail2Ban"
],
"summary": "Operate fail2ban",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.Operate"
}
}
],
"responses": {},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"operation"
],
"formatEN": "[operation] Fail2Ban",
"formatZH": "[operation] Fail2Ban",
"paramKeys": []
}
}
},
"/toolbox/fail2ban/operate/sshd": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "配置 sshd",
"consumes": [
"application/json"
],
"tags": [
"Fail2Ban"
],
"summary": "Operate sshd of fail2ban",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.Operate"
}
}
],
"responses": {}
}
},
"/toolbox/fail2ban/search": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取 Fail2Ban ip",
"consumes": [
"application/json"
],
"tags": [
"Fail2Ban"
],
"summary": "Page fail2ban ip list",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.Fail2BanSearch"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "Array"
}
}
}
}
},
"/toolbox/fail2ban/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改 Fail2Ban 配置",
"consumes": [
"application/json"
],
"tags": [
"Fail2Ban"
],
"summary": "Update fail2ban conf",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.Fail2BanUpdate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"key",
"value"
],
"formatEN": "update fail2ban conf [key] =\u003e [value]",
"formatZH": "修改 Fail2Ban 配置 [key] =\u003e [value]",
"paramKeys": []
}
}
},
"/toolbox/fail2ban/update/byconf": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "通过文件修改 fail2ban 配置",
"consumes": [
"application/json"
],
"tags": [
"Fail2Ban"
],
"summary": "Update fail2ban conf by file",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.UpdateByFile"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/websites": {
"post": {
"security": [
@ -13999,6 +14222,71 @@ const docTemplate = `{
}
}
},
"dto.Fail2BanBaseInfo": {
"type": "object",
"properties": {
"banAction": {
"type": "string"
},
"banTime": {
"type": "string"
},
"findTime": {
"type": "string"
},
"isActive": {
"type": "boolean"
},
"isEnable": {
"type": "boolean"
},
"isExist": {
"type": "boolean"
},
"maxRetry": {
"type": "integer"
},
"version": {
"type": "string"
}
}
},
"dto.Fail2BanSearch": {
"type": "object",
"required": [
"status"
],
"properties": {
"status": {
"type": "string",
"enum": [
"banned",
"ignore"
]
}
}
},
"dto.Fail2BanUpdate": {
"type": "object",
"required": [
"key"
],
"properties": {
"key": {
"type": "string",
"enum": [
"port",
"bantime",
"findtime",
"maxretry",
"banaction"
]
},
"value": {
"type": "string"
}
}
},
"dto.FirewallBaseInfo": {
"type": "object",
"properties": {
@ -15961,6 +16249,14 @@ const docTemplate = `{
}
}
},
"dto.UpdateByFile": {
"type": "object",
"properties": {
"file": {
"type": "string"
}
}
},
"dto.UpdateDescription": {
"type": "object",
"required": [

View file

@ -10079,6 +10079,229 @@
}
}
},
"/toolbox/fail2ban/base": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取 Fail2Ban 基础信息",
"tags": [
"Fail2Ban"
],
"summary": "Load fail2ban base info",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.Fail2BanBaseInfo"
}
}
}
}
},
"/toolbox/fail2ban/load/conf": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取 fail2ban 配置文件",
"consumes": [
"application/json"
],
"tags": [
"Fail2Ban"
],
"summary": "Load fail2ban conf",
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/toolbox/fail2ban/operate": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改 Fail2Ban 状态",
"consumes": [
"application/json"
],
"tags": [
"Fail2Ban"
],
"summary": "Operate fail2ban",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.Operate"
}
}
],
"responses": {},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"operation"
],
"formatEN": "[operation] Fail2Ban",
"formatZH": "[operation] Fail2Ban",
"paramKeys": []
}
}
},
"/toolbox/fail2ban/operate/sshd": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "配置 sshd",
"consumes": [
"application/json"
],
"tags": [
"Fail2Ban"
],
"summary": "Operate sshd of fail2ban",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.Operate"
}
}
],
"responses": {}
}
},
"/toolbox/fail2ban/search": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取 Fail2Ban ip",
"consumes": [
"application/json"
],
"tags": [
"Fail2Ban"
],
"summary": "Page fail2ban ip list",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.Fail2BanSearch"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "Array"
}
}
}
}
},
"/toolbox/fail2ban/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改 Fail2Ban 配置",
"consumes": [
"application/json"
],
"tags": [
"Fail2Ban"
],
"summary": "Update fail2ban conf",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.Fail2BanUpdate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"key",
"value"
],
"formatEN": "update fail2ban conf [key] =\u003e [value]",
"formatZH": "修改 Fail2Ban 配置 [key] =\u003e [value]",
"paramKeys": []
}
}
},
"/toolbox/fail2ban/update/byconf": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "通过文件修改 fail2ban 配置",
"consumes": [
"application/json"
],
"tags": [
"Fail2Ban"
],
"summary": "Update fail2ban conf by file",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.UpdateByFile"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/websites": {
"post": {
"security": [
@ -13992,6 +14215,71 @@
}
}
},
"dto.Fail2BanBaseInfo": {
"type": "object",
"properties": {
"banAction": {
"type": "string"
},
"banTime": {
"type": "string"
},
"findTime": {
"type": "string"
},
"isActive": {
"type": "boolean"
},
"isEnable": {
"type": "boolean"
},
"isExist": {
"type": "boolean"
},
"maxRetry": {
"type": "integer"
},
"version": {
"type": "string"
}
}
},
"dto.Fail2BanSearch": {
"type": "object",
"required": [
"status"
],
"properties": {
"status": {
"type": "string",
"enum": [
"banned",
"ignore"
]
}
}
},
"dto.Fail2BanUpdate": {
"type": "object",
"required": [
"key"
],
"properties": {
"key": {
"type": "string",
"enum": [
"port",
"bantime",
"findtime",
"maxretry",
"banaction"
]
},
"value": {
"type": "string"
}
}
},
"dto.FirewallBaseInfo": {
"type": "object",
"properties": {
@ -15954,6 +16242,14 @@
}
}
},
"dto.UpdateByFile": {
"type": "object",
"properties": {
"file": {
"type": "string"
}
}
},
"dto.UpdateDescription": {
"type": "object",
"required": [

View file

@ -973,6 +973,50 @@ definitions:
- fileName
- source
type: object
dto.Fail2BanBaseInfo:
properties:
banAction:
type: string
banTime:
type: string
findTime:
type: string
isActive:
type: boolean
isEnable:
type: boolean
isExist:
type: boolean
maxRetry:
type: integer
version:
type: string
type: object
dto.Fail2BanSearch:
properties:
status:
enum:
- banned
- ignore
type: string
required:
- status
type: object
dto.Fail2BanUpdate:
properties:
key:
enum:
- port
- bantime
- findtime
- maxretry
- banaction
type: string
value:
type: string
required:
- key
type: object
dto.FirewallBaseInfo:
properties:
name:
@ -2296,6 +2340,11 @@ definitions:
label:
type: string
type: object
dto.UpdateByFile:
properties:
file:
type: string
type: object
dto.UpdateDescription:
properties:
description:
@ -10781,6 +10830,145 @@ paths:
formatEN: upgrade service => [version]
formatZH: 更新系统 => [version]
paramKeys: []
/toolbox/fail2ban/base:
get:
description: 获取 Fail2Ban 基础信息
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.Fail2BanBaseInfo'
security:
- ApiKeyAuth: []
summary: Load fail2ban base info
tags:
- Fail2Ban
/toolbox/fail2ban/load/conf:
get:
consumes:
- application/json
description: 获取 fail2ban 配置文件
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Load fail2ban conf
tags:
- Fail2Ban
/toolbox/fail2ban/operate:
post:
consumes:
- application/json
description: 修改 Fail2Ban 状态
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.Operate'
responses: {}
security:
- ApiKeyAuth: []
summary: Operate fail2ban
tags:
- Fail2Ban
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operation
formatEN: '[operation] Fail2Ban'
formatZH: '[operation] Fail2Ban'
paramKeys: []
/toolbox/fail2ban/operate/sshd:
post:
consumes:
- application/json
description: 配置 sshd
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.Operate'
responses: {}
security:
- ApiKeyAuth: []
summary: Operate sshd of fail2ban
tags:
- Fail2Ban
/toolbox/fail2ban/search:
post:
consumes:
- application/json
description: 获取 Fail2Ban ip
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.Fail2BanSearch'
responses:
"200":
description: OK
schema:
type: Array
security:
- ApiKeyAuth: []
summary: Page fail2ban ip list
tags:
- Fail2Ban
/toolbox/fail2ban/update:
post:
consumes:
- application/json
description: 修改 Fail2Ban 配置
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.Fail2BanUpdate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Update fail2ban conf
tags:
- Fail2Ban
x-panel-log:
BeforeFunctions: []
bodyKeys:
- key
- value
formatEN: update fail2ban conf [key] => [value]
formatZH: 修改 Fail2Ban 配置 [key] => [value]
paramKeys: []
/toolbox/fail2ban/update/byconf:
post:
consumes:
- application/json
description: 通过文件修改 fail2ban 配置
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.UpdateByFile'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Update fail2ban conf by file
tags:
- Fail2Ban
/websites:
post:
consumes:

View file

@ -34,3 +34,6 @@ export interface DescriptionUpdate {
id: number;
description: string;
}
export interface UpdateByFile {
file: string;
}

View file

@ -0,0 +1,29 @@
export namespace Toolbox {
export interface Fail2banBaseInfo {
isEnable: boolean;
isActive: boolean;
isExist: boolean;
version: string;
port: number;
maxRetry: number;
banTime: string;
findTime: string;
banAction: string;
logPath: string;
}
export interface Fail2banSearch {
status: string;
}
export interface Fail2banUpdate {
key: string;
value: string;
}
export interface Fail2banSet {
ips: Array<string>;
operate: string;
}
}

View file

@ -0,0 +1,31 @@
import http from '@/api';
import { UpdateByFile } from '../interface';
import { Toolbox } from '../interface/toolbox';
// fail2ban
export const getFail2banBase = () => {
return http.get<Toolbox.Fail2banBaseInfo>(`/toolbox/fail2ban/base`);
};
export const getFail2banConf = () => {
return http.get<string>(`/toolbox/fail2ban/load/conf`);
};
export const searchFail2ban = (param: Toolbox.Fail2banSearch) => {
return http.post<Array<string>>(`/toolbox/fail2ban/search`, param);
};
export const operateFail2ban = (operate: string) => {
return http.post(`/toolbox/fail2ban/operate`, { operation: operate });
};
export const operatorFail2banSSHD = (param: Toolbox.Fail2banSet) => {
return http.post(`/toolbox/fail2ban/operate/sshd`, param);
};
export const updateFail2ban = (param: Toolbox.Fail2banUpdate) => {
return http.post(`/toolbox/fail2ban/update`, param);
};
export const updateFail2banByFile = (param: UpdateByFile) => {
return http.post(`/toolbox/fail2ban/update/byconf`, param);
};

View file

@ -96,7 +96,7 @@ const handleClose = () => {
};
const toDoc = () => {
window.open('https://1panel.cn/docs/', '_blank');
window.open('https://1panel.cn/docs/', '_blank', 'noopener,noreferrer');
};
const toForum = () => {

View file

@ -866,6 +866,25 @@ const message = {
keyPassword: 'Private key password',
emptyTerminal: 'No terminal is currently connected',
},
toolbox: {
fail2ban: {
noFail2ban: 'Fail2Ban service not detected, please refer to the official documentation for installation',
unActive: 'The Fail2Ban service is not enabled at present, please enable it first!',
operation: 'Perform [{0}] operation on Fail2Ban service, continue?',
fail2banChange: 'Fail2Ban Configuration Modification',
ignoreHelper: 'The IP list in the whitelist will be ignored for blocking, continue?',
bannedHelper: 'The IP list in the blacklist will be blocked by the server, continue?',
maxRetry: 'Maximum Retry Attempts',
banTime: 'Ban Time',
banTimeHelper: 'Default ban time is 10 minutes, -1 indicates permanent ban',
findTime: 'Discovery Period',
banAction: 'Ban Action',
banActionOption: 'Ban specified IP addresses using {0}',
allPorts: ' (All Ports)',
ignoreIP: 'IP Whitelist',
bannedIP: 'IP Blacklist',
},
},
logs: {
panelLog: 'Panel logs',
operation: 'Operation logs',
@ -887,6 +906,7 @@ const message = {
files: 'File Manage',
runtimes: 'Runtime',
process: 'Process',
toolbox: 'Toolbox',
logs: 'Panel Logs',
settings: 'Panel Setting',
cronjobs: 'Cronjob',

View file

@ -830,6 +830,25 @@ const message = {
keyPassword: '私鑰密碼',
emptyTerminal: '暫無終端連接',
},
toolbox: {
fail2ban: {
noFail2ban: '未檢測到 Fail2Ban 服務請參考官方文檔進行安裝',
unActive: '當前未開啟 Fail2Ban 服務請先開啟',
operation: ' Fail2Ban 服務進行 [{0}] 操作是否繼續',
fail2banChange: 'Fail2Ban 配置修改',
ignoreHelper: '白名單中的 IP 列表將被忽略屏蔽是否繼續',
bannedHelper: '黑名單中的 IP 列表將被伺服器屏蔽是否繼續',
maxRetry: '最大重試次數',
banTime: '禁用時間',
banTimeHelper: '默認禁用時間為 10 分鐘禁用時間為 -1 則表示永久禁用',
findTime: '發現周期',
banAction: '禁用方式',
banActionOption: '通過 {0} 來禁用指定的 IP 地址',
allPorts: ' (所有端口)',
ignoreIP: 'IP 白名單',
bannedIP: 'IP 黑名單',
},
},
logs: {
panelLog: '面板日誌',
operation: '操作日誌',
@ -851,6 +870,7 @@ const message = {
files: '文件管理',
runtimes: '運行環境',
process: '進程管理',
toolbox: '工具箱',
logs: '日誌審計',
settings: '面板設置',
cronjobs: '計劃任務',

View file

@ -831,6 +831,25 @@ const message = {
keyPassword: '私钥密码',
emptyTerminal: '暂无终端连接',
},
toolbox: {
fail2ban: {
noFail2ban: '未检测到 Fail2Ban 服务请参考官方文档进行安装',
unActive: '当前未开启 Fail2Ban 服务请先开启',
operation: ' Fail2Ban 服务进行 [{0}] 操作是否继续',
fail2banChange: 'Fail2Ban 配置修改',
ignoreHelper: '白名单中的 IP 列表将被忽略屏蔽是否继续',
bannedHelper: '黑名单中的 IP 列表将被服务器屏蔽是否继续',
maxRetry: '最大重试次数',
banTime: '禁用时间',
banTimeHelper: '默认禁用时间为 10 分钟禁用时间为 -1 则表示永久禁用',
findTime: '发现周期',
banAction: '禁用方式',
banActionOption: '通过 {0} 来禁用指定的 IP 地址',
allPorts: ' (所有端口)',
ignoreIP: 'IP 白名单',
bannedIP: 'IP 黑名单',
},
},
logs: {
panelLog: '面板日志',
operation: '操作日志',
@ -852,6 +871,7 @@ const message = {
files: '文件管理',
runtimes: '运行环境',
process: '进程管理',
toolbox: '工具箱',
logs: '日志审计',
settings: '面板设置',
cronjobs: '计划任务',

View file

@ -91,17 +91,6 @@ const hostRouter = {
requiresAuth: false,
},
},
{
path: '/hosts/tool/supersivor',
name: 'Supervisor',
component: () => import('@/views/host/tool/supervisor/index.vue'),
meta: {
title: 'menu.supervisor',
activeMenu: '/hosts/tool/supersivor',
keepAlive: true,
requiresAuth: false,
},
},
{
path: '/hosts/ssh/ssh',
name: 'SSH',

View file

@ -1,7 +1,7 @@
import { Layout } from '@/routers/constant';
const logsRouter = {
sort: 7,
sort: 8,
path: '/logs',
component: Layout,
redirect: '/logs/operation',

View file

@ -1,7 +1,7 @@
import { Layout } from '@/routers/constant';
const settingRouter = {
sort: 8,
sort: 9,
path: '/settings',
component: Layout,
redirect: '/settings/panel',

View file

@ -0,0 +1,45 @@
import { Layout } from '@/routers/constant';
const toolboxRouter = {
sort: 7,
path: '/toolbox',
component: Layout,
redirect: '/toolbox/supervisor',
meta: {
title: 'menu.toolbox',
icon: 'p-toolbox',
},
children: [
{
path: '/toolbox',
name: 'Toolbox',
redirect: '/toolbox/supervisor',
component: () => import('@/views/toolbox/index.vue'),
meta: {},
children: [
{
path: 'supervisor',
name: 'Supervisor',
component: () => import('@/views/toolbox/supervisor/index.vue'),
hidden: true,
meta: {
activeMenu: '/toolbox',
requiresAuth: false,
},
},
{
path: 'fail2Ban',
name: 'Fail2Ban',
component: () => import('@/views/toolbox/fail2ban/index.vue'),
hidden: true,
meta: {
activeMenu: '/toolbox',
requiresAuth: false,
},
},
],
},
],
};
export default toolboxRouter;

View file

@ -285,6 +285,43 @@ export function getProvider(provider: string): string {
}
}
export function splitTime(item: string): any {
if (item.indexOf('s') !== -1) {
return { time: Number(item.replaceAll('s', '')), unit: 's' };
}
if (item.indexOf('m') !== -1) {
return { time: Number(item.replaceAll('m', '')), unit: 'm' };
}
if (item.indexOf('h') !== -1) {
return { time: Number(item.replaceAll('h', '')), unit: 'h' };
}
if (item.indexOf('d') !== -1) {
return { time: Number(item.replaceAll('d', '')), unit: 'd' };
}
if (item.indexOf('y') !== -1) {
return { time: Number(item.replaceAll('y', '')), unit: 'y' };
}
return { time: Number(item), unit: 's' };
}
export function transTimeUnit(val: string): any {
if (val.indexOf('s') !== -1) {
return val.replaceAll('s', i18n.global.t('commons.units.second'));
}
if (val.indexOf('m') !== -1) {
return val.replaceAll('m', i18n.global.t('commons.units.minute'));
}
if (val.indexOf('h') !== -1) {
return val.replaceAll('h', i18n.global.t('commons.units.hour'));
}
if (val.indexOf('d') !== -1) {
return val.replaceAll('d', i18n.global.t('commons.units.day'));
}
if (val.indexOf('y') !== -1) {
return val.replaceAll('y', i18n.global.t('commons.units.year'));
}
return val + i18n.global.t('commons.units.second');
}
export function getAge(d1: string): string {
const dateBegin = new Date(d1);
const dateEnd = new Date();

View file

@ -367,7 +367,7 @@ const save = async (key: string, value: string) => {
};
const toDoc = () => {
window.open('https://1panel.cn/docs/user_manual/containers/setting/', '_blank');
window.open('https://1panel.cn/docs/user_manual/containers/setting/', '_blank', 'noopener,noreferrer');
};
const onOperator = async (operation: string) => {

View file

@ -208,7 +208,7 @@ const onOpenDialog = async (
};
const toDoc = () => {
window.open('https://1panel.cn/docs/user_manual/hosts/firewall/', '_blank');
window.open('https://1panel.cn/docs/user_manual/hosts/firewall/', '_blank', 'noopener,noreferrer');
};
const onChange = async (info: any) => {

View file

@ -268,7 +268,7 @@ const quickJump = () => {
router.push({ name: 'AppInstalled' });
};
const toDoc = () => {
window.open('https://1panel.cn/docs/user_manual/hosts/firewall/', '_blank');
window.open('https://1panel.cn/docs/user_manual/hosts/firewall/', '_blank', 'noopener,noreferrer');
};
const onChangeStatus = async (row: Host.RuleInfo, status: string) => {

View file

@ -26,7 +26,8 @@
<el-divider direction="vertical" />
<el-button type="primary" link>{{ $t('firewall.noPing') }}</el-button>
<el-switch
style="margin-left: 10px"
size="small"
class="ml-2"
inactive-value="Disable"
active-value="Enable"
@change="onPingOperate"

View file

@ -38,7 +38,8 @@
{{ $t('ssh.autoStart') }}
</el-button>
<el-switch
style="margin-left: 10px"
size="small"
class="ml-2"
inactive-value="disable"
active-value="enable"
@change="onOperate(autoStart)"

View file

@ -47,16 +47,16 @@ const search = async () => {
};
const toDoc = () => {
window.open('https://1panel.cn/docs/', '_blank');
window.open('https://1panel.cn/docs/', '_blank', 'noopener,noreferrer');
};
const toGithub = () => {
window.open('https://github.com/1Panel-dev/1Panel', '_blank');
window.open('https://github.com/1Panel-dev/1Panel', '_blank', 'noopener,noreferrer');
};
const toIssue = () => {
window.open('https://github.com/1Panel-dev/1Panel/issues', '_blank');
window.open('https://github.com/1Panel-dev/1Panel/issues', '_blank', 'noopener,noreferrer');
};
const toGithubStar = () => {
window.open('https://github.com/1Panel-dev/1Panel', '_blank');
window.open('https://github.com/1Panel-dev/1Panel', '_blank', 'noopener,noreferrer');
};
onMounted(() => {

View file

@ -348,7 +348,7 @@ function hasEndpoint(val: string) {
}
const toDoc = () => {
window.open('https://1panel.cn/docs/user_manual/settings/', '_blank');
window.open('https://1panel.cn/docs/user_manual/settings/', '_blank', 'noopener,noreferrer');
};
const getBuckets = async (formEl: FormInstance | undefined) => {

View file

@ -0,0 +1,138 @@
<template>
<div>
<el-drawer v-model="drawerVisible" :destroy-on-close="true" :close-on-click-modal="false" size="30%">
<template #header>
<DrawerHeader :header="$t('toolbox.fail2ban.banAction')" :back="handleClose" />
</template>
<el-form ref="formRef" label-position="top" :model="form" @submit.prevent v-loading="loading">
<el-row type="flex" justify="center">
<el-col :span="22">
<el-form-item
:label="$t('toolbox.fail2ban.banAction')"
prop="banAction"
:rules="Rules.requiredSelect"
>
<el-select v-model="form.banAction">
<el-option value="iptables-allports" class="option">
<span class="option-content">iptables-allports</span>
<span class="input-help option-helper">
{{
$t('toolbox.fail2ban.banActionOption', ['iptables']) +
$t('toolbox.fail2ban.allPorts')
}}
</span>
</el-option>
<el-option value="iptables-multiport" class="option">
<span class="option-content">iptables-multiport</span>
<span class="input-help option-helper">
{{ $t('toolbox.fail2ban.banActionOption', ['iptables']) }}
</span>
</el-option>
<el-option value="firewallcmd-ipset" class="option">
<span class="option-content">firewallcmd-ipset</span>
<span class="input-help option-helper">
{{ $t('toolbox.fail2ban.banActionOption', ['firewallcmd ipset']) }}
</span>
</el-option>
<el-option value="ufw" class="option">
<span class="option-content">ufw</span>
<span class="input-help option-helper">
{{ $t('toolbox.fail2ban.banActionOption', ['ufw']) }}
</span>
</el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button :disabled="loading" type="primary" @click="onSave(formRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { FormInstance } from 'element-plus';
import { Rules } from '@/global/form-rules';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { updateFail2ban } from '@/api/modules/toolbox';
const emit = defineEmits<{ (e: 'search'): void }>();
interface DialogProps {
banAction: string;
}
const drawerVisible = ref();
const loading = ref();
const form = reactive({
banAction: '',
});
const formRef = ref<FormInstance>();
const acceptParams = (params: DialogProps): void => {
form.banAction = params.banAction;
drawerVisible.value = true;
};
const onSave = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
ElMessageBox.confirm(
i18n.global.t('ssh.sshChangeHelper', [i18n.global.t('toolbox.fail2ban.banAction'), form.banAction]),
i18n.global.t('toolbox.fail2ban.fail2banChange'),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
},
).then(async () => {
await updateFail2ban({ key: 'banaction', value: form.banAction })
.then(async () => {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
loading.value = false;
drawerVisible.value = false;
emit('search');
})
.catch(() => {
loading.value = false;
});
});
});
};
const handleClose = () => {
drawerVisible.value = false;
};
defineExpose({
acceptParams,
});
</script>
<style lang="scss" scoped>
.option {
height: 50px;
}
.option-content {
float: left;
position: relative;
display: block;
}
.option-helper {
float: left;
display: block;
position: absolute;
margin-top: 20px;
}
</style>

View file

@ -0,0 +1,106 @@
<template>
<div>
<el-drawer v-model="drawerVisible" :destroy-on-close="true" :close-on-click-modal="false" size="30%">
<template #header>
<DrawerHeader :header="$t('toolbox.fail2ban.banTime')" :back="handleClose" />
</template>
<el-form ref="formRef" label-position="top" :model="form" @submit.prevent v-loading="loading">
<el-row type="flex" justify="center">
<el-col :span="22">
<el-form-item :label="$t('toolbox.fail2ban.banTime')" prop="banTime" :rules="Rules.number">
<el-input type="number" v-model.number="form.banTime">
<template #append>
<el-select v-model.number="form.banTimeUnit" style="width: 100px">
<el-option :label="$t('commons.units.second')" value="s" />
<el-option :label="$t('commons.units.minute')" value="m" />
<el-option :label="$t('commons.units.hour')" value="h" />
<el-option :label="$t('commons.units.day')" value="d" />
<el-option :label="$t('commons.units.year')" value="y" />
</el-select>
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button :disabled="loading" type="primary" @click="onSave(formRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { FormInstance } from 'element-plus';
import { Rules } from '@/global/form-rules';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { updateFail2ban } from '@/api/modules/toolbox';
import { splitTime, transTimeUnit } from '@/utils/util';
const emit = defineEmits<{ (e: 'search'): void }>();
interface DialogProps {
banTime: string;
}
const drawerVisible = ref();
const loading = ref();
const form = reactive({
banTime: 300,
banTimeUnit: 's',
});
const formRef = ref<FormInstance>();
const acceptParams = (params: DialogProps): void => {
let item = splitTime(params.banTime);
form.banTime = item.time;
form.banTimeUnit = item.unit;
drawerVisible.value = true;
};
const onSave = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
ElMessageBox.confirm(
i18n.global.t('ssh.sshChangeHelper', [
i18n.global.t('toolbox.fail2ban.banTime'),
form.banTime + transTimeUnit(form.banTimeUnit),
]),
i18n.global.t('toolbox.fail2ban.fail2banChange'),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
},
).then(async () => {
await updateFail2ban({ key: 'bantime', value: form.banTime + form.banTimeUnit })
.then(async () => {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
loading.value = false;
drawerVisible.value = false;
emit('search');
})
.catch(() => {
loading.value = false;
});
});
});
};
const handleClose = () => {
drawerVisible.value = false;
};
defineExpose({
acceptParams,
});
</script>

View file

@ -0,0 +1,110 @@
<template>
<div>
<el-drawer v-model="drawerVisible" :destroy-on-close="true" :close-on-click-modal="false" size="30%">
<template #header>
<DrawerHeader :header="$t('toolbox.fail2ban.findTime')" :back="handleClose" />
</template>
<el-form ref="formRef" label-position="top" :model="form" @submit.prevent v-loading="loading">
<el-row type="flex" justify="center">
<el-col :span="22">
<el-form-item
:label="$t('toolbox.fail2ban.findTime')"
prop="findTime"
:rules="Rules.requiredInput"
>
<el-input clearable v-model.number="form.findTime">
<template #append>
<el-select v-model="form.findTimeUnit" style="width: 100px">
<el-option :label="$t('commons.units.second')" value="s" />
<el-option :label="$t('commons.units.minute')" value="m" />
<el-option :label="$t('commons.units.hour')" value="h" />
<el-option :label="$t('commons.units.day')" value="d" />
<el-option :label="$t('commons.units.year')" value="y" />
</el-select>
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button :disabled="loading" type="primary" @click="onSave(formRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { FormInstance } from 'element-plus';
import { Rules } from '@/global/form-rules';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { updateFail2ban } from '@/api/modules/toolbox';
import { splitTime, transTimeUnit } from '@/utils/util';
const emit = defineEmits<{ (e: 'search'): void }>();
interface DialogProps {
findTime: string;
}
const drawerVisible = ref();
const loading = ref();
const form = reactive({
findTime: 300,
findTimeUnit: 's',
});
const formRef = ref<FormInstance>();
const acceptParams = (params: DialogProps): void => {
let item = splitTime(params.findTime);
form.findTime = item.time;
form.findTimeUnit = item.unit;
drawerVisible.value = true;
};
const onSave = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
ElMessageBox.confirm(
i18n.global.t('ssh.sshChangeHelper', [
i18n.global.t('toolbox.fail2ban.findTime'),
form.findTime + transTimeUnit(form.findTimeUnit),
]),
i18n.global.t('toolbox.fail2ban.fail2banChange'),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
},
).then(async () => {
await updateFail2ban({ key: 'findtime', value: form.findTime + form.findTimeUnit })
.then(async () => {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
loading.value = false;
drawerVisible.value = false;
emit('search');
})
.catch(() => {
loading.value = false;
});
});
});
};
const handleClose = () => {
drawerVisible.value = false;
};
defineExpose({
acceptParams,
});
</script>

View file

@ -0,0 +1,305 @@
<template>
<div v-loading="loading">
<div class="app-status" style="margin-top: 20px">
<el-card v-if="form.isExist">
<div>
<el-tag effect="dark" type="success">Fail2Ban</el-tag>
<el-tag round class="status-content" v-if="form.isActive" type="success">
{{ $t('commons.status.running') }}
</el-tag>
<el-tag round class="status-content" v-if="!form.isActive" type="info">
{{ $t('commons.status.stopped') }}
</el-tag>
<el-tag class="status-content">{{ form.version }}</el-tag>
<span class="buttons">
<el-button v-if="form.isActive" type="primary" @click="onOperate('stop')" link>
{{ $t('commons.button.stop') }}
</el-button>
<el-button v-if="!form.isActive" type="primary" @click="onOperate('start')" link>
{{ $t('commons.button.start') }}
</el-button>
<el-divider direction="vertical" />
<el-button type="primary" @click="onOperate('restart')" link>
{{ $t('container.restart') }}
</el-button>
<el-divider direction="vertical" />
<el-button type="primary" link>
{{ $t('ssh.autoStart') }}
</el-button>
<el-switch
size="small"
class="ml-2"
inactive-value="disable"
active-value="enable"
@change="onOperate(autoStart)"
v-model="autoStart"
/>
</span>
</div>
</el-card>
</div>
<div v-if="form.isExist">
<el-card v-if="!form.isActive" class="mask-prompt">
<span>{{ $t('toolbox.fail2ban.unActive') }}</span>
</el-card>
<LayoutContent title="Fail2Ban" :divider="true" :class="{ mask: !form.isActive }">
<template #toolbar>
<el-row>
<el-col :span="16">
<el-button type="primary" plain @click="onLoadList('ignore')">
{{ $t('toolbox.fail2ban.ignoreIP') }}
</el-button>
<el-button type="primary" plain @click="onLoadList('banned')">
{{ $t('toolbox.fail2ban.bannedIP') }}
</el-button>
</el-col>
</el-row>
</template>
<template #main>
<el-radio-group v-model="confShowType" @change="changeMode">
<el-radio-button label="base">{{ $t('database.baseConf') }}</el-radio-button>
<el-radio-button label="all">{{ $t('database.allConf') }}</el-radio-button>
</el-radio-group>
<el-row style="margin-top: 20px" v-if="confShowType === 'base'">
<el-col :span="1"><br /></el-col>
<el-col :xs="24" :sm="20" :md="20" :lg="10" :xl="10">
<el-form :model="form" label-position="left" ref="formRef" label-width="120px">
<el-form-item :label="$t('toolbox.fail2ban.maxRetry')" prop="maxRetry">
<el-input disabled v-model="form.maxRetry">
<template #append>
<el-button @click="onChangeMaxRetry" icon="Setting">
{{ $t('commons.button.set') }}
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('toolbox.fail2ban.banTime')" prop="banTime">
<el-input disabled v-model="form.banTimeItem">
<template #append>
<el-button @click="onChangeBanTime" icon="Setting">
{{ $t('commons.button.set') }}
</el-button>
</template>
</el-input>
<span class="input-help">{{ $t('toolbox.fail2ban.banTimeHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('toolbox.fail2ban.findTime')" prop="findTime">
<el-input disabled v-model="form.findTimeItem">
<template #append>
<el-button @click="onChangeFindTime" icon="Setting">
{{ $t('commons.button.set') }}
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('toolbox.fail2ban.banAction')" prop="banAction">
<el-input disabled v-model="form.banAction">
<template #append>
<el-button @click="onChangeBanAction" icon="Setting">
{{ $t('commons.button.set') }}
</el-button>
</template>
</el-input>
</el-form-item>
</el-form>
</el-col>
</el-row>
<div v-if="confShowType === 'all'">
<codemirror
:autofocus="true"
placeholder="# The Fail2Ban configuration file does not exist or is empty (/etc/ssh/sshd_config)"
:indent-with-tab="true"
:tabSize="4"
style="margin-top: 10px; height: calc(100vh - 460px)"
:lineWrapping="true"
:matchBrackets="true"
theme="cobalt"
:styleActiveLine="true"
:extensions="extensions"
v-model="fail2banConf"
/>
<el-button :disabled="loading" type="primary" @click="onSaveFile" style="margin-top: 5px">
{{ $t('commons.button.save') }}
</el-button>
</div>
</template>
</LayoutContent>
</div>
<div v-else>
<LayoutContent title="Fail2Ban" :divider="true">
<template #main>
<div class="app-warn">
<div>
<span>{{ $t('toolbox.fail2ban.noFail2ban') }}</span>
<el-link
style="font-size: 12px; margin-left: 5px"
@click="toDoc"
icon="Position"
type="primary"
>
{{ $t('firewall.quickJump') }}
</el-link>
<div>
<img alt="" src="@/assets/images/no_app.svg" />
</div>
</div>
</div>
</template>
</LayoutContent>
</div>
<MaxRetry ref="maxRetryRef" @search="search" />
<BanTime ref="banTimeRef" @search="search" />
<FindTime ref="findTimeRef" @search="search" />
<BanAction ref="banActionRef" @search="search" />
<IPs ref="listRef" />
</div>
</template>
<script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue';
import { Codemirror } from 'vue-codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { oneDark } from '@codemirror/theme-one-dark';
import MaxRetry from '@/views/toolbox/fail2ban/max-retry/index.vue';
import BanTime from '@/views/toolbox/fail2ban/ban-time/index.vue';
import FindTime from '@/views/toolbox/fail2ban/find-time/index.vue';
import BanAction from '@/views/toolbox/fail2ban/ban-action/index.vue';
import IPs from '@/views/toolbox/fail2ban/ips/index.vue';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { getFail2banConf, getFail2banBase, operateFail2ban, updateFail2banByFile } from '@/api/modules/toolbox';
import { ElMessageBox } from 'element-plus';
import { transTimeUnit } from '@/utils/util';
const loading = ref(false);
const formRef = ref();
const extensions = [javascript(), oneDark];
const confShowType = ref('base');
const maxRetryRef = ref();
const banTimeRef = ref();
const findTimeRef = ref();
const banActionRef = ref();
const listRef = ref();
const autoStart = ref('enable');
const fail2banConf = ref();
const form = reactive({
isEnable: false,
isActive: false,
isExist: false,
version: '-',
port: 22,
maxRetry: 5,
banTime: '',
banTimeItem: '',
findTime: '',
findTimeItem: '',
banAction: '',
logPath: '',
});
const onLoadList = async (type: string) => {
listRef.value.acceptParams({ operate: type });
};
const toDoc = () => {
window.open('https://1panel.cn/docs/user_manual/toolbox/fail2ban/', '_blank', 'noopener,noreferrer');
};
const onSaveFile = async () => {
ElMessageBox.confirm(i18n.global.t('ssh.sshFileChangeHelper'), i18n.global.t('toolbox.fail2ban.fail2banChange'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(async () => {
loading.value = true;
await updateFail2banByFile({ file: fail2banConf.value })
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
loading.value = false;
});
});
};
const onChangeMaxRetry = () => {
maxRetryRef.value.acceptParams({ maxRetry: form.maxRetry });
};
const onChangeBanTime = () => {
banTimeRef.value.acceptParams({ banTime: form.banTime });
};
const onChangeFindTime = () => {
findTimeRef.value.acceptParams({ findTime: form.findTime });
};
const onChangeBanAction = () => {
banActionRef.value.acceptParams({ banAction: form.banAction });
};
const onOperate = async (operation: string) => {
let msg = operation === 'enable' || operation === 'disable' ? 'ssh.' : 'commons.button.';
ElMessageBox.confirm(i18n.global.t('toolbox.fail2ban.operation', [i18n.global.t(msg + operation)]), 'Fail2Ban', {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
})
.then(async () => {
loading.value = true;
await operateFail2ban(operation)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
})
.catch(() => {
autoStart.value = operation === 'enable' ? 'disable' : 'enable';
loading.value = false;
});
})
.catch(() => {
autoStart.value = operation === 'enable' ? 'disable' : 'enable';
});
};
const loadSSHConf = async () => {
const res = await getFail2banConf();
fail2banConf.value = res.data || '';
};
const changeMode = async () => {
if (confShowType.value === 'all') {
loadSSHConf();
} else {
search();
}
};
const search = async () => {
const res = await getFail2banBase();
form.isEnable = res.data.isEnable;
form.isActive = res.data.isActive;
form.isExist = res.data.isExist;
autoStart.value = form.isEnable ? 'enable' : 'disable';
form.version = res.data.version;
form.port = res.data.port;
form.maxRetry = res.data.maxRetry;
form.banTime = res.data.banTime;
form.banTimeItem = transTimeUnit(form.banTime);
form.findTime = res.data.findTime;
form.findTimeItem = transTimeUnit(form.findTime);
form.banAction = res.data.banAction;
form.logPath = res.data.logPath;
};
onMounted(() => {
search();
});
</script>

View file

@ -0,0 +1,121 @@
<template>
<el-drawer :close-on-click-modal="false" v-model="drawerVisible" size="30%">
<template #header>
<Header :header="$t('toolbox.fail2ban.' + form.operate + 'IP')" :back="handleClose"></Header>
</template>
<el-form ref="formRef" label-position="top" :model="form" @submit.prevent :rules="rules" v-loading="loading">
<el-row type="flex" justify="center">
<el-col :span="22">
<el-form-item :label="$t('toolbox.fail2ban.' + form.operate + 'IP')" prop="ips">
<el-input :autosize="{ minRows: 2 }" type="textarea" clearable v-model="form.ips" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button :disabled="loading" type="primary" @click="onSave(formRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { operatorFail2banSSHD, searchFail2ban } from '@/api/modules/toolbox';
import Header from '@/components/drawer-header/index.vue';
import { FormInstance } from 'element-plus';
import { MsgSuccess } from '@/utils/message';
import i18n from '@/lang';
import { checkCidr, checkIpV4V6 } from '@/utils/util';
const emit = defineEmits<{ (e: 'search'): void }>();
const drawerVisible = ref(false);
const loading = ref();
const form = reactive({
operate: 'ignore',
ips: '',
});
const rules = reactive({
ips: [{ validator: checkIPs, trigger: 'blur' }],
});
function checkIPs(rule: any, value: any, callback: any) {
if (form.ips !== '') {
let addr = form.ips.split('\n');
for (const item of addr) {
if (item.indexOf('/') !== -1) {
if (checkCidr(item)) {
return callback(new Error(i18n.global.t('firewall.addressFormatError')));
}
} else if (checkIpV4V6(item)) {
return callback(new Error(i18n.global.t('firewall.addressFormatError')));
}
}
}
callback();
}
interface DialogProps {
operate: string;
}
const formRef = ref<FormInstance>();
const acceptParams = (params: DialogProps): void => {
form.operate = params.operate;
form.ips = '';
drawerVisible.value = true;
search();
};
const search = () => {
let params = {
status: form.operate,
};
searchFail2ban(params).then((res) => {
let dataItem = res.data || [];
form.ips = dataItem.join('\n');
});
};
const onSave = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
ElMessageBox.confirm(
i18n.global.t('toolbox.fail2ban.' + form.operate + 'Helper'),
i18n.global.t('commons.button.set'),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
},
).then(async () => {
let param = {
operate: form.operate,
ips: form.ips.split('\n'),
};
loading.value = true;
await operatorFail2banSSHD(param)
.then(async () => {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
loading.value = false;
drawerVisible.value = false;
emit('search');
})
.catch(() => {
loading.value = false;
});
});
});
};
const handleClose = () => {
drawerVisible.value = false;
};
defineExpose({ acceptParams });
</script>

View file

@ -0,0 +1,95 @@
<template>
<div>
<el-drawer v-model="drawerVisible" :destroy-on-close="true" :close-on-click-modal="false" size="30%">
<template #header>
<DrawerHeader :header="$t('toolbox.fail2ban.maxRetry')" :back="handleClose" />
</template>
<el-form ref="formRef" label-position="top" :model="form" @submit.prevent v-loading="loading">
<el-row type="flex" justify="center">
<el-col :span="22">
<el-form-item
:label="$t('toolbox.fail2ban.maxRetry')"
prop="maxRetry"
:rules="Rules.requiredInput"
>
<el-input clearable v-model.number="form.maxRetry">
<template #append>{{ $t('commons.units.time') }}</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button :disabled="loading" type="primary" @click="onSave(formRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { FormInstance } from 'element-plus';
import { Rules } from '@/global/form-rules';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { updateFail2ban } from '@/api/modules/toolbox';
const emit = defineEmits<{ (e: 'search'): void }>();
interface DialogProps {
maxRetry: string;
}
const drawerVisible = ref();
const loading = ref();
const form = reactive({
maxRetry: 5,
});
const formRef = ref<FormInstance>();
const acceptParams = (params: DialogProps): void => {
form.maxRetry = Number(params.maxRetry);
drawerVisible.value = true;
};
const onSave = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
ElMessageBox.confirm(
i18n.global.t('ssh.sshChangeHelper', [i18n.global.t('toolbox.fail2ban.maxRetry'), form.maxRetry]),
i18n.global.t('toolbox.fail2ban.fail2banChange'),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
},
).then(async () => {
await updateFail2ban({ key: 'maxretry', value: form.maxRetry + '' })
.then(async () => {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
loading.value = false;
drawerVisible.value = false;
emit('search');
})
.catch(() => {
loading.value = false;
});
});
});
};
const handleClose = () => {
drawerVisible.value = false;
};
defineExpose({
acceptParams,
});
</script>

View file

@ -9,12 +9,15 @@
<script lang="ts" setup>
import i18n from '@/lang';
import RouterButton from '@/components/router-button/index.vue';
const buttons = [
{
label: i18n.global.t('menu.supervisor'),
path: '/hosts/tool/supervisor',
path: '/toolbox/supervisor',
},
{
label: 'Fail2Ban',
path: '/toolbox/fail2ban',
},
];
</script>

View file

@ -36,5 +36,3 @@ onMounted(() => {
getConfig();
});
</script>
<style scoped></style>

View file

@ -59,5 +59,3 @@ onMounted(() => {
getConfig();
});
</script>
<style scoped></style>

View file

@ -1,6 +1,5 @@
<template>
<div>
<ToolRouter />
<el-card v-if="showStopped" class="mask-prompt">
<span>{{ $t('tool.supervisor.notStartWarn') }}</span>
</el-card>
@ -150,7 +149,6 @@
</template>
<script setup lang="ts">
import ToolRouter from '@/views/host/tool/index.vue';
import SuperVisorStatus from './status/index.vue';
import { ref } from '@vue/runtime-core';
import ConfigSuperVisor from './config/index.vue';

View file

@ -47,7 +47,7 @@
{{ $t('firewall.quickJump') }}
</span>
<div>
<img src="@/assets/images/no_app.svg" />
<img alt="" src="@/assets/images/no_app.svg" />
</div>
</div>
</div>
@ -89,7 +89,7 @@ const setting = () => {
};
const toDoc = async () => {
window.open('https://1panel.cn/docs/user_manual/hosts/supervisor/', '_blank');
window.open('https://1panel.cn/docs/user_manual/hosts/supervisor/', '_blank', 'noopener,noreferrer');
};
const init = async () => {