2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 05:31:44 +08:00

feat: 防火墙完善

This commit is contained in:
耗子
2024-10-17 04:44:58 +08:00
parent 324c61625d
commit 7b0e98bf02
37 changed files with 490 additions and 463 deletions

View File

@@ -3,8 +3,9 @@ package middleware
import (
"net/http"
"github.com/TheTNB/panel/internal/app"
"github.com/go-rat/chix"
"github.com/TheTNB/panel/internal/app"
)
// Status 检查程序状态

View File

@@ -5,9 +5,11 @@ type FirewallStatus struct {
}
type FirewallRule struct {
PortStart uint `json:"port_start" validate:"required,gte=1,lte=65535"`
PortEnd uint `json:"port_end" validate:"required,gte=1,lte=65535"`
Protocols []string `json:"protocols" validate:"min=1,dive,oneof=tcp udp"`
Address string `json:"address"`
Strategy string `json:"strategy" validate:"required,oneof=accept drop"`
Family string `json:"family" validate:"required,oneof=ipv4 ipv6"`
PortStart uint `json:"port_start" validate:"required,gte=1,lte=65535"`
PortEnd uint `json:"port_end" validate:"required,gte=1,lte=65535"`
Protocol string `json:"protocol" validate:"min=1,oneof=tcp udp tcp/udp"`
Address string `json:"address"`
Strategy string `json:"strategy" validate:"required,oneof=accept drop reject"`
Direction string `json:"direction"`
}

View File

@@ -68,7 +68,7 @@ func Bind[T any](r *http.Request) (*T, error) {
if err := binder.Query(req); err != nil {
return nil, err
}
if slices.Contains([]string{"POST", "PUT", "PATCH"}, strings.ToUpper(r.Method)) {
if slices.Contains([]string{"POST", "PUT", "PATCH", "DELETE"}, strings.ToUpper(r.Method)) {
if err := binder.Body(req); err != nil {
return nil, err
}

View File

@@ -2,7 +2,6 @@ package service
import (
"fmt"
"github.com/shirou/gopsutil/disk"
"net"
"net/http"
"regexp"
@@ -10,6 +9,7 @@ import (
"github.com/go-rat/chix"
"github.com/hashicorp/go-version"
"github.com/shirou/gopsutil/disk"
"github.com/shirou/gopsutil/host"
"github.com/spf13/cast"

View File

@@ -2,7 +2,6 @@ package service
import (
"net/http"
"slices"
"github.com/go-rat/chix"
@@ -80,13 +79,11 @@ func (s *FirewallService) CreateRule(w http.ResponseWriter, r *http.Request) {
return
}
for protocol := range slices.Values(req.Protocols) {
if err = s.firewall.Port(firewall.FireInfo{
PortStart: req.PortStart, PortEnd: req.PortEnd, Protocol: protocol, Address: req.Address, Strategy: req.Strategy,
}, firewall.OperationAdd); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
if err = s.firewall.Port(firewall.FireInfo{
Family: req.Family, PortStart: req.PortStart, PortEnd: req.PortEnd, Protocol: req.Protocol, Address: req.Address, Strategy: req.Strategy, Direction: req.Direction,
}, firewall.OperationAdd); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
Success(w, nil)
@@ -99,13 +96,11 @@ func (s *FirewallService) DeleteRule(w http.ResponseWriter, r *http.Request) {
return
}
for protocol := range slices.Values(req.Protocols) {
if err = s.firewall.Port(firewall.FireInfo{
PortStart: req.PortStart, PortEnd: req.PortEnd, Protocol: protocol, Address: req.Address, Strategy: req.Strategy,
}, firewall.OperationRemove); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
if err = s.firewall.Port(firewall.FireInfo{
Family: req.Family, PortStart: req.PortStart, PortEnd: req.PortEnd, Protocol: req.Protocol, Address: req.Address, Strategy: req.Strategy, Direction: req.Direction,
}, firewall.OperationRemove); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
Success(w, nil)

View File

@@ -6,7 +6,7 @@ type FireInfo struct {
PortStart uint `json:"port_start"` // 1-65535
PortEnd uint `json:"port_end"` // 1-65535
Protocol string `json:"protocol"` // tcp udp tcp/udp
Strategy string `json:"strategy"` // accept drop
Strategy string `json:"strategy"` // accept drop reject
Direction string `json:"direction"` // in out 入站或出站
}

View File

@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"regexp"
"slices"
"strings"
"sync"
@@ -28,7 +29,7 @@ type Firewall struct {
func NewFirewall() *Firewall {
firewall := &Firewall{
forwardListRegex: regexp.MustCompile(`^port=(\d{1,5}):proto=(.+?):toport=(\d{1,5}):toaddr=(.*)$`),
richRuleRegex: regexp.MustCompile(`^rule family="([^"]+)" (?:source address="([^"]+)" )?(?:port port="([^"]+)" )?(?:protocol="([^"]+)" )?(accept|drop|reject)$`),
richRuleRegex: regexp.MustCompile(`^rule family="([^"]+)"(?: .*?(source|destination) address="([^"]+)")?(?: .*?port port="([^"]+)")?(?: .*?protocol="([^"]+)")?.*?(accept|drop|reject)$`),
}
return firewall
@@ -71,7 +72,9 @@ func (r *Firewall) ListRule() ([]FireInfo, error) {
}
item.Protocol = ruleItem[1]
}
item.Family = "ipv4"
item.Strategy = "accept"
item.Direction = "in"
data = append(data, item)
}
}()
@@ -131,8 +134,8 @@ func (r *Firewall) ListRichRule() ([]FireInfo, error) {
if len(rule) == 0 {
continue
}
if itemRule, err := r.parseRichRule(rule); err == nil {
data = append(data, *itemRule)
if richRules, err := r.parseRichRule(rule); err == nil {
data = append(data, richRules)
}
}
@@ -143,40 +146,47 @@ func (r *Firewall) Port(rule FireInfo, operation Operation) error {
if rule.PortEnd == 0 {
rule.PortEnd = rule.PortStart
}
if rule.PortStart > rule.PortEnd {
return fmt.Errorf("invalid port range: %d-%d", rule.PortStart, rule.PortEnd)
}
// 不支持的切换使用rich rules
if rule.Direction != "in" || rule.Family != "ipv4" || rule.Address != "" || rule.Strategy != "accept" {
return r.RichRules(rule, operation)
}
stdout, err := shell.Execf("firewall-cmd --zone=public --%s-port=%d-%d/%s --permanent", operation, rule.PortStart, rule.PortEnd, rule.Protocol)
if err != nil {
return fmt.Errorf("%s port %d-%d/%s failed, err: %s", operation, rule.PortStart, rule.PortEnd, rule.Protocol, stdout)
protocols := strings.Split(rule.Protocol, "/")
for protocol := range slices.Values(protocols) {
stdout, err := shell.Execf("firewall-cmd --zone=public --%s-port=%d-%d/%s --permanent", operation, rule.PortStart, rule.PortEnd, protocol)
if err != nil {
return fmt.Errorf("%s port %d-%d/%s failed, err: %s", operation, rule.PortStart, rule.PortEnd, protocol, stdout)
}
}
_, err = shell.Execf("firewall-cmd --reload")
_, err := shell.Execf("firewall-cmd --reload")
return err
}
func (r *Firewall) RichRules(rule FireInfo, operation Operation) error {
families := strings.Split(rule.Family, "/") // ipv4 ipv6
for _, family := range families {
protocols := strings.Split(rule.Protocol, "/")
for protocol := range slices.Values(protocols) {
var ruleBuilder strings.Builder
ruleBuilder.WriteString(fmt.Sprintf(`rule family="%s" `, family))
ruleBuilder.WriteString(fmt.Sprintf(`rule family="%s" `, rule.Family))
if len(rule.Address) != 0 {
if rule.Direction == "in" {
ruleBuilder.WriteString(fmt.Sprintf(`source address="%s" `, rule.Address))
} else if rule.Direction == "out" {
ruleBuilder.WriteString(fmt.Sprintf(`destination address="%s" `, rule.Address))
} else {
return fmt.Errorf("invalid direction: %s", rule.Direction)
}
}
if rule.PortStart != 0 && rule.PortEnd != 0 {
if rule.PortStart != 0 && rule.PortEnd != 0 && (rule.PortStart != 1 && rule.PortEnd != 65535) { // 1-65535是解析出来无端口规则的情况
ruleBuilder.WriteString(fmt.Sprintf(`port port="%d-%d" `, rule.PortStart, rule.PortEnd))
}
if len(rule.Protocol) != 0 {
ruleBuilder.WriteString(fmt.Sprintf(`protocol="%s" `, rule.Protocol))
if operation == OperationRemove && protocol != "" && rule.Protocol != "tcp/udp" { // 删除操作,可以不指定协议
ruleBuilder.WriteString(fmt.Sprintf(`protocol="%s" `, protocol))
}
if operation == OperationAdd && protocol != "" {
ruleBuilder.WriteString(fmt.Sprintf(`protocol="%s" `, protocol))
}
ruleBuilder.WriteString(rule.Strategy)
@@ -213,29 +223,46 @@ func (r *Firewall) PortForward(info Forward, operation Operation) error {
return err
}
func (r *Firewall) parseRichRule(line string) (*FireInfo, error) {
itemRule := new(FireInfo)
if r.richRuleRegex.MatchString(line) {
match := r.richRuleRegex.FindStringSubmatch(line)
if len(match) < 6 {
return nil, errors.New("invalid rich rule")
}
itemRule.Family = match[1]
itemRule.Address = match[2]
ports := strings.Split(match[3], "-")
if len(ports) > 1 {
itemRule.PortStart = cast.ToUint(ports[0])
itemRule.PortEnd = cast.ToUint(ports[1])
} else {
itemRule.PortStart = cast.ToUint(match[3])
itemRule.PortEnd = cast.ToUint(match[3])
}
itemRule.Protocol = match[4]
itemRule.Strategy = match[5]
func (r *Firewall) parseRichRule(line string) (FireInfo, error) {
if !r.richRuleRegex.MatchString(line) {
return FireInfo{}, errors.New("invalid rich rule format")
}
return itemRule, nil
match := r.richRuleRegex.FindStringSubmatch(line)
if len(match) < 7 {
return FireInfo{}, errors.New("invalid rich rule")
}
fireInfo := FireInfo{
Family: match[1],
Address: match[3],
Protocol: match[5],
Strategy: match[6],
}
if match[2] == "destination" {
fireInfo.Direction = "out"
} else {
fireInfo.Direction = "in"
}
if fireInfo.Protocol == "" {
fireInfo.Protocol = "tcp/udp"
}
ports := strings.Split(match[4], "-")
if len(ports) == 2 { // 添加端口范围
fireInfo.PortStart = cast.ToUint(ports[0])
fireInfo.PortEnd = cast.ToUint(ports[1])
} else if len(ports) == 1 && ports[0] != "" { // 添加单个端口
port := cast.ToUint(ports[0])
fireInfo.PortStart = port
fireInfo.PortEnd = port
} else if len(ports) == 1 && ports[0] == "" { // 未添加端口规则,表示所有端口
fireInfo.PortStart = 1
fireInfo.PortEnd = 65535
}
return fireInfo, nil
}
func (r *Firewall) enableForward() error {

View File

@@ -8,16 +8,10 @@ import (
"os/exec"
"strings"
"time"
"github.com/TheTNB/panel/pkg/slice"
)
// Execf 执行 shell 命令
func Execf(shell string, args ...any) (string, error) {
if !CheckArgs(slice.ToString(args)...) {
return "", errors.New("发现危险的命令参数,中止执行")
}
var cmd *exec.Cmd
_ = os.Setenv("LC_ALL", "C")
cmd = exec.Command("bash", "-c", fmt.Sprintf(shell, args...))
@@ -36,10 +30,6 @@ func Execf(shell string, args ...any) (string, error) {
// ExecfAsync 异步执行 shell 命令
func ExecfAsync(shell string, args ...any) error {
if !CheckArgs(slice.ToString(args)...) {
return errors.New("发现危险的命令参数,中止执行")
}
var cmd *exec.Cmd
_ = os.Setenv("LC_ALL", "C")
cmd = exec.Command("bash", "-c", fmt.Sprintf(shell, args...))
@@ -60,10 +50,6 @@ func ExecfAsync(shell string, args ...any) error {
// ExecfWithTimeout 执行 shell 命令并设置超时时间
func ExecfWithTimeout(timeout time.Duration, shell string, args ...any) (string, error) {
if !CheckArgs(slice.ToString(args)...) {
return "", errors.New("发现危险的命令参数,中止执行")
}
var cmd *exec.Cmd
_ = os.Setenv("LC_ALL", "C")
cmd = exec.Command("bash", "-c", fmt.Sprintf(shell, args...))
@@ -94,21 +80,3 @@ func ExecfWithTimeout(timeout time.Duration, shell string, args ...any) (string,
return strings.TrimSpace(stdout.String()), err
}
// CheckArgs 检查危险的参数
func CheckArgs(args ...string) bool {
if len(args) == 0 {
return true
}
dangerous := []string{"&", "|", ";", "$", "'", `"`, "(", ")", "`", "\n", "\r", ">", "<", "{", "}", "[", "]", "\\"}
for _, arg := range args {
for _, char := range dangerous {
if strings.Contains(arg, char) {
return false
}
}
}
return true
}

View File

@@ -1,13 +1,14 @@
package types
import (
"time"
"github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/disk"
"github.com/shirou/gopsutil/host"
"github.com/shirou/gopsutil/load"
"github.com/shirou/gopsutil/mem"
"github.com/shirou/gopsutil/net"
"time"
)
// CurrentInfo 监控信息

View File

@@ -11,12 +11,12 @@ export default {
// 获取防火墙规则
firewallRules: (page: number, limit: number): Promise<AxiosResponse<any>> =>
request.get('/firewall/rule', { params: { page, limit } }),
// 添加防火墙规则
addFirewallRule: (port: number, protocol: string): Promise<AxiosResponse<any>> =>
request.post('/firewall/rule', { port, protocol }),
// 创建防火墙规则
createFirewallRule: (rule: any): Promise<AxiosResponse<any>> =>
request.post('/firewall/rule', rule),
// 删除防火墙规则
deleteFirewallRule: (port: number, protocol: string): Promise<AxiosResponse<any>> =>
request.delete('/firewall/rule', { params: { port, protocol } }),
deleteFirewallRule: (rule: any): Promise<AxiosResponse<any>> =>
request.delete('/firewall/rule', { data: rule }),
// 获取SSH
ssh: (): Promise<AxiosResponse<any>> => request.get('/safe/ssh'),
// 设置SSH

View File

@@ -64,64 +64,6 @@
"actions": "操作"
}
},
"safeIndex": {
"title": "系统安全",
"buttons": {
"delete": "删除",
"add": "添加",
"batchDelete": "批量删除"
},
"columns": {
"port": "端口",
"protocol": "协议",
"actions": "操作"
},
"confirm": {
"delete": "确定删除规则吗?",
"batchDelete": " 高危操作!确定删除选中的规则吗?"
},
"alerts": {
"undelete": "取消删除",
"delete": "删除成功",
"add": "添加成功",
"setup": "设置成功",
"select": "请选择要删除的规则",
"ruleDelete": "规则 {rule} 删除成功"
},
"filter": {
"fields": {
"firewall": {
"label": "防火墙状态",
"checked": "开启",
"unchecked": "关闭"
},
"ssh": {
"label": "SSH状态",
"checked": "开启",
"unchecked": "关闭"
},
"ping": {
"label": "Ping状态",
"checked": "开启",
"unchecked": "关闭"
},
"port": {
"label": "SSH端口"
}
}
},
"portControl": {
"title": "端口控制",
"fields": {
"port": {
"placeholder": "例如3306, 1000-2000"
},
"protocol": {
"placeholder": "协议"
}
}
}
},
"settingIndex": {
"title": "面板设置",
"info": "修改面板端口 / 入口后,需要在浏览器地址栏做相应修改方可打开面板!",

View File

@@ -228,7 +228,7 @@ onMounted(() => {
<template #action>
<div flex items-center>
<n-button class="ml-16" type="primary" @click="handleUpdateCache">
<TheIcon :size="18" class="mr-5" icon="material-symbols:refresh" />
<TheIcon :size="18" icon="material-symbols:refresh" />
{{ $t('appIndex.buttons.updateCache') }}
</n-button>
</div>

View File

@@ -309,7 +309,7 @@ onMounted(() => {
type="primary"
@click="handleSaveWhiteList"
>
<TheIcon :size="18" class="mr-5" icon="material-symbols:save-outline" />
<TheIcon :size="18" icon="material-symbols:save-outline" />
保存白名单
</n-button>
<n-button
@@ -318,7 +318,7 @@ onMounted(() => {
type="primary"
@click="addJailModal = true"
>
<TheIcon :size="18" class="mr-5" icon="material-symbols:add" />
<TheIcon :size="18" icon="material-symbols:add" />
添加规则
</n-button>
</template>
@@ -338,32 +338,24 @@ onMounted(() => {
</n-alert>
<n-space>
<n-button type="success" @click="handleStart">
<TheIcon
:size="24"
class="mr-5"
icon="material-symbols:play-arrow-outline-rounded"
/>
<TheIcon :size="24" icon="material-symbols:play-arrow-outline-rounded" />
启动
</n-button>
<n-popconfirm @positive-click="handleStop">
<template #trigger>
<n-button type="error">
<TheIcon
:size="24"
class="mr-5"
icon="material-symbols:stop-outline-rounded"
/>
<TheIcon :size="24" icon="material-symbols:stop-outline-rounded" />
停止
</n-button>
</template>
停止 Fail2ban 会导致所有规则失效确定停止吗
</n-popconfirm>
<n-button type="warning" @click="handleRestart">
<TheIcon :size="18" class="mr-5" icon="material-symbols:replay-rounded" />
<TheIcon :size="18" icon="material-symbols:replay-rounded" />
重启
</n-button>
<n-button type="primary" @click="handleReload">
<TheIcon :size="20" class="mr-5" icon="material-symbols:refresh-rounded" />
<TheIcon :size="20" icon="material-symbols:refresh-rounded" />
重载
</n-button>
</n-space>

View File

@@ -112,28 +112,20 @@ onMounted(() => {
</n-alert>
<n-space>
<n-button type="success" @click="handleStart('frps')">
<TheIcon
:size="24"
class="mr-5"
icon="material-symbols:play-arrow-outline-rounded"
/>
<TheIcon :size="24" icon="material-symbols:play-arrow-outline-rounded" />
启动
</n-button>
<n-popconfirm @positive-click="handleStop('frps')">
<template #trigger>
<n-button type="error">
<TheIcon
:size="24"
class="mr-5"
icon="material-symbols:stop-outline-rounded"
/>
<TheIcon :size="24" icon="material-symbols:stop-outline-rounded" />
停止
</n-button>
</template>
确定要停止 Frps
</n-popconfirm>
<n-button type="warning" @click="handleRestart('frps')">
<TheIcon :size="18" class="mr-5" icon="material-symbols:replay-rounded" />
<TheIcon :size="18" icon="material-symbols:replay-rounded" />
重启
</n-button>
</n-space>
@@ -142,7 +134,7 @@ onMounted(() => {
<n-card title="修改配置" rounded-10>
<template #header-extra>
<n-button type="primary" @click="handleSaveConfig('frps')">
<TheIcon :size="18" class="mr-5" icon="material-symbols:save-outline-rounded" />
<TheIcon :size="18" icon="material-symbols:save-outline-rounded" />
保存
</n-button>
</template>
@@ -176,28 +168,20 @@ onMounted(() => {
</n-alert>
<n-space>
<n-button type="success" @click="handleStart('frpc')">
<TheIcon
:size="24"
class="mr-5"
icon="material-symbols:play-arrow-outline-rounded"
/>
<TheIcon :size="24" icon="material-symbols:play-arrow-outline-rounded" />
启动
</n-button>
<n-popconfirm @positive-click="handleStop('frpc')">
<template #trigger>
<n-button type="error">
<TheIcon
:size="24"
class="mr-5"
icon="material-symbols:stop-outline-rounded"
/>
<TheIcon :size="24" icon="material-symbols:stop-outline-rounded" />
停止
</n-button>
</template>
确定要停止 Frpc
</n-popconfirm>
<n-button type="warning" @click="handleRestart('frpc')">
<TheIcon :size="18" class="mr-5" icon="material-symbols:replay-rounded" />
<TheIcon :size="18" icon="material-symbols:replay-rounded" />
重启
</n-button>
</n-space>
@@ -206,7 +190,7 @@ onMounted(() => {
<n-card title="修改配置" rounded-10>
<template #header-extra>
<n-button type="primary" @click="handleSaveConfig('frpc')">
<TheIcon :size="18" class="mr-5" icon="material-symbols:save-outline-rounded" />
<TheIcon :size="18" icon="material-symbols:save-outline-rounded" />
保存
</n-button>
</template>

View File

@@ -82,7 +82,7 @@ onMounted(() => {
type="primary"
@click="handleSaveConfig"
>
<TheIcon :size="18" class="mr-5" icon="material-symbols:save-outline" />
<TheIcon :size="18" icon="material-symbols:save-outline" />
保存
</n-button>
</template>
@@ -101,24 +101,20 @@ onMounted(() => {
</n-alert>
<n-space>
<n-button type="success" @click="handleStart">
<TheIcon
:size="24"
class="mr-5"
icon="material-symbols:play-arrow-outline-rounded"
/>
<TheIcon :size="24" icon="material-symbols:play-arrow-outline-rounded" />
启动
</n-button>
<n-popconfirm @positive-click="handleStop">
<template #trigger>
<n-button type="error">
<TheIcon :size="24" class="mr-5" icon="material-symbols:stop-outline-rounded" />
<TheIcon :size="24" icon="material-symbols:stop-outline-rounded" />
停止
</n-button>
</template>
确定要停止 Gitea
</n-popconfirm>
<n-button type="warning" @click="handleRestart">
<TheIcon :size="18" class="mr-5" icon="material-symbols:replay-rounded" />
<TheIcon :size="18" icon="material-symbols:replay-rounded" />
重启
</n-button>
</n-space>

View File

@@ -154,7 +154,7 @@ onMounted(() => {
type="primary"
@click="handleSaveConfig"
>
<TheIcon :size="18" class="mr-5" icon="material-symbols:save-outline" />
<TheIcon :size="18" icon="material-symbols:save-outline" />
保存
</n-button>
<n-button
@@ -163,7 +163,7 @@ onMounted(() => {
type="primary"
@click="handleClearErrorLog"
>
<TheIcon :size="18" class="mr-5" icon="material-symbols:delete-outline" />
<TheIcon :size="18" icon="material-symbols:delete-outline" />
清空日志
</n-button>
<n-button
@@ -172,7 +172,7 @@ onMounted(() => {
type="primary"
@click="handleClearSlowLog"
>
<TheIcon :size="18" class="mr-5" icon="material-symbols:delete-outline" />
<TheIcon :size="18" icon="material-symbols:delete-outline" />
清空慢日志
</n-button>
</template>
@@ -192,32 +192,24 @@ onMounted(() => {
</n-alert>
<n-space>
<n-button type="success" @click="handleStart">
<TheIcon
:size="24"
class="mr-5"
icon="material-symbols:play-arrow-outline-rounded"
/>
<TheIcon :size="24" icon="material-symbols:play-arrow-outline-rounded" />
启动
</n-button>
<n-popconfirm @positive-click="handleStop">
<template #trigger>
<n-button type="error">
<TheIcon
:size="24"
class="mr-5"
icon="material-symbols:stop-outline-rounded"
/>
<TheIcon :size="24" icon="material-symbols:stop-outline-rounded" />
停止
</n-button>
</template>
停止 MySQL 会导致使用 MySQL 的网站无法访问确定要停止吗
</n-popconfirm>
<n-button type="warning" @click="handleRestart">
<TheIcon :size="18" class="mr-5" icon="material-symbols:replay-rounded" />
<TheIcon :size="18" icon="material-symbols:replay-rounded" />
重启
</n-button>
<n-button type="primary" @click="handleReload">
<TheIcon :size="20" class="mr-5" icon="material-symbols:refresh-rounded" />
<TheIcon :size="20" icon="material-symbols:refresh-rounded" />
重载
</n-button>
</n-space>

View File

@@ -125,7 +125,7 @@ onMounted(() => {
type="primary"
@click="handleSaveConfig"
>
<TheIcon :size="18" class="mr-5" icon="material-symbols:save-outline" />
<TheIcon :size="18" icon="material-symbols:save-outline" />
保存
</n-button>
<n-button
@@ -134,7 +134,7 @@ onMounted(() => {
type="primary"
@click="handleClearErrorLog"
>
<TheIcon :size="18" class="mr-5" icon="material-symbols:delete-outline" />
<TheIcon :size="18" icon="material-symbols:delete-outline" />
清空日志
</n-button>
</template>
@@ -153,28 +153,24 @@ onMounted(() => {
</n-alert>
<n-space>
<n-button type="success" @click="handleStart">
<TheIcon
:size="24"
class="mr-5"
icon="material-symbols:play-arrow-outline-rounded"
/>
<TheIcon :size="24" icon="material-symbols:play-arrow-outline-rounded" />
启动
</n-button>
<n-popconfirm @positive-click="handleStop">
<template #trigger>
<n-button type="error">
<TheIcon :size="24" class="mr-5" icon="material-symbols:stop-outline-rounded" />
<TheIcon :size="24" icon="material-symbols:stop-outline-rounded" />
停止
</n-button>
</template>
停止 OpenResty 会导致所有网站无法访问确定要停止吗
</n-popconfirm>
<n-button type="warning" @click="handleRestart">
<TheIcon :size="18" class="mr-5" icon="material-symbols:replay-rounded" />
<TheIcon :size="18" icon="material-symbols:replay-rounded" />
重启
</n-button>
<n-button type="primary" @click="handleReload">
<TheIcon :size="20" class="mr-5" icon="material-symbols:refresh-rounded" />
<TheIcon :size="20" icon="material-symbols:refresh-rounded" />
重载
</n-button>
</n-space>

View File

@@ -242,7 +242,7 @@ onMounted(() => {
type="primary"
@click="handleSaveConfig"
>
<TheIcon :size="18" class="mr-5" icon="material-symbols:save-outline" />
<TheIcon :size="18" icon="material-symbols:save-outline" />
保存
</n-button>
<n-button
@@ -251,7 +251,7 @@ onMounted(() => {
type="primary"
@click="handleSaveFPMConfig"
>
<TheIcon :size="18" class="mr-5" icon="material-symbols:save-outline" />
<TheIcon :size="18" icon="material-symbols:save-outline" />
保存
</n-button>
<n-button
@@ -260,7 +260,7 @@ onMounted(() => {
type="primary"
@click="handleClearErrorLog"
>
<TheIcon :size="18" class="mr-5" icon="material-symbols:delete-outline" />
<TheIcon :size="18" icon="material-symbols:delete-outline" />
清空错误日志
</n-button>
<n-button
@@ -269,7 +269,7 @@ onMounted(() => {
type="primary"
@click="handleClearSlowLog"
>
<TheIcon :size="18" class="mr-5" icon="material-symbols:delete-outline" />
<TheIcon :size="18" icon="material-symbols:delete-outline" />
清空慢日志
</n-button>
</template>
@@ -289,32 +289,24 @@ onMounted(() => {
</n-alert>
<n-space>
<n-button type="success" @click="handleStart">
<TheIcon
:size="24"
class="mr-5"
icon="material-symbols:play-arrow-outline-rounded"
/>
<TheIcon :size="24" icon="material-symbols:play-arrow-outline-rounded" />
启动
</n-button>
<n-popconfirm @positive-click="handleStop">
<template #trigger>
<n-button type="error">
<TheIcon
:size="24"
class="mr-5"
icon="material-symbols:stop-outline-rounded"
/>
<TheIcon :size="24" icon="material-symbols:stop-outline-rounded" />
停止
</n-button>
</template>
停止 PHP {{ version }} 会导致使用 PHP {{ version }} 的网站无法访问确定要停止吗
</n-popconfirm>
<n-button type="warning" @click="handleRestart">
<TheIcon :size="18" class="mr-5" icon="material-symbols:replay-rounded" />
<TheIcon :size="18" icon="material-symbols:replay-rounded" />
重启
</n-button>
<n-button type="primary" @click="handleReload">
<TheIcon :size="20" class="mr-5" icon="material-symbols:refresh-rounded" />
<TheIcon :size="20" icon="material-symbols:refresh-rounded" />
重载
</n-button>
</n-space>

View File

@@ -50,7 +50,7 @@ onMounted(() => {
<common-page show-footer>
<template #action>
<n-button v-if="currentTab == 'status'" class="ml-16" type="primary" @click="handleSave">
<TheIcon :size="18" class="mr-5" icon="material-symbols:save-outline" />
<TheIcon :size="18" icon="material-symbols:save-outline" />
保存
</n-button>
<n-button
@@ -59,7 +59,7 @@ onMounted(() => {
type="primary"
@click="handleSaveConfig"
>
<TheIcon :size="18" class="mr-5" icon="material-symbols:save-outline" />
<TheIcon :size="18" icon="material-symbols:save-outline" />
保存
</n-button>
</template>

View File

@@ -91,7 +91,7 @@ onMounted(() => {
type="primary"
@click="handleSaveRegistryConfig"
>
<TheIcon :size="18" class="mr-5" icon="material-symbols:save-outline" />
<TheIcon :size="18" icon="material-symbols:save-outline" />
保存
</n-button>
<n-button
@@ -100,7 +100,7 @@ onMounted(() => {
type="primary"
@click="handleSaveStorageConfig"
>
<TheIcon :size="18" class="mr-5" icon="material-symbols:save-outline" />
<TheIcon :size="18" icon="material-symbols:save-outline" />
保存
</n-button>
</template>
@@ -123,28 +123,20 @@ onMounted(() => {
</n-alert>
<n-space>
<n-button type="success" @click="handleStart">
<TheIcon
:size="24"
class="mr-5"
icon="material-symbols:play-arrow-outline-rounded"
/>
<TheIcon :size="24" icon="material-symbols:play-arrow-outline-rounded" />
启动
</n-button>
<n-popconfirm @positive-click="handleStop">
<template #trigger>
<n-button type="error">
<TheIcon
:size="24"
class="mr-5"
icon="material-symbols:stop-outline-rounded"
/>
<TheIcon :size="24" icon="material-symbols:stop-outline-rounded" />
停止
</n-button>
</template>
确定要停止 Podman
</n-popconfirm>
<n-button type="warning" @click="handleRestart">
<TheIcon :size="18" class="mr-5" icon="material-symbols:replay-rounded" />
<TheIcon :size="18" icon="material-symbols:replay-rounded" />
重启
</n-button>
</n-space>

View File

@@ -136,7 +136,7 @@ onMounted(() => {
type="primary"
@click="handleSaveConfig"
>
<TheIcon :size="18" class="mr-5" icon="material-symbols:save-outline" />
<TheIcon :size="18" icon="material-symbols:save-outline" />
保存
</n-button>
<n-button
@@ -145,11 +145,11 @@ onMounted(() => {
type="primary"
@click="handleSaveUserConfig"
>
<TheIcon :size="18" class="mr-5" icon="material-symbols:save-outline" />
<TheIcon :size="18" icon="material-symbols:save-outline" />
保存
</n-button>
<n-button v-if="currentTab == 'log'" class="ml-16" type="primary" @click="handleClearLog">
<TheIcon :size="18" class="mr-5" icon="material-symbols:delete-outline" />
<TheIcon :size="18" icon="material-symbols:delete-outline" />
清空日志
</n-button>
</template>
@@ -169,32 +169,24 @@ onMounted(() => {
</n-alert>
<n-space>
<n-button type="success" @click="handleStart">
<TheIcon
:size="24"
class="mr-5"
icon="material-symbols:play-arrow-outline-rounded"
/>
<TheIcon :size="24" icon="material-symbols:play-arrow-outline-rounded" />
启动
</n-button>
<n-popconfirm @positive-click="handleStop">
<template #trigger>
<n-button type="error">
<TheIcon
:size="24"
class="mr-5"
icon="material-symbols:stop-outline-rounded"
/>
<TheIcon :size="24" icon="material-symbols:stop-outline-rounded" />
停止
</n-button>
</template>
停止 PostgreSQL 会导致使用 PostgreSQL 的网站无法访问确定要停止吗
</n-popconfirm>
<n-button type="warning" @click="handleRestart">
<TheIcon :size="18" class="mr-5" icon="material-symbols:replay-rounded" />
<TheIcon :size="18" icon="material-symbols:replay-rounded" />
重启
</n-button>
<n-button type="primary" @click="handleReload">
<TheIcon :size="20" class="mr-5" icon="material-symbols:refresh-rounded" />
<TheIcon :size="20" icon="material-symbols:refresh-rounded" />
重载
</n-button>
</n-space>

View File

@@ -222,7 +222,7 @@ onMounted(() => {
<common-page show-footer>
<template #action>
<n-button v-if="currentTab == 'status'" class="ml-16" type="primary" @click="handleSavePort">
<TheIcon :size="18" class="mr-5" icon="material-symbols:save-outline" />
<TheIcon :size="18" icon="material-symbols:save-outline" />
保存
</n-button>
<n-button
@@ -231,7 +231,7 @@ onMounted(() => {
type="primary"
@click="addUserModal = true"
>
<TheIcon :size="18" class="mr-5" icon="material-symbols:add" />
<TheIcon :size="18" icon="material-symbols:add" />
添加用户
</n-button>
</template>
@@ -251,28 +251,20 @@ onMounted(() => {
</n-alert>
<n-space>
<n-button type="success" @click="handleStart">
<TheIcon
:size="24"
class="mr-5"
icon="material-symbols:play-arrow-outline-rounded"
/>
<TheIcon :size="24" icon="material-symbols:play-arrow-outline-rounded" />
启动
</n-button>
<n-popconfirm @positive-click="handleStop">
<template #trigger>
<n-button type="error">
<TheIcon
:size="24"
class="mr-5"
icon="material-symbols:stop-outline-rounded"
/>
<TheIcon :size="24" icon="material-symbols:stop-outline-rounded" />
停止
</n-button>
</template>
停止 Pure-Ftpd 会导致无法使用 FTP 服务确定要停止吗
</n-popconfirm>
<n-button type="warning" @click="handleRestart">
<TheIcon :size="18" class="mr-5" icon="material-symbols:replay-rounded" />
<TheIcon :size="18" icon="material-symbols:replay-rounded" />
重启
</n-button>
</n-space>

View File

@@ -100,7 +100,7 @@ onMounted(() => {
type="primary"
@click="handleSaveConfig"
>
<TheIcon :size="18" class="mr-5" icon="material-symbols:save-outline" />
<TheIcon :size="18" icon="material-symbols:save-outline" />
保存
</n-button>
</template>
@@ -120,28 +120,20 @@ onMounted(() => {
</n-alert>
<n-space>
<n-button type="success" @click="handleStart">
<TheIcon
:size="24"
class="mr-5"
icon="material-symbols:play-arrow-outline-rounded"
/>
<TheIcon :size="24" icon="material-symbols:play-arrow-outline-rounded" />
启动
</n-button>
<n-popconfirm @positive-click="handleStop">
<template #trigger>
<n-button type="error">
<TheIcon
:size="24"
class="mr-5"
icon="material-symbols:stop-outline-rounded"
/>
<TheIcon :size="24" icon="material-symbols:stop-outline-rounded" />
停止
</n-button>
</template>
停止 Redis 会导致使用 Redis 的网站无法访问确定要停止吗
</n-popconfirm>
<n-button type="warning" @click="handleRestart">
<TheIcon :size="18" class="mr-5" icon="material-symbols:replay-rounded" />
<TheIcon :size="18" icon="material-symbols:replay-rounded" />
重启
</n-button>
</n-space>

View File

@@ -244,7 +244,7 @@ onMounted(() => {
type="primary"
@click="handleSaveConfig"
>
<TheIcon :size="18" class="mr-5" icon="material-symbols:save-outline" />
<TheIcon :size="18" icon="material-symbols:save-outline" />
保存
</n-button>
<n-button
@@ -253,7 +253,7 @@ onMounted(() => {
type="primary"
@click="addModuleModal = true"
>
<TheIcon :size="18" class="mr-5" icon="material-symbols:add" />
<TheIcon :size="18" icon="material-symbols:add" />
添加模块
</n-button>
</template>
@@ -273,28 +273,20 @@ onMounted(() => {
</n-alert>
<n-space>
<n-button type="success" @click="handleStart">
<TheIcon
:size="24"
class="mr-5"
icon="material-symbols:play-arrow-outline-rounded"
/>
<TheIcon :size="24" icon="material-symbols:play-arrow-outline-rounded" />
启动
</n-button>
<n-popconfirm @positive-click="handleStop">
<template #trigger>
<n-button type="error">
<TheIcon
:size="24"
class="mr-5"
icon="material-symbols:stop-outline-rounded"
/>
<TheIcon :size="24" icon="material-symbols:stop-outline-rounded" />
停止
</n-button>
</template>
停止 Rsync 服务后将无法使用 Rsync 功能确定要停止吗
</n-popconfirm>
<n-button type="warning" @click="handleRestart">
<TheIcon :size="18" class="mr-5" icon="material-symbols:replay-rounded" />
<TheIcon :size="18" icon="material-symbols:replay-rounded" />
重启
</n-button>
</n-space>

View File

@@ -116,7 +116,7 @@ onMounted(() => {
<common-page show-footer>
<template #action>
<n-button class="ml-16" type="primary" @click="addMountModal = true">
<TheIcon :size="18" class="mr-5" icon="material-symbols:add" />
<TheIcon :size="18" icon="material-symbols:add" />
添加挂载
</n-button>
</template>

View File

@@ -368,7 +368,7 @@ onUnmounted(() => {
type="primary"
@click="handleSaveConfig"
>
<TheIcon :size="18" class="mr-5" icon="material-symbols:save-outline" />
<TheIcon :size="18" icon="material-symbols:save-outline" />
保存
</n-button>
<n-button
@@ -377,11 +377,11 @@ onUnmounted(() => {
type="primary"
@click="addProcessModal = true"
>
<TheIcon :size="18" class="mr-5" icon="material-symbols:add" />
<TheIcon :size="18" icon="material-symbols:add" />
添加进程
</n-button>
<n-button v-if="currentTab == 'log'" class="ml-16" type="primary" @click="handleClearLog">
<TheIcon :size="18" class="mr-5" icon="material-symbols:delete-outline" />
<TheIcon :size="18" icon="material-symbols:delete-outline" />
清空日志
</n-button>
</template>
@@ -401,32 +401,24 @@ onUnmounted(() => {
</n-alert>
<n-space>
<n-button type="success" @click="handleStart">
<TheIcon
:size="24"
class="mr-5"
icon="material-symbols:play-arrow-outline-rounded"
/>
<TheIcon :size="24" icon="material-symbols:play-arrow-outline-rounded" />
启动
</n-button>
<n-popconfirm @positive-click="handleStop">
<template #trigger>
<n-button type="error">
<TheIcon
:size="24"
class="mr-5"
icon="material-symbols:stop-outline-rounded"
/>
<TheIcon :size="24" icon="material-symbols:stop-outline-rounded" />
停止
</n-button>
</template>
停止 Supervisor 会导致 Supervisor 管理的所有进程被杀死确定要停止吗
</n-popconfirm>
<n-button type="warning" @click="handleRestart">
<TheIcon :size="18" class="mr-5" icon="material-symbols:replay-rounded" />
<TheIcon :size="18" icon="material-symbols:replay-rounded" />
重启
</n-button>
<n-button type="primary" @click="handleReload">
<TheIcon :size="20" class="mr-5" icon="material-symbols:refresh-rounded" />
<TheIcon :size="20" icon="material-symbols:refresh-rounded" />
重载
</n-button>
</n-space>

View File

@@ -82,15 +82,15 @@ onMounted(() => {
<common-page show-footer>
<template #action>
<n-button v-if="currentTab == 'dns'" class="ml-16" type="primary" @click="handleSaveDNS">
<TheIcon :size="18" class="mr-5" icon="material-symbols:save-outline" />
<TheIcon :size="18" icon="material-symbols:save-outline" />
保存
</n-button>
<n-button v-if="currentTab == 'swap'" class="ml-16" type="primary" @click="handleSaveSwap">
<TheIcon :size="18" class="mr-5" icon="material-symbols:save-outline" />
<TheIcon :size="18" icon="material-symbols:save-outline" />
保存
</n-button>
<n-button v-if="currentTab == 'hosts'" class="ml-16" type="primary" @click="handleSaveHosts">
<TheIcon :size="18" class="mr-5" icon="material-symbols:save-outline" />
<TheIcon :size="18" icon="material-symbols:save-outline" />
保存
</n-button>
<n-button
@@ -99,7 +99,7 @@ onMounted(() => {
type="primary"
@click="handleSaveTimezone"
>
<TheIcon :size="18" class="mr-5" icon="material-symbols:save-outline" />
<TheIcon :size="18" icon="material-symbols:save-outline" />
修改
</n-button>
<n-button
@@ -108,7 +108,7 @@ onMounted(() => {
type="primary"
@click="handleSaveRootPassword"
>
<TheIcon :size="18" class="mr-5" icon="material-symbols:save-outline" />
<TheIcon :size="18" icon="material-symbols:save-outline" />
修改
</n-button>
</template>

View File

@@ -30,7 +30,7 @@ const handleCreate = () => {
<template #action>
<div flex items-center>
<n-button class="ml-16" type="primary" @click="createModal = true">
<TheIcon :size="18" class="mr-5" icon="material-symbols:add" />
<TheIcon :size="18" icon="material-symbols:add" />
创建备份
</n-button>
</div>

View File

@@ -43,7 +43,7 @@ const uploadRequest = ({ file, onFinish, onError, onProgress }: UploadCustomRequ
>
<n-upload-dragger>
<div style="margin-bottom: 12px">
<the-icon :size="48" class="mr-5" icon="bi:arrow-up-square" />
<the-icon :size="48" icon="bi:arrow-up-square" />
</div>
<NText style="font-size: 16px"> 点击或者拖动文件到该区域来上传</NText>
<NP depth="3" style="margin: 8px 0 0 0"> 不支持断点续传大文件建议使用 FTP 上传 </NP>

View File

@@ -674,18 +674,14 @@ if (import.meta.hot) {
<n-popconfirm @positive-click="handleRestartPanel">
<template #trigger>
<n-button type="warning" size="small">
<TheIcon :size="20" class="mr-5" icon="mdi:restart" />
<TheIcon :size="20" icon="mdi:restart" />
重启面板
</n-button>
</template>
确定要重启面板吗
</n-popconfirm>
<n-button type="success" @click="handleUpdate" size="small">
<TheIcon
:size="20"
class="mr-5"
icon="mdi:arrow-up-bold-circle-outline"
/>
<TheIcon :size="20" icon="mdi:arrow-up-bold-circle-outline" />
检查更新
</n-button>
</n-space>

View File

@@ -62,11 +62,7 @@ onMounted(() => {
<template #action>
<div>
<n-button v-if="versions" class="ml-16" type="primary" @click="handleUpdate">
<TheIcon
:size="18"
class="mr-5"
icon="material-symbols:arrow-circle-up-outline-rounded"
/>
<TheIcon :size="18" icon="material-symbols:arrow-circle-up-outline-rounded" />
{{ $t('homeUpdate.button.update') }}
</n-button>
</div>

View File

@@ -498,7 +498,7 @@ onMounted(() => {
<n-popconfirm @positive-click="handleClear">
<template #trigger>
<n-button type="error">
<TheIcon :size="18" class="mr-5" icon="material-symbols:delete-outline" />
<TheIcon :size="18" icon="material-symbols:delete-outline" />
清除监控记录
</n-button>
</template>

View File

@@ -0,0 +1,149 @@
<script setup lang="ts">
import safe from '@/api/panel/safe'
import { NButton } from 'naive-ui'
const show = defineModel<boolean>('show', { type: Boolean, required: true })
const loading = ref(false)
const protocols = [
{
label: 'TCP',
value: 'tcp'
},
{
label: 'UDP',
value: 'udp'
},
{
label: 'TCP/UDP',
value: 'tcp/udp'
}
]
const families = [
{
label: 'IPv4',
value: 'ipv4'
},
{
label: 'IPv6',
value: 'ipv6'
}
]
const strategies = [
{
label: '接受',
value: 'accept'
},
{
label: '丢弃',
value: 'drop'
},
{
label: '拒绝',
value: 'reject'
}
]
const directions = [
{
label: '传入',
value: 'in'
},
{
label: '传出',
value: 'out'
}
]
const createModel = ref({
family: 'ipv4',
protocol: 'tcp',
port_start: 80,
port_end: 80,
address: '',
strategy: 'accept',
direction: 'in'
})
const handleCreate = async () => {
await safe.createFirewallRule(createModel.value).then(() => {
window.$message.success('创建成功')
createModel.value = {
family: 'ipv4',
protocol: 'tcp',
port_start: 80,
port_end: 80,
address: '',
strategy: 'accept',
direction: 'in'
}
})
}
</script>
<template>
<n-modal
v-model:show="show"
preset="card"
title="创建规则"
style="width: 60vw"
size="huge"
:bordered="false"
:segmented="false"
@close="show = false"
>
<n-form :model="createModel">
<n-form-item path="protocols" label="传输协议">
<n-select v-model:value="createModel.protocol" :options="protocols" />
</n-form-item>
<n-form-item path="family" label="网络协议">
<n-select v-model:value="createModel.family" :options="families" />
</n-form-item>
<n-row :gutter="[0, 24]">
<n-col :span="12">
<n-form-item path="port_start" label="起始端口">
<n-input-number
v-model:value="createModel.port_start"
:min="1"
:max="65535"
placeholder="80"
/>
</n-form-item>
</n-col>
<n-col :span="12">
<n-form-item path="port_end" label="结束端口">
<n-input-number
v-model:value="createModel.port_end"
:min="1"
:max="65535"
placeholder="80"
/>
</n-form-item>
</n-col>
</n-row>
<n-form-item path="address" label="目标">
<n-input
v-model:value="createModel.address"
placeholder="可选输入 IP 或 IP 段127.0.0.1 或 172.16.0.0/24多个以英文逗号隔开"
/>
</n-form-item>
<n-form-item path="strategy" label="策略">
<n-select v-model:value="createModel.strategy" :options="strategies" />
</n-form-item>
<n-form-item path="strategy" label="方向">
<n-select v-model:value="createModel.direction" :options="directions" />
</n-form-item>
</n-form>
<n-row :gutter="[0, 24]">
<n-col :span="24">
<n-button type="info" :loading="loading" :disabled="loading" block @click="handleCreate">
提交
</n-button>
</n-col>
</n-row>
</n-modal>
</template>
<style scoped lang="scss"></style>

View File

@@ -1,12 +1,12 @@
<script setup lang="ts">
import { NButton, NDataTable, NPopconfirm, NSpace } from 'naive-ui'
import { useI18n } from 'vue-i18n'
import { NButton, NDataTable, NPopconfirm, NSpace, NTag } from 'naive-ui'
import safe from '@/api/panel/safe'
import { renderIcon } from '@/utils'
import CreateModal from '@/views/safe/CreateModal.vue'
import type { FirewallRule } from '@/views/safe/types'
const { t } = useI18n()
const createModalShow = ref(false)
const model = ref({
firewallStatus: false,
@@ -18,38 +18,130 @@ const model = ref({
const columns: any = [
{ type: 'selection', fixed: 'left' },
{
title: t('safeIndex.columns.port'),
key: 'port',
width: 200,
resizable: true,
ellipsis: { tooltip: true }
},
{
title: t('safeIndex.columns.protocol'),
title: '传输协议',
key: 'protocol',
width: 150,
resizable: true,
ellipsis: { tooltip: true }
ellipsis: { tooltip: true },
render(row: any): any {
return h(NTag, null, {
default: () => {
if (row.protocol !== '') {
return row.protocol
}
return '无'
}
})
}
},
{
title: t('safeIndex.columns.actions'),
title: '网络协议',
key: 'family',
width: 150,
resizable: true,
ellipsis: { tooltip: true },
render(row: any): any {
return h(NTag, null, {
default: () => {
if (row.family !== '') {
return row.family
}
return '无'
}
})
}
},
{
title: '端口',
key: 'port',
width: 250,
resizable: true,
ellipsis: { tooltip: true },
render(row: any): any {
if (row.port_start == row.port_end) {
return row.port_start
}
return `${row.port_start}-${row.port_end}`
}
},
{
title: '策略',
key: 'strategy',
width: 150,
render(row: any): any {
return h(
NTag,
{
type:
row.strategy === 'accept' ? 'success' : row.strategy === 'drop' ? 'warning' : 'error'
},
{
default: () => {
switch (row.strategy) {
case 'accept':
return '接受'
case 'drop':
return '丢弃'
case 'reject':
return '拒绝'
default:
return '未知'
}
}
}
)
}
},
{
title: '方向',
key: 'direction',
width: 150,
render(row: any): any {
return h(NTag, null, {
default: () => {
switch (row.direction) {
case 'in':
return '传入'
case 'out':
return '传出'
default:
return '未知'
}
}
})
}
},
{
title: '目标',
key: 'address',
render(row: any): any {
return h(NTag, null, {
default: () => {
if (row.address === '') {
return '所有'
}
return row.address
}
})
}
},
{
title: '操作',
key: 'actions',
width: 140,
align: 'center',
fixed: 'right',
width: 150,
hideInExcel: true,
render(row: any) {
return [
h(
NPopconfirm,
{
onPositiveClick: () => handleDelete(row),
onNegativeClick: () => {
window.$message.info(t('safeIndex.alerts.undelete'))
}
onPositiveClick: () => handleDelete(row)
},
{
default: () => {
return t('safeIndex.confirm.delete')
return '确定要删除吗?'
},
trigger: () => {
return h(
@@ -60,7 +152,7 @@ const columns: any = [
style: 'margin-left: 15px;'
},
{
default: () => t('safeIndex.buttons.delete'),
default: () => '删除',
icon: renderIcon('material-symbols:delete-outline', { size: 14 })
}
)
@@ -86,41 +178,23 @@ const pagination = reactive({
const selectedRowKeys = ref<any>([])
const addModel = ref({
port: 80,
protocol: 'tcp'
})
const handleDelete = async (row: any) => {
await safe.deleteFirewallRule(row.port, row.protocol).then(() => {
window.$message.success(t('safeIndex.alerts.delete'))
await safe.deleteFirewallRule(row).then(() => {
window.$message.success('删除成功')
})
getFirewallRules(pagination.page, pagination.pageSize).then((res) => {
fetchFirewallRules(pagination.page, pagination.pageSize).then((res) => {
data.value = res.items
pagination.itemCount = res.total
pagination.pageCount = res.total / pagination.pageSize + 1
})
}
const handleAdd = async () => {
await safe.addFirewallRule(addModel.value.port, addModel.value.protocol).then(() => {
window.$message.success(t('safeIndex.alerts.add'))
addModel.value.port = 80
addModel.value.protocol = 'tcp'
})
getFirewallRules(pagination.page, pagination.pageSize).then((res) => {
data.value = res.items
pagination.itemCount = res.total
pagination.pageCount = res.total / pagination.pageSize + 1
})
}
const getFirewallRules = async (page: number, limit: number) => {
const fetchFirewallRules = async (page: number, limit: number) => {
const { data } = await safe.firewallRules(page, limit)
return data
}
const getSetting = async () => {
const fetchSetting = async () => {
safe.firewallStatus().then((res) => {
model.value.firewallStatus = res.data
})
@@ -135,44 +209,39 @@ const getSetting = async () => {
const handleFirewallStatus = () => {
safe.setFirewallStatus(model.value.firewallStatus).then(() => {
window.$message.success(t('safeIndex.alerts.setup'))
window.$message.success('设置成功')
})
}
const handleSsh = () => {
safe.setSsh(model.value.sshStatus, model.value.sshPort).then(() => {
window.$message.success(t('safeIndex.alerts.setup'))
window.$message.success('设置成功')
})
}
const handlePingStatus = () => {
safe.setPingStatus(model.value.pingStatus).then(() => {
window.$message.success(t('safeIndex.alerts.setup'))
window.$message.success('设置成功')
})
}
const batchDelete = async () => {
if (selectedRowKeys.value.length === 0) {
window.$message.info(t('safeIndex.alerts.select'))
window.$message.info('请选择要删除的规则')
return
}
for (const key of selectedRowKeys.value) {
// 通过 / 分割端口和协议
const [port, protocol] = key.split('/')
if (!port || !protocol) {
continue
}
await safe.deleteFirewallRule(port, protocol).then(() => {
let rule = data.value.find((item) => item.port === port && item.protocol === protocol)
window.$message.success(
t('safeIndex.alerts.ruleDelete', { rule: rule?.port + '/' + rule?.protocol })
)
// 解析json
const rule = JSON.parse(key)
await safe.deleteFirewallRule(rule).then(() => {
let port =
rule.port_start == rule.port_end ? rule.port_start : `${rule.port_start}-${rule.port_end}`
window.$message.success(`${rule.family} 规则 ${port}/${rule.protocol} 删除成功`)
})
}
getFirewallRules(pagination.page, pagination.pageSize).then((res) => {
fetchFirewallRules(pagination.page, pagination.pageSize).then((res) => {
data.value = res.items
pagination.itemCount = res.total
pagination.pageCount = res.total / pagination.pageSize + 1
@@ -185,7 +254,7 @@ const onChecked = (rowKeys: any) => {
const onPageChange = (page: number) => {
pagination.page = page
getFirewallRules(page, pagination.pageSize).then((res) => {
fetchFirewallRules(page, pagination.pageSize).then((res) => {
data.value = res.items
pagination.itemCount = res.total
pagination.pageCount = res.total / pagination.pageSize + 1
@@ -197,13 +266,13 @@ const onPageSizeChange = (pageSize: number) => {
onPageChange(1)
}
watch(createModalShow, () => {
onPageChange(1)
})
onMounted(() => {
getSetting()
getFirewallRules(pagination.page, pagination.pageSize).then((res) => {
data.value = res.items
pagination.itemCount = res.total
pagination.pageCount = res.total / pagination.pageSize + 1
})
fetchSetting()
onPageChange(1)
})
</script>
@@ -212,61 +281,34 @@ onMounted(() => {
<n-space vertical>
<n-card flex-1 rounded-10>
<n-form inline>
<n-form-item :label="$t('safeIndex.filter.fields.firewall.label')">
<n-switch
v-model:value="model.firewallStatus"
@update:value="handleFirewallStatus"
:checkedChildren="$t('safeIndex.filter.fields.firewall.checked')"
:unCheckedChildren="$t('safeIndex.filter.fields.firewall.unchecked')"
/>
<n-form-item label="防火墙">
<n-switch v-model:value="model.firewallStatus" @update:value="handleFirewallStatus" />
</n-form-item>
<n-form-item :label="$t('safeIndex.filter.fields.ssh.label')">
<n-switch
v-model:value="model.sshStatus"
@update:value="handleSsh"
:checkedChildren="$t('safeIndex.filter.fields.ssh.checked')"
:unCheckedChildren="$t('safeIndex.filter.fields.ssh.unchecked')"
/>
<n-form-item label="SSH">
<n-switch v-model:value="model.sshStatus" @update:value="handleSsh" />
</n-form-item>
<n-form-item :label="$t('safeIndex.filter.fields.ping.label')">
<n-switch
v-model:value="model.pingStatus"
@update:value="handlePingStatus"
:checkedChildren="$t('safeIndex.filter.fields.ping.checked')"
:unCheckedChildren="$t('safeIndex.filter.fields.ping.unchecked')"
/>
<n-form-item label="Ping">
<n-switch v-model:value="model.pingStatus" @update:value="handlePingStatus" />
</n-form-item>
<n-form-item :label="$t('safeIndex.filter.fields.port.label')">
<n-form-item label="SSH端口">
<n-input-number v-model:value="model.sshPort" @blur="handleSsh" />
</n-form-item>
</n-form>
</n-card>
<n-space flex items-center>
<n-button type="primary" @click="createModalShow = true">
<TheIcon :size="18" icon="material-symbols:add" />
创建规则
</n-button>
<n-popconfirm @positive-click="batchDelete">
<template #trigger>
<n-button type="warning"> {{ $t('safeIndex.buttons.batchDelete') }} </n-button>
<n-button type="warning">
<TheIcon :size="18" icon="material-symbols:delete-outline" />
批量删除
</n-button>
</template>
{{ $t('safeIndex.confirm.batchDelete') }}
确定要批量删除吗
</n-popconfirm>
<n-text>{{ $t('safeIndex.portControl.title') }}</n-text>
<n-input-number
v-model:value="addModel.port"
:placeholder="$t('safeIndex.portControl.fields.port.placeholder')"
:min="1"
:max="65535"
/>
<n-select
v-model:value="addModel.protocol"
:placeholder="$t('safeIndex.portControl.fields.protocol.placeholder')"
style="width: 120px"
:options="[
{ label: 'TCP', value: 'tcp' },
{ label: 'UDP', value: 'udp' }
]"
/>
<n-button type="primary" @click="handleAdd">
{{ $t('safeIndex.buttons.add') }}
</n-button>
</n-space>
<n-data-table
@@ -276,7 +318,7 @@ onMounted(() => {
:scroll-x="1200"
:columns="columns"
:data="data"
:row-key="(row: any) => row.port + '/' + row.protocol"
:row-key="(row: any) => JSON.stringify(row)"
:pagination="pagination"
@update:checked-row-keys="onChecked"
@update:page="onPageChange"
@@ -284,6 +326,7 @@ onMounted(() => {
/>
</n-space>
</common-page>
<create-modal v-model:show="createModalShow" />
</template>
<style scoped lang="scss"></style>

View File

@@ -15,7 +15,7 @@ export default {
path: '',
component: () => import('./IndexView.vue'),
meta: {
title: 'safeIndex.title',
title: '系统安全',
icon: 'mdi:server-security',
role: ['admin'],
requireAuth: true

View File

@@ -1,4 +1,7 @@
export interface FirewallRule {
port: string
protocol: string
port_start: number
port_end: number
protocols: string[]
address: string
strategy: string
}

View File

@@ -125,7 +125,7 @@ onMounted(() => {
<div flex items-center>
<n-tag type="warning">如果您修改了原文那么点击保存后其余的修改将不会生效</n-tag>
<n-button class="ml-16" type="primary" @click="handleSave">
<TheIcon :size="18" class="mr-5" icon="material-symbols:save-outline" />
<TheIcon :size="18" icon="material-symbols:save-outline" />
保存
</n-button>
</div>
@@ -287,7 +287,7 @@ onMounted(() => {
<n-popconfirm @positive-click="handleReset">
<template #trigger>
<n-button type="success">
<TheIcon :size="18" class="mr-5" icon="material-symbols:refresh" />
<TheIcon :size="18" icon="material-symbols:refresh" />
重置配置
</n-button>
</template>