refactor(admin): consolidate duplicate configuration
Remove duplicate comment, captcha, filing, and website-security configuration paths. Move front submission verification to the captcha scene matrix and retire obsolete admin routes and components.
This commit is contained in:
@@ -1,311 +0,0 @@
|
||||
<template>
|
||||
<ExtensionsSectionPage
|
||||
title="网站安全"
|
||||
desc="扩展安全策略和第三方安全集成。"
|
||||
>
|
||||
<n-card title="安全概览" size="small" class="aksrt-security__section">
|
||||
<n-grid :cols="4" :x-gap="12" :y-gap="12">
|
||||
<n-grid-item v-for="item in securityItems" :key="item.key">
|
||||
<div class="aksrt-security__overview-card">
|
||||
<FaIcon :icon="item.icon" :size="18" :color="item.color" />
|
||||
<div>
|
||||
<strong>{{ item.value }}</strong>
|
||||
<p>{{ item.label }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</n-card>
|
||||
|
||||
<n-card title="安全能力" size="small" class="aksrt-security__section">
|
||||
<n-list bordered size="small">
|
||||
<n-list-item v-for="feat in features" :key="feat.key">
|
||||
<n-thing :title="feat.name" :description="feat.description">
|
||||
<template #avatar>
|
||||
<FaIcon :icon="feat.icon" :size="16" :color="feat.color" />
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<n-space align="center" size="small">
|
||||
<n-tag :type="feat.enabled ? 'success' : 'default'" size="small">
|
||||
{{ feat.enabled ? '已启用' : '未启用' }}
|
||||
</n-tag>
|
||||
<router-link v-if="feat.settingsRoute" :to="feat.settingsRoute" class="aksrt-security__config-link">
|
||||
前往配置
|
||||
</router-link>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</n-card>
|
||||
|
||||
<n-card title="在线测试" size="small" class="aksrt-security__section">
|
||||
<n-grid :cols="2" :x-gap="16" :y-gap="16">
|
||||
<n-grid-item>
|
||||
<n-form label-placement="top">
|
||||
<n-form-item label="IP 归属地测试">
|
||||
<n-input v-model:value="ipTest.ip" placeholder="输入 IPv4 / IPv6,例如 8.8.8.8" />
|
||||
</n-form-item>
|
||||
<n-space>
|
||||
<n-button :loading="ipTest.loading" @click="handleTestIp">测试 IP 接口</n-button>
|
||||
</n-space>
|
||||
<pre v-if="ipTest.result" class="aksrt-security__output">{{ ipTest.result }}</pre>
|
||||
</n-form>
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<n-form label-placement="top">
|
||||
<n-form-item label="内容审核测试">
|
||||
<n-input v-model:value="auditTest.text" type="textarea" :autosize="{ minRows: 4, maxRows: 8 }" placeholder="输入一段测试文本,检查当前审核策略是否生效" />
|
||||
</n-form-item>
|
||||
<n-space>
|
||||
<n-button :loading="auditTest.loading" @click="handleTestAudit">测试内容审核</n-button>
|
||||
</n-space>
|
||||
<pre v-if="auditTest.result" class="aksrt-security__output">{{ auditTest.result }}</pre>
|
||||
</n-form>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</n-card>
|
||||
|
||||
<n-card title="风险开关" size="small" class="aksrt-security__section">
|
||||
<n-alert type="warning" size="small" style="margin-bottom: 12px;">
|
||||
以下开关会影响站点安全策略,请谨慎操作。
|
||||
</n-alert>
|
||||
<n-form label-placement="left" label-width="200" size="small">
|
||||
<n-form-item label="允许未审核评论显示">
|
||||
<n-switch v-model:value="riskSwitches.show_unapproved_comments" />
|
||||
<n-text depth="3" style="margin-left: 8px; font-size: 12px;">
|
||||
开启后未审核评论也会在前台显示(标记为待审核)
|
||||
</n-text>
|
||||
</n-form-item>
|
||||
<n-form-item label="允许前台投稿无需审核">
|
||||
<n-switch v-model:value="riskSwitches.skip_submit_moderation" />
|
||||
<n-text depth="3" style="margin-left: 8px; font-size: 12px;">
|
||||
开启后用户投稿直接发布,不经过审核流程
|
||||
</n-text>
|
||||
</n-form-item>
|
||||
<n-form-item label="允许游客评论">
|
||||
<n-switch v-model:value="riskSwitches.guest_comment" />
|
||||
<n-text depth="3" style="margin-left: 8px; font-size: 12px;">
|
||||
开启后未登录用户也可以发表评论
|
||||
</n-text>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<template #action>
|
||||
<n-space justify="end">
|
||||
<n-button type="primary" size="small" :loading="saving" @click="handleSaveRisk">保存风险设置</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-card>
|
||||
|
||||
<n-card title="安全审计" size="small" class="aksrt-security__section">
|
||||
<n-empty v-if="auditLogs.length === 0" description="暂无审计记录" />
|
||||
<n-list v-else bordered size="small">
|
||||
<n-list-item v-for="log in auditLogs" :key="log.id">
|
||||
<n-thing :title="log.action" :description="log.detail">
|
||||
<template #header-extra>
|
||||
<n-text depth="3" style="font-size: 11px;">{{ log.time }}</n-text>
|
||||
</template>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</n-card>
|
||||
</ExtensionsSectionPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import ExtensionsSectionPage from '@/pages/extensions/ExtensionsSectionPage.vue'
|
||||
import FaIcon from '@/components/form/FaIcon.vue'
|
||||
import api from '@/api/index.js'
|
||||
|
||||
const message = useMessage()
|
||||
const saving = ref(false)
|
||||
|
||||
const securityItems = ref([
|
||||
{ key: 'email_gate', icon: 'fas fa-envelope', color: '#1890ff', value: '—', label: '邮箱验证' },
|
||||
{ key: 'captcha', icon: 'fas fa-shield-alt', color: '#52c41a', value: '—', label: '验证码保护' },
|
||||
{ key: 'moderation', icon: 'fas fa-gavel', color: '#722ed1', value: '—', label: '内容审核' },
|
||||
{ key: 'ip_geo', icon: 'fas fa-map-marker-alt', color: '#fa8c16', value: '—', label: 'IP 归属地' },
|
||||
])
|
||||
|
||||
const features = ref([
|
||||
{ key: 'email_verification', icon: 'fas fa-envelope', color: '#1890ff', name: '邮箱验证', description: '新用户注册需要邮箱验证', enabled: false, settingsRoute: '/users/auth' },
|
||||
{ key: 'captcha_comment', icon: 'fas fa-shield-alt', color: '#52c41a', name: '评论验证码', description: '发表评论时需要人机验证', enabled: false, settingsRoute: '/permissions/login' },
|
||||
{ key: 'captcha_download', icon: 'fas fa-shield-alt', color: '#52c41a', name: '下载验证码', description: '下载资源时需要人机验证', enabled: false, settingsRoute: '/permissions/login' },
|
||||
{ key: 'content_audit', icon: 'fas fa-shield-virus', color: '#722ed1', name: '内容审核 API', description: '接入第三方内容安全审核', enabled: false, settingsRoute: '/extensions/audit' },
|
||||
{ key: 'ip_geolocation', icon: 'fas fa-map-marker-alt', color: '#fa8c16', name: 'IP 归属地', description: '在评论和用户主页显示 IP 归属地', enabled: false, settingsRoute: '/extensions/ip' },
|
||||
])
|
||||
|
||||
const riskSwitches = ref({
|
||||
show_unapproved_comments: false,
|
||||
skip_submit_moderation: false,
|
||||
guest_comment: false,
|
||||
})
|
||||
|
||||
const auditLogs = ref([])
|
||||
const ipTest = ref({ ip: '8.8.8.8', loading: false, result: '' })
|
||||
const auditTest = ref({ text: '这是一段用于审核测试的示例文本。', loading: false, result: '' })
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await api.request('/settings/security')
|
||||
if (res?.data) {
|
||||
const d = res.data
|
||||
const svc = res.services || {}
|
||||
|
||||
const captcha = d.captcha || svc.captcha || {}
|
||||
const captchaOn = !!(captcha.enabled && captcha.configured)
|
||||
const captchaScenes = captcha.scenes || {}
|
||||
const audit = d.content_audit || svc.audit || {}
|
||||
const ipGeo = d.ip_geo || svc.ip_geo || {}
|
||||
|
||||
securityItems.value[0].value = d.email_gate_enabled ? '已开启' : '未开启'
|
||||
securityItems.value[0].color = d.email_gate_enabled ? '#52c41a' : '#8a909c'
|
||||
securityItems.value[1].value = captchaOn ? '已开启' : '未开启'
|
||||
securityItems.value[1].color = captchaOn ? '#52c41a' : '#8a909c'
|
||||
|
||||
if (audit.enabled) {
|
||||
securityItems.value[2].value = audit.configured ? '已配置' : '待补全'
|
||||
securityItems.value[2].color = audit.configured ? '#52c41a' : '#faad14'
|
||||
} else {
|
||||
securityItems.value[2].value = '未开启'
|
||||
securityItems.value[2].color = '#8a909c'
|
||||
}
|
||||
|
||||
if (ipGeo.enabled) {
|
||||
securityItems.value[3].value = ipGeo.configured ? '已配置' : '待补全'
|
||||
securityItems.value[3].color = ipGeo.configured ? '#52c41a' : '#faad14'
|
||||
} else {
|
||||
securityItems.value[3].value = '未开启'
|
||||
securityItems.value[3].color = '#8a909c'
|
||||
}
|
||||
|
||||
features.value[0].enabled = !!d.email_gate_enabled
|
||||
features.value[1].enabled = !!captchaScenes.comment
|
||||
features.value[2].enabled = !!captcha.download_active
|
||||
features.value[3].enabled = !!audit.enabled
|
||||
features.value[4].enabled = !!ipGeo.enabled
|
||||
|
||||
if (d.risk_switches) {
|
||||
riskSwitches.value = { ...riskSwitches.value, ...d.risk_switches }
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Endpoint may not exist yet
|
||||
}
|
||||
|
||||
try {
|
||||
const logRes = await api.request('/security/audit-log')
|
||||
if (logRes?.data?.logs) {
|
||||
auditLogs.value = logRes.data.logs.slice(0, 10)
|
||||
}
|
||||
} catch {
|
||||
// Endpoint may not exist yet
|
||||
}
|
||||
})
|
||||
|
||||
async function handleSaveRisk() {
|
||||
saving.value = true
|
||||
try {
|
||||
await api.request('/settings/security', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ risk_switches: riskSwitches.value }),
|
||||
})
|
||||
message.success('风险设置已保存')
|
||||
} catch (err) {
|
||||
message.error('保存失败: ' + (err?.message || '未知错误'))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTestIp() {
|
||||
ipTest.value.loading = true
|
||||
ipTest.value.result = ''
|
||||
try {
|
||||
const res = await api.testIpGeo(String(ipTest.value.ip || '').trim())
|
||||
const data = res?.data || {}
|
||||
ipTest.value.result = JSON.stringify(data, null, 2)
|
||||
if (data.ok) {
|
||||
message.success(data.display ? `IP 测试成功:${data.display}` : 'IP 接口测试完成')
|
||||
} else {
|
||||
message.warning(data.message || '未查询到归属地信息')
|
||||
}
|
||||
} catch (err) {
|
||||
message.error(err?.message || 'IP 测试失败')
|
||||
} finally {
|
||||
ipTest.value.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTestAudit() {
|
||||
auditTest.value.loading = true
|
||||
auditTest.value.result = ''
|
||||
try {
|
||||
const res = await api.testContentAudit(String(auditTest.value.text || '').trim())
|
||||
const data = res?.data || {}
|
||||
auditTest.value.result = JSON.stringify(data, null, 2)
|
||||
if (data.ok) {
|
||||
message.success(data.message || '内容审核测试完成')
|
||||
} else {
|
||||
message.warning(data.message || '内容审核未通过或配置异常')
|
||||
}
|
||||
} catch (err) {
|
||||
message.error(err?.message || '内容审核测试失败')
|
||||
} finally {
|
||||
auditTest.value.loading = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.aksrt-security__section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.aksrt-security__overview-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 14px;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.aksrt-security__overview-card strong {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
.aksrt-security__overview-card p {
|
||||
margin: 2px 0 0;
|
||||
font-size: 12px;
|
||||
color: #8a909c;
|
||||
}
|
||||
|
||||
.aksrt-security__config-link {
|
||||
font-size: 12px;
|
||||
color: #1890ff;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aksrt-security__config-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.aksrt-security__output {
|
||||
margin: 12px 0 0;
|
||||
padding: 12px 14px;
|
||||
border-radius: 12px;
|
||||
background: #101828;
|
||||
color: #d0d7e2;
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user