feat(token): 一作用域一Token(签发N个作用域生成N把); 新增Open API /api/open/token/ensure(get-or-create, X-Open-Key鉴权, 明文不可回吐)
This commit is contained in:
@@ -26,8 +26,8 @@ const audits = ref([])
|
||||
const toolFilter = reactive({ category: '', kind: '', keyword: '' })
|
||||
const auditFilter = reactive({ person: '', tokenPrefix: '', tool: '', result: '', client: '', from: '', to: '' })
|
||||
const tokenDialog = ref(false)
|
||||
const tokenForm = reactive({ id: null, memo: '', scopeIds: [], expireTime: null })
|
||||
const issuedPlaintext = ref('')
|
||||
const tokenForm = reactive({ id: null, memo: '', scopeIds: [], scopeId: null, expireTime: null })
|
||||
const issuedTokens = ref([])
|
||||
const scopeDialog = ref(false)
|
||||
const scopeForm = reactive({ id: null, name: '', code: '', memo: '' })
|
||||
const scopeToolsDialog = ref(false)
|
||||
@@ -106,24 +106,26 @@ async function selectUser(user) {
|
||||
|
||||
function openToken(token = null) {
|
||||
Object.assign(tokenForm, token ? {
|
||||
id: token.id, memo: token.memo || '', scopeIds: [...token.scopeIds], expireTime: token.expireTime || null,
|
||||
} : { id: null, memo: '', scopeIds: [], expireTime: null })
|
||||
id: token.id, memo: token.memo || '', scopeIds: [], scopeId: token.scopeIds[0] ?? null,
|
||||
expireTime: token.expireTime || null,
|
||||
} : { id: null, memo: '', scopeIds: [], scopeId: null, expireTime: null })
|
||||
tokenDialog.value = true
|
||||
}
|
||||
|
||||
async function saveToken() {
|
||||
if (!tokenForm.scopeIds.length) return ElMessage.warning('至少选择一个作用域')
|
||||
const scopeIds = tokenForm.id ? (tokenForm.scopeId ? [tokenForm.scopeId] : []) : tokenForm.scopeIds
|
||||
if (!scopeIds.length) return ElMessage.warning('至少选择一个作用域')
|
||||
await run(async () => {
|
||||
const body = JSON.stringify({ memo: tokenForm.memo, scopeIds: tokenForm.scopeIds, expireTime: tokenForm.expireTime })
|
||||
const body = JSON.stringify({ memo: tokenForm.memo, scopeIds, expireTime: tokenForm.expireTime })
|
||||
if (tokenForm.id) await api(`/tokens/${tokenForm.id}`, { method: 'PUT', body })
|
||||
else {
|
||||
const issued = await api(`/users/${selectedUser.value.id}/tokens`, { method: 'POST', body })
|
||||
issuedPlaintext.value = issued.plaintext
|
||||
// 一作用域一 Token:选 N 个作用域返回 N 把明文
|
||||
issuedTokens.value = await api(`/users/${selectedUser.value.id}/tokens`, { method: 'POST', body })
|
||||
}
|
||||
tokenDialog.value = false
|
||||
tokens.value = await api(`/users/${selectedUser.value.id}/tokens`)
|
||||
await loadUsers()
|
||||
ElMessage.success(tokenForm.id ? 'Token 已更新' : 'Token 已签发')
|
||||
ElMessage.success(tokenForm.id ? 'Token 已更新' : `已签发 ${issuedTokens.value.length} 把 Token`)
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
@@ -206,7 +208,10 @@ async function loadAudit() { audits.value = await api(`/audit${qs(auditFilter)}`
|
||||
function date(value) { return value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '—' }
|
||||
function expiry(value) { return value ? date(value) : '永久' }
|
||||
function enterprise(value) { return ({ 1: '代理', 2: '物业' })[value] || `类型 ${value ?? '未配置'}` }
|
||||
async function copyToken() { await navigator.clipboard.writeText(issuedPlaintext.value); ElMessage.success('已复制') }
|
||||
async function copyText(value) { await navigator.clipboard.writeText(value); ElMessage.success('已复制') }
|
||||
async function copyAllTokens() {
|
||||
await copyText(issuedTokens.value.map(item => `${item.token.scopeNames.join('/')}: ${item.plaintext}`).join('\n'))
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
@@ -388,7 +393,15 @@ onMounted(async () => {
|
||||
<el-form label-width="88px">
|
||||
<el-form-item label="所属人员"><strong>{{ selectedUser?.name }}</strong></el-form-item>
|
||||
<el-form-item label="用途备注"><el-input v-model="tokenForm.memo" maxlength="255" placeholder="如:Kimi 桌面用" aria-label="Token 用途备注" data-testid="admin-token-memo" /></el-form-item>
|
||||
<el-form-item label="作用域"><el-checkbox-group v-model="tokenForm.scopeIds" data-testid="admin-token-scopes"><el-checkbox v-for="scope in scopes.filter(item => item.status === 1)" :key="scope.id" :value="scope.id">{{ scope.name }}</el-checkbox></el-checkbox-group></el-form-item>
|
||||
<el-form-item label="作用域">
|
||||
<el-radio-group v-if="tokenForm.id" v-model="tokenForm.scopeId" data-testid="admin-token-scopes">
|
||||
<el-radio v-for="scope in scopes.filter(item => item.status === 1)" :key="scope.id" :value="scope.id">{{ scope.name }}</el-radio>
|
||||
</el-radio-group>
|
||||
<el-checkbox-group v-else v-model="tokenForm.scopeIds" data-testid="admin-token-scopes">
|
||||
<el-checkbox v-for="scope in scopes.filter(item => item.status === 1)" :key="scope.id" :value="scope.id">{{ scope.name }}</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
<div class="hint" style="margin:4px 0 0">{{ tokenForm.id ? '一把 Token 只绑一个作用域' : '一作用域一 Token:选几个作用域就签发几把' }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="过期时间"><el-date-picker v-model="tokenForm.expireTime" type="datetime" value-format="YYYY-MM-DDTHH:mm:ssZ" placeholder="留空为永久" aria-label="Token 过期时间" data-testid="admin-token-expiry" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer><el-button data-testid="admin-token-cancel" @click="tokenDialog = false">取消</el-button><el-button type="primary" data-testid="admin-token-save" @click="saveToken">确认</el-button></template>
|
||||
@@ -410,9 +423,13 @@ onMounted(async () => {
|
||||
<template #footer><el-button data-testid="admin-scope-tools-cancel" @click="scopeToolsDialog = false">取消</el-button><el-button type="primary" data-testid="admin-scope-tools-save" @click="saveScopeTools">保存配置</el-button></template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog :model-value="!!issuedPlaintext" title="Token 签发成功" width="560px" :close-on-click-modal="false" @close="issuedPlaintext = ''">
|
||||
<el-alert title="明文只展示这一次,请立即复制并妥善保存。" type="warning" :closable="false" show-icon />
|
||||
<code class="token-secret" data-testid="admin-issued-token">{{ issuedPlaintext }}</code>
|
||||
<template #footer><el-button type="primary" data-testid="admin-copy-token" @click="copyToken">复制 Token</el-button><el-button data-testid="admin-close-token" @click="issuedPlaintext = ''">我已保存</el-button></template>
|
||||
<el-dialog :model-value="!!issuedTokens.length" title="Token 签发成功" width="600px" :close-on-click-modal="false" @close="issuedTokens = []">
|
||||
<el-alert title="每个作用域一把 Token,明文只展示这一次,请立即复制并妥善保存。" type="warning" :closable="false" show-icon />
|
||||
<div v-for="item in issuedTokens" :key="item.token.id" class="issued-row">
|
||||
<el-tag size="small" effect="plain">{{ item.token.scopeNames.join('/') }}</el-tag>
|
||||
<code class="token-secret" :data-testid="`admin-issued-token-${item.token.id}`">{{ item.plaintext }}</code>
|
||||
<el-button link type="primary" :data-testid="`admin-copy-token-${item.token.id}`" @click="copyText(item.plaintext)">复制</el-button>
|
||||
</div>
|
||||
<template #footer><el-button type="primary" data-testid="admin-copy-all-tokens" @click="copyAllTokens">全部复制</el-button><el-button data-testid="admin-close-token" @click="issuedTokens = []">我已保存</el-button></template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
@@ -60,4 +60,6 @@ code { font-family: Menlo, Consolas, monospace; font-size: 12px; }
|
||||
.tool-group .el-checkbox__label { display: flex; flex-direction: column; white-space: normal; }
|
||||
.tool-group small { margin-top: 3px; color: #909399; }
|
||||
.token-secret { display: block; margin-top: 14px; padding: 14px; word-break: break-all; border-radius: 4px; background: #f4f4f5; font-size: 13px; }
|
||||
.issued-row { display: flex; align-items: center; gap: 10px; margin-top: 12px; }
|
||||
.issued-row .token-secret { flex: 1; margin-top: 0; padding: 9px 12px; }
|
||||
@media (max-width: 1200px) { .token-layout { grid-template-columns: 300px minmax(640px, 1fr); } }
|
||||
|
||||
Reference in New Issue
Block a user