2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 13:47:15 +08:00
Files
panel/web/src/views/apps/fail2ban/IndexView.vue
2025-05-15 17:56:54 +08:00

508 lines
15 KiB
Vue

<script setup lang="ts">
defineOptions({
name: 'apps-fail2ban-index'
})
import { NButton, NDataTable, NInput, NPopconfirm, NSwitch } from 'naive-ui'
import { useGettext } from 'vue3-gettext'
import fail2ban from '@/api/apps/fail2ban'
import app from '@/api/panel/app'
import systemctl from '@/api/panel/systemctl'
import website from '@/api/panel/website'
import { renderIcon } from '@/utils'
const { $gettext } = useGettext()
const currentTab = ref('status')
const status = ref(false)
const isEnabled = ref(false)
const white = ref('')
const addJailModal = ref(false)
const addJailModel = ref({
name: 'ssh',
type: 'website',
maxretry: 30,
findtime: 300,
bantime: 600,
website_name: '',
website_mode: 'cc',
website_path: '/'
})
const jailModal = ref(false)
const jailCurrentlyBan = ref(0)
const jailTotalBan = ref(0)
const jailBanedList = ref<any[]>([])
const statusType = computed(() => {
return status.value ? 'success' : 'error'
})
const statusStr = computed(() => {
return status.value ? $gettext('Running') : $gettext('Stopped')
})
const jailsColumns: any = [
{
title: $gettext('Name'),
key: 'name',
minWidth: 250,
ellipsis: { tooltip: true }
},
{
title: $gettext('Status'),
key: 'enabled',
minWidth: 60,
render(row: any) {
return h(NSwitch, {
size: 'small',
rubberBand: false,
value: row.enabled,
disabled: true
})
}
},
{ title: $gettext('Max Retries'), key: 'max_retry', minWidth: 150, ellipsis: { tooltip: true } },
{ title: $gettext('Ban Time'), key: 'ban_time', minWidth: 150, ellipsis: { tooltip: true } },
{ title: $gettext('Find Time'), key: 'find_time', minWidth: 150, ellipsis: { tooltip: true } },
{
title: $gettext('Actions'),
key: 'actions',
width: 280,
hideInExcel: true,
render(row: any) {
return [
h(
NButton,
{
size: 'small',
type: 'warning',
secondary: true,
onClick: async () => {
await getJailInfo(row.name)
jailModal.value = true
}
},
{
default: () => $gettext('View'),
icon: renderIcon('material-symbols:visibility', { size: 14 })
}
),
h(
NPopconfirm,
{
onPositiveClick: () => handleDeleteJail(row.name)
},
{
default: () => {
return $gettext('Are you sure you want to delete rule %{ name }?', { name: row.name })
},
trigger: () => {
return h(
NButton,
{
size: 'small',
type: 'error',
style: 'margin-left: 15px'
},
{
default: () => $gettext('Delete'),
icon: renderIcon('material-symbols:delete-outline', { size: 14 })
}
)
}
}
)
]
}
}
]
const banedIPColumns: any = [
{
title: 'IP',
key: 'ip',
minWidth: 200,
resizable: true,
ellipsis: { tooltip: true }
},
{
title: $gettext('Actions'),
key: 'actions',
width: 100,
hideInExcel: true,
render(row: any) {
return [
h(
NPopconfirm,
{
onPositiveClick: () => handleUnBan(row.name, row.ip)
},
{
default: () => {
return $gettext('Are you sure you want to unban %{ ip }?', { ip: row.ip })
},
trigger: () => {
return h(
NButton,
{
size: 'small',
type: 'error'
},
{
default: () => $gettext('Unban'),
icon: renderIcon('material-symbols:delete-outline', { size: 14 })
}
)
}
}
)
]
}
}
]
const websites = ref<any[]>([])
const getWhiteList = async () => {
white.value = await fail2ban.whitelist()
}
const handleSaveWhiteList = () => {
useRequest(fail2ban.setWhitelist(white.value)).onSuccess(() => {
window.$message.success($gettext('Saved successfully'))
})
}
const getWebsiteList = async (page: number, limit: number) => {
const data = await website.list(page, limit)
for (const item of data.items) {
websites.value.push({
label: item.name,
value: item.name
})
}
addJailModel.value.website_name = websites.value[0]?.value
}
const { loading, data, page, total, pageSize, pageCount, refresh } = usePagination(
(page, pageSize) => fail2ban.jails(page, pageSize),
{
initialData: { total: 0, list: [] },
initialPageSize: 20,
total: (res: any) => res.total,
data: (res: any) => res.items
}
)
const getStatus = async () => {
status.value = await systemctl.status('fail2ban')
}
const getIsEnabled = async () => {
isEnabled.value = await systemctl.isEnabled('fail2ban')
}
const handleStart = async () => {
await systemctl.start('fail2ban')
window.$message.success($gettext('Started successfully'))
await getStatus()
}
const handleIsEnabled = async () => {
if (isEnabled.value) {
await systemctl.enable('fail2ban')
window.$message.success($gettext('Autostart enabled successfully'))
} else {
await systemctl.disable('fail2ban')
window.$message.success($gettext('Autostart disabled successfully'))
}
await getIsEnabled()
}
const handleStop = async () => {
await systemctl.stop('fail2ban')
window.$message.success($gettext('Stopped successfully'))
await getStatus()
}
const handleRestart = async () => {
await systemctl.restart('fail2ban')
window.$message.success($gettext('Restarted successfully'))
await getStatus()
}
const handleReload = async () => {
await systemctl.reload('fail2ban')
window.$message.success($gettext('Reloaded successfully'))
await getStatus()
}
const handleAddJail = () => {
useRequest(fail2ban.add(addJailModel.value)).onSuccess(() => {
refresh()
window.$message.success($gettext('Added successfully'))
addJailModal.value = false
})
}
const handleDeleteJail = (name: string) => {
useRequest(fail2ban.delete(name)).onSuccess(() => {
refresh()
window.$message.success($gettext('Deleted successfully'))
})
}
const getJailInfo = async (name: string) => {
const data = await fail2ban.jail(name)
jailCurrentlyBan.value = data.currently_ban
jailTotalBan.value = data.total_ban
jailBanedList.value = data.baned_list
}
const handleUnBan = (name: string, ip: string) => {
useRequest(fail2ban.unban(name, ip)).onSuccess(() => {
window.$message.success($gettext('Unbanned successfully'))
getJailInfo(name)
})
}
onMounted(() => {
refresh()
getStatus()
getIsEnabled()
getWhiteList()
useRequest(app.isInstalled('nginx')).onSuccess(({ data }) => {
if (data.installed) {
getWebsiteList(1, 10000)
}
})
})
</script>
<template>
<common-page show-footer>
<template #action>
<n-button
v-if="currentTab == 'status'"
class="ml-16"
type="primary"
@click="handleSaveWhiteList"
>
<the-icon :size="18" icon="material-symbols:save-outline" />
{{ $gettext('Save Whitelist') }}
</n-button>
<n-button
v-if="currentTab == 'jails'"
class="ml-16"
type="primary"
@click="addJailModal = true"
>
<the-icon :size="18" icon="material-symbols:add" />
{{ $gettext('Add Rule') }}
</n-button>
</template>
<n-tabs v-model:value="currentTab" type="line" animated>
<n-tab-pane name="status" :tab="$gettext('Running Status')">
<n-space vertical>
<n-card :title="$gettext('Running Status')">
<template #header-extra>
<n-switch v-model:value="isEnabled" @update:value="handleIsEnabled">
<template #checked> {{ $gettext('Autostart On') }} </template>
<template #unchecked> {{ $gettext('Autostart Off') }} </template>
</n-switch>
</template>
<n-space vertical>
<n-alert :type="statusType">
{{ statusStr }}
</n-alert>
<n-space>
<n-button type="success" @click="handleStart">
<the-icon :size="24" icon="material-symbols:play-arrow-outline-rounded" />
{{ $gettext('Start') }}
</n-button>
<n-popconfirm @positive-click="handleStop">
<template #trigger>
<n-button type="error">
<the-icon :size="24" icon="material-symbols:stop-outline-rounded" />
{{ $gettext('Stop') }}
</n-button>
</template>
{{
$gettext(
'Stopping Fail2ban will disable all rules. Are you sure you want to stop?'
)
}}
</n-popconfirm>
<n-button type="warning" @click="handleRestart">
<the-icon :size="18" icon="material-symbols:replay-rounded" />
{{ $gettext('Restart') }}
</n-button>
<n-button type="primary" @click="handleReload">
<the-icon :size="20" icon="material-symbols:refresh-rounded" />
{{ $gettext('Reload') }}
</n-button>
</n-space>
</n-space>
</n-card>
<n-card :title="$gettext('IP Whitelist')">
<n-input
v-model:value="white"
type="textarea"
autosize
:placeholder="$gettext('IP whitelist, separated by commas')"
/>
</n-card>
</n-space>
</n-tab-pane>
<n-tab-pane name="jails" :tab="$gettext('Rule Management')">
<n-card :title="$gettext('Rule List')" :segmented="true">
<n-data-table
striped
remote
:scroll-x="1000"
:loading="loading"
:columns="jailsColumns"
:data="data"
:row-key="(row: any) => row.name"
v-model:page="page"
v-model:pageSize="pageSize"
:pagination="{
page: page,
pageCount: pageCount,
pageSize: pageSize,
itemCount: total,
showQuickJumper: true,
showSizePicker: true,
pageSizes: [20, 50, 100, 200]
}"
/>
</n-card>
</n-tab-pane>
<n-tab-pane name="run-log" :tab="$gettext('Runtime Logs')">
<realtime-log service="fail2ban" />
</n-tab-pane>
</n-tabs>
</common-page>
<n-modal v-model:show="addJailModal" :title="$gettext('Add Rule')">
<n-card
closable
@close="() => (addJailModal = false)"
:title="$gettext('Add Rule')"
style="width: 60vw"
>
<n-space vertical>
<n-alert type="info">
{{
$gettext(
'If an IP exceeds the maximum retries within the find time (seconds), it will be banned for the ban time (seconds)'
)
}}
</n-alert>
<n-alert type="warning">
{{
$gettext(
'Protected ports are automatically obtained. If you modify the port corresponding to a rule, please delete and re-add the rule, otherwise protection may not be effective'
)
}}
</n-alert>
<n-form :model="addJailModel">
<n-form-item :label="$gettext('Type')">
<n-select
v-model:value="addJailModel.type"
:options="[
{ label: $gettext('Website'), value: 'website' },
{ label: $gettext('Service'), value: 'service' }
]"
>
</n-select>
</n-form-item>
<n-form-item v-if="addJailModel.type === 'website'" :label="$gettext('Select Website')">
<n-select
v-model:value="addJailModel.website_name"
:options="websites"
:placeholder="$gettext('Select Website')"
/>
</n-form-item>
<n-form-item v-if="addJailModel.type === 'website'" :label="$gettext('Protection Mode')">
<n-select
v-model:value="addJailModel.website_mode"
:options="[
{ label: 'CC', value: 'cc' },
{ label: $gettext('Path'), value: 'path' }
]"
>
</n-select>
</n-form-item>
<n-form-item
v-if="addJailModel.type === 'website' && addJailModel.website_mode === 'path'"
:label="$gettext('Protection Path')"
>
<n-input
v-model:value="addJailModel.website_path"
:placeholder="$gettext('Protection Path')"
/>
</n-form-item>
<n-form-item v-if="addJailModel.type === 'service'" :label="$gettext('Service')">
<n-select
v-model:value="addJailModel.name"
:options="[
{ label: 'SSH', value: 'ssh' },
{ label: 'MySQL', value: 'mysql' },
{ label: 'Pure-Ftpd', value: 'pure-ftpd' }
]"
>
</n-select>
</n-form-item>
<n-form-item path="maxretry" :label="$gettext('Max Retries')">
<n-input-number v-model:value="addJailModel.maxretry" @keydown.enter.prevent :min="1" />
</n-form-item>
<n-form-item path="findtime" :label="$gettext('Find Time')">
<n-input-number v-model:value="addJailModel.findtime" @keydown.enter.prevent :min="1" />
</n-form-item>
<n-form-item path="bantime" :label="$gettext('Ban Time')">
<n-input-number v-model:value="addJailModel.bantime" @keydown.enter.prevent :min="1" />
</n-form-item>
</n-form>
<n-button type="info" block @click="handleAddJail">{{ $gettext('Submit') }}</n-button>
</n-space>
</n-card>
</n-modal>
<n-modal v-model:show="jailModal" :title="$gettext('View Rule')">
<n-card
closable
@close="() => (jailModal = false)"
:title="$gettext('View Rule')"
style="width: 60vw"
>
<n-space vertical>
<n-card :title="$gettext('Rule Information')" :segmented="true">
<n-space vertical>
<n-space>
<n-text>{{ $gettext('Currently Banned') }}</n-text>
<n-text>{{ jailCurrentlyBan }}</n-text>
</n-space>
<n-space>
<n-text>{{ $gettext('Total Bans') }}</n-text>
<n-text>{{ jailTotalBan }}</n-text>
</n-space>
</n-space>
</n-card>
<n-card :title="$gettext('Ban List')" :segmented="true">
<n-data-table
striped
remote
:scroll-x="300"
:loading="false"
:columns="banedIPColumns"
:data="jailBanedList"
:row-key="(row: any) => row.ip"
:pagination="false"
/>
</n-card>
</n-space>
</n-card>
</n-modal>
</template>