Files
wordpress-theme-aksrt/assets/admin/src/pages/extensions/ExtensionSecurity.vue
T
kate522 3675b7dcef fix(extensions): IP 归属地管理端测试可绕过启用开关
后台测试此前受 ip_geo_enabled 短路,未启用时结果恒为空;强制 live lookup 并返回可读失败原因。
2026-07-12 14:35:19 +08:00

300 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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 || {}
securityItems.value[0].value = d.email_gate_enabled ? '已开启' : '未开启'
securityItems.value[0].color = d.email_gate_enabled ? '#52c41a' : '#8a909c'
securityItems.value[1].value = d.captcha_enabled ? '已开启' : '未开启'
securityItems.value[1].color = d.captcha_enabled ? '#52c41a' : '#8a909c'
if (svc.audit?.enabled) {
securityItems.value[2].value = svc.audit.configured ? '已配置' : '待补全'
securityItems.value[2].color = svc.audit.configured ? '#52c41a' : '#faad14'
} else {
securityItems.value[2].value = '未开启'
securityItems.value[2].color = '#8a909c'
}
if (svc.ip_geo?.enabled) {
securityItems.value[3].value = svc.ip_geo.configured ? '已配置' : '待补全'
securityItems.value[3].color = svc.ip_geo.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 = !!d.captcha_comment_enabled
features.value[2].enabled = !!d.captcha_download_enabled
features.value[3].enabled = !!(svc.audit?.enabled ?? d.content_audit_enabled)
features.value[4].enabled = !!(svc.ip_geo?.enabled ?? d.ip_geo_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
try {
const res = await api.testContentAudit(auditTest.value.text)
auditTest.value.result = JSON.stringify(res?.data || {}, null, 2)
message.success('内容审核测试完成')
} 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>