XuqmGroup-Web/tenant-platform/src/views/security/SecurityCenterView.vue

214 行
6.7 KiB
Vue

<template>
<div>
<div class="page-header">
<div>
<h2>安全中心</h2>
<p class="subtitle">管理登录安全应用密钥和子账号风险控制</p>
</div>
<el-button @click="$router.push('/accounts')">子账号管理</el-button>
</div>
<el-row :gutter="16" style="margin-bottom: 16px">
<el-col :xs="24" :md="8">
<el-card shadow="hover" class="summary-card">
<el-statistic title="当前账号" :value="auth.user?.nickname || auth.user?.username || '-'" />
</el-card>
</el-col>
<el-col :xs="24" :md="8">
<el-card shadow="hover" class="summary-card">
<el-statistic title="应用数量" :value="apps.length" />
</el-card>
</el-col>
<el-col :xs="24" :md="8">
<el-card shadow="hover" class="summary-card">
<el-statistic title="子账号数量" :value="subAccountCount" />
</el-card>
</el-col>
</el-row>
<el-card style="margin-bottom: 16px">
<template #header>账号保护建议</template>
<el-space wrap>
<el-tag type="success">强密码</el-tag>
<el-tag type="warning">邮箱验证</el-tag>
<el-tag type="info">子账号最小权限</el-tag>
<el-tag type="info">密钥定期轮换</el-tag>
</el-space>
<el-divider />
<el-button type="primary" plain @click="$router.push('/forgot-password')">重置当前账号密码</el-button>
</el-card>
<el-card>
<template #header>应用密钥管理</template>
<el-table :data="apps" v-loading="loading" border stripe>
<el-table-column prop="name" label="应用名称" min-width="160" />
<el-table-column prop="packageName" label="包名" min-width="180" />
<el-table-column prop="appKey" label="AppKey" min-width="220" show-overflow-tooltip />
<el-table-column prop="createdAt" label="创建时间" width="180">
<template #default="{ row }">{{ fmt(row.createdAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openSecretDialog(row, 'REVEAL_SECRET')">查看密钥</el-button>
<el-button link type="warning" @click="openSecretDialog(row, 'RESET_SECRET')">重置密钥</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog
v-model="showDialog"
:title="dialogMode === 'REVEAL_SECRET' ? '查看 AppSecret' : '重置 AppSecret'"
width="460px"
@closed="closeDialog"
>
<div v-if="!codeSent">
<p class="dialog-text">
{{ dialogMode === 'REVEAL_SECRET'
? '系统将向租户邮箱发送验证码,验证后可查看密钥。'
: '系统将向租户邮箱发送验证码,验证后会立即重置密钥。' }}
</p>
<el-button type="primary" :loading="sendingCode" @click="sendVerifyCode">发送验证码</el-button>
</div>
<div v-else>
<p class="dialog-text">请输入邮箱验证码</p>
<el-input v-model="verifyCode" maxlength="6" placeholder="6位验证码" />
</div>
<template #footer>
<el-button @click="showDialog = false">取消</el-button>
<el-button v-if="codeSent" type="primary" :loading="submitting" @click="submitVerify">
确认
</el-button>
</template>
</el-dialog>
<el-dialog v-model="showResult" title="AppSecret" width="420px">
<el-alert type="success" :closable="false" show-icon>
<template #title>操作已完成</template>
<template #default>
<div class="secret-box">{{ secretResult || '无结果' }}</div>
</template>
</el-alert>
<template #footer>
<el-button type="primary" @click="showResult = false">知道了</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { accountApi } from '@/api/account'
import { appApi, type App } from '@/api/app'
import { useAuthStore } from '@/stores/auth'
const auth = useAuthStore()
const apps = ref<App[]>([])
const loading = ref(false)
const subAccountCount = ref(0)
const showDialog = ref(false)
const showResult = ref(false)
const sendingCode = ref(false)
const submitting = ref(false)
const codeSent = ref(false)
const verifyCode = ref('')
const selectedApp = ref<App | null>(null)
const dialogMode = ref<'REVEAL_SECRET' | 'RESET_SECRET'>('REVEAL_SECRET')
const secretResult = ref('')
async function loadData() {
loading.value = true
try {
const [appsRes, subRes] = await Promise.all([appApi.list(), accountApi.list()])
apps.value = appsRes.data.data
subAccountCount.value = subRes.data.data.length
} finally {
loading.value = false
}
}
function openSecretDialog(app: App, mode: 'REVEAL_SECRET' | 'RESET_SECRET') {
selectedApp.value = app
dialogMode.value = mode
showDialog.value = true
codeSent.value = false
verifyCode.value = ''
secretResult.value = ''
}
async function sendVerifyCode() {
if (!selectedApp.value) return
sendingCode.value = true
try {
await appApi.requestSecretVerify(selectedApp.value.id, dialogMode.value)
codeSent.value = true
ElMessage.success('验证码已发送到邮箱')
} finally {
sendingCode.value = false
}
}
async function submitVerify() {
if (!selectedApp.value) return
if (!verifyCode.value.trim()) {
ElMessage.warning('请输入验证码')
return
}
submitting.value = true
try {
if (dialogMode.value === 'REVEAL_SECRET') {
const res = await appApi.revealSecret(selectedApp.value.id, verifyCode.value.trim())
secretResult.value = res.data.data.appSecret
} else {
const res = await appApi.resetSecret(selectedApp.value.id, verifyCode.value.trim())
secretResult.value = res.data.data.appSecret
}
showDialog.value = false
showResult.value = true
ElMessage.success(dialogMode.value === 'REVEAL_SECRET' ? '密钥已查看' : '密钥已重置')
} finally {
submitting.value = false
}
}
function closeDialog() {
selectedApp.value = null
verifyCode.value = ''
codeSent.value = false
}
function fmt(value: string) {
return value ? new Date(value).toLocaleString('zh-CN') : '-'
}
onMounted(loadData)
</script>
<style scoped>
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
margin-bottom: 16px;
}
.subtitle {
margin: 6px 0 0;
color: #606266;
}
.summary-card {
min-height: 110px;
}
.dialog-text {
color: #606266;
margin: 0 0 16px;
}
.secret-box {
margin-top: 8px;
font-family: monospace;
word-break: break-all;
}
</style>