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:
2026-08-13 02:26:17 +08:00
parent 0752a2fbf6
commit ab70a5eae9
32 changed files with 86 additions and 912 deletions
@@ -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>