feat(par): 管理后台增加 PSK 管理页面

- 新增 /psks 页面:列表展示、生成新PSK、启用/禁用、复制密钥
- 版本号唯一,密钥支持手动输入或随机生成
- 侧边菜单增加「PSK 管理」入口
This commit is contained in:
mediabot-pt
2026-06-29 22:09:23 +08:00
parent 8565629a80
commit 94c0d2a398
4 changed files with 219 additions and 1 deletions

View File

@@ -45,7 +45,11 @@ export default {
users: {
list: () => api.get('/admin/accounts'),
setTrustLevel: (id, level) => api.post(`/admin/accounts/${id}/trust-level`, null, { params: { level } }),
},
psks: {
list: () => api.get('/admin/psks'),
add: (data) => api.post('/admin/psks', data),
toggle: (id) => api.post(`/admin/psks/${id}/toggle`)
},
health: () => api.get('/health', { baseURL: '' })
}

View File

@@ -35,6 +35,12 @@ const routes = [
name: 'Users',
component: () => import('../views/Users.vue'),
meta: { title: '用户管理' }
},
{
path: 'psks',
name: 'Psks',
component: () => import('../views/Psks.vue'),
meta: { title: 'PSK 管理' }
}
]
}

View File

@@ -26,6 +26,10 @@
<el-icon><UserFilled /></el-icon>
<span>用户管理</span>
</el-menu-item>
<el-menu-item index="/psks">
<el-icon><Key /></el-icon>
<span>PSK 管理</span>
</el-menu-item>
</el-menu>
</el-aside>
<el-container>

204
web/src/views/Psks.vue Normal file
View File

@@ -0,0 +1,204 @@
<template>
<div class="psks-page">
<div class="page-header">
<h2>PSK 管理</h2>
<el-button type="primary" @click="showAddDialog">
<el-icon><Plus /></el-icon>
生成 PSK
</el-button>
</div>
<el-card class="tips-card">
<el-alert type="info" :closable="false" show-icon>
<template #title>
PSK(预共享密钥)用于外部客户端(如 mediabot)接入认证。
每个 PSK 生成后需将密钥分发给客户端配置使用。
</template>
</el-alert>
</el-card>
<el-table :data="pskList" border stripe v-loading="loading" style="width: 100%; margin-top: 16px">
<el-table-column prop="id" label="ID" width="70" />
<el-table-column prop="version" label="版本" width="100" />
<el-table-column label="密钥" min-width="200">
<template #default="{ row }">
<span v-if="!row.showSecret">{{ '•'.repeat(32) }}</span>
<span v-else class="secret-text">{{ row.secret }}</span>
<el-button link type="primary" size="small" @click="row.showSecret = !row.showSecret" style="margin-left: 8px">
{{ row.showSecret ? '隐藏' : '显示' }}
</el-button>
<el-button link type="primary" size="small" @click="copySecret(row.secret)">复制</el-button>
</template>
</el-table-column>
<el-table-column prop="description" label="描述" min-width="150">
<template #default="{ row }">
{{ row.description || '-' }}
</template>
</el-table-column>
<el-table-column label="状态" width="90">
<template #default="{ row }">
<el-tag :type="row.isActive ? 'success' : 'danger'" size="small">
{{ row.isActive ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建时间" width="180" />
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-popconfirm
:title="row.isActive ? '确认禁用此 PSK?禁用后客户端将无法使用该密钥认证' : '确认启用此 PSK?'"
@confirm="handleToggle(row)"
>
<template #reference>
<el-button :type="row.isActive ? 'danger' : 'success'" size="small" link>
{{ row.isActive ? '禁用' : '启用' }}
</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<!-- 生成 PSK 弹窗 -->
<el-dialog v-model="addDialogVisible" title="生成 PSK" width="480px" :close-on-click-modal="false">
<el-form :model="addForm" label-width="80px">
<el-form-item label="版本号">
<el-input v-model="addForm.version" placeholder="如 v2, v3" />
<div class="form-tip">唯一标识,不可与已有版本重复</div>
</el-form-item>
<el-form-item label="描述">
<el-input v-model="addForm.description" placeholder="此 PSK 的用途或使用方" />
</el-form-item>
<el-form-item label="密钥">
<el-input v-model="addForm.secret" placeholder="留空自动生成" />
<el-button link type="primary" size="small" @click="generateSecret" style="margin-top: 4px">
随机生成
</el-button>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="addDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleAdd" :loading="adding">生成</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import api from '../api'
import { ElMessage } from 'element-plus'
const pskList = ref([])
const loading = ref(false)
const adding = ref(false)
const addDialogVisible = ref(false)
const addForm = reactive({
version: '',
description: '',
secret: ''
})
const fetchList = async () => {
loading.value = true
try {
const res = await api.psks.list()
pskList.value = (res.data || []).map(item => ({ ...item, showSecret: false }))
} catch {
ElMessage.error('获取 PSK 列表失败')
} finally {
loading.value = false
}
}
const showAddDialog = () => {
addForm.version = 'v' + (pskList.value.length + 1)
addForm.description = ''
addForm.secret = ''
addDialogVisible.value = true
}
const generateSecret = () => {
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
let result = ''
for (let i = 0; i < 32; i++) {
result += chars[Math.floor(Math.random() * chars.length)]
}
addForm.secret = result
}
const handleAdd = async () => {
if (!addForm.version.trim()) {
ElMessage.warning('请输入版本号')
return
}
if (!addForm.secret.trim()) {
ElMessage.warning('请输入或生成密钥')
return
}
adding.value = true
try {
await api.psks.add({
version: addForm.version.trim(),
description: addForm.description.trim(),
secret: addForm.secret.trim()
})
ElMessage.success('PSK 已生成')
addDialogVisible.value = false
fetchList()
} catch (e) {
ElMessage.error(e.response?.data?.message || '生成失败')
} finally {
adding.value = false
}
}
const handleToggle = async (row) => {
try {
await api.psks.toggle(row.id)
ElMessage.success(row.isActive ? 'PSK 已禁用' : 'PSK 已启用')
fetchList()
} catch {
ElMessage.error('操作失败')
}
}
const copySecret = async (secret) => {
try {
await navigator.clipboard.writeText(secret)
ElMessage.success('已复制到剪贴板')
} catch {
ElMessage.error('复制失败,请手动复制')
}
}
onMounted(fetchList)
</script>
<style scoped>
.psks-page {
max-width: 960px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.page-header h2 {
margin: 0;
font-size: 20px;
}
.tips-card {
margin-top: 16px;
}
.secret-text {
font-family: monospace;
word-break: break-all;
}
.form-tip {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
</style>