- 添加 expiresAt 和 refreshUserSig 参数支持自动续签 - 修改 PushSDK 初始化方式,自动完成设备注册和厂商初始化 - 调整过期续签策略,从提前 15 分钟改为提前 5 分钟触发 - 重构 RN SDK 文档结构,简化安装和使用方式 - 更新统一登录流程,支持 profile 信息传递 - 添加 IM 数据库自动隔离功能 - 修复 Android 群消息聚合问题 - 补充自动化测试验证和错误处理机制
214 行
6.7 KiB
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>
|