mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
Merge remote-tracking branch 'origin/pr/399' into aether-rust-pioneer
# Conflicts: # crates/aether-data/src/lifecycle/bootstrap/postgres.rs # crates/aether-data/src/lifecycle/migrate/tests.rs
This commit is contained in:
@@ -13,6 +13,9 @@ export interface ManagementToken {
|
||||
description?: string
|
||||
token_display: string
|
||||
allowed_ips?: string[] | null
|
||||
permissions?: string[] | null
|
||||
permission_mode?: 'legacy_full' | 'full' | 'read_only' | 'custom'
|
||||
permission_summary?: string
|
||||
expires_at?: string | null
|
||||
last_used_at?: string | null
|
||||
last_used_ip?: string | null
|
||||
@@ -32,6 +35,7 @@ export interface CreateManagementTokenRequest {
|
||||
name: string
|
||||
description?: string
|
||||
allowed_ips?: string[]
|
||||
permissions?: string[] | null
|
||||
expires_at?: string | null
|
||||
}
|
||||
|
||||
@@ -45,10 +49,25 @@ export interface UpdateManagementTokenRequest {
|
||||
name?: string
|
||||
description?: string | null
|
||||
allowed_ips?: string[] | null
|
||||
permissions?: string[] | null
|
||||
expires_at?: string | null
|
||||
is_active?: boolean
|
||||
}
|
||||
|
||||
export interface ManagementTokenPermissionCatalogItem {
|
||||
key: string
|
||||
scope: string
|
||||
scope_label: string
|
||||
access: 'read' | 'write' | 'admin'
|
||||
access_label: string
|
||||
}
|
||||
|
||||
export interface ManagementTokenPermissionCatalogResponse {
|
||||
items: ManagementTokenPermissionCatalogItem[]
|
||||
all_permissions: string[]
|
||||
read_only_permissions: string[]
|
||||
}
|
||||
|
||||
export interface ManagementTokenListResponse {
|
||||
items: ManagementToken[]
|
||||
total: number
|
||||
@@ -179,6 +198,43 @@ export const adminManagementTokenApi = {
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取可分配权限目录(管理员)
|
||||
*/
|
||||
async getPermissionCatalog(): Promise<ManagementTokenPermissionCatalogResponse> {
|
||||
const response = await apiClient.get<ManagementTokenPermissionCatalogResponse>(
|
||||
'/api/admin/management-tokens/permissions/catalog'
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建 Management Token(管理员)
|
||||
*/
|
||||
async createToken(
|
||||
data: CreateManagementTokenRequest
|
||||
): Promise<CreateManagementTokenResponse> {
|
||||
const response = await apiClient.post<CreateManagementTokenResponse>(
|
||||
'/api/admin/management-tokens',
|
||||
data
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新 Management Token(管理员)
|
||||
*/
|
||||
async updateToken(
|
||||
tokenId: string,
|
||||
data: UpdateManagementTokenRequest
|
||||
): Promise<{ message: string; data: ManagementToken }> {
|
||||
const response = await apiClient.put<{ message: string; data: ManagementToken }>(
|
||||
`/api/admin/management-tokens/${tokenId}`,
|
||||
data
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除任意 Token(管理员)
|
||||
*/
|
||||
@@ -199,5 +255,17 @@ export const adminManagementTokenApi = {
|
||||
`/api/admin/management-tokens/${tokenId}/status`
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 重新生成任意 Token(管理员)
|
||||
*/
|
||||
async regenerateToken(
|
||||
tokenId: string
|
||||
): Promise<{ token: string; data: ManagementToken }> {
|
||||
const response = await apiClient.post<{ token: string; data: ManagementToken }>(
|
||||
`/api/admin/management-tokens/${tokenId}/regenerate`
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
|
||||
<div>
|
||||
<h3 class="text-sm sm:text-base font-semibold">
|
||||
访问令牌
|
||||
{{ isAdminPage ? '管理令牌' : '访问令牌' }}
|
||||
</h3>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">
|
||||
<template v-if="quota">
|
||||
@@ -20,9 +20,12 @@
|
||||
class="text-destructive font-medium"
|
||||
>(已达上限)</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-else-if="canManageTokens">
|
||||
用于程序化访问管理 API 的令牌
|
||||
</template>
|
||||
<template v-else>
|
||||
仅管理员可以创建和管理此类令牌
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -30,12 +33,13 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- 新增按钮 -->
|
||||
<Button
|
||||
v-if="canManageTokens"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
title="创建新令牌"
|
||||
:disabled="quota ? quota.used >= quota.max : false"
|
||||
@click="showCreateDialog = true"
|
||||
@click="openCreateDialog"
|
||||
>
|
||||
<Plus class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
@@ -69,9 +73,10 @@
|
||||
>
|
||||
<template #actions>
|
||||
<Button
|
||||
v-if="canManageTokens"
|
||||
size="lg"
|
||||
class="shadow-lg shadow-primary/20"
|
||||
@click="showCreateDialog = true"
|
||||
@click="openCreateDialog"
|
||||
>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
创建访问令牌
|
||||
@@ -94,6 +99,18 @@
|
||||
<TableHead class="min-w-[160px] h-12 font-semibold">
|
||||
令牌
|
||||
</TableHead>
|
||||
<TableHead
|
||||
v-if="isAdminPage"
|
||||
class="min-w-[160px] h-12 font-semibold"
|
||||
>
|
||||
所属用户
|
||||
</TableHead>
|
||||
<TableHead class="min-w-[150px] h-12 font-semibold">
|
||||
权限
|
||||
</TableHead>
|
||||
<TableHead class="min-w-[150px] h-12 font-semibold">
|
||||
IP 限制
|
||||
</TableHead>
|
||||
<TableHead class="min-w-[80px] h-12 font-semibold text-center">
|
||||
使用次数
|
||||
</TableHead>
|
||||
@@ -103,7 +120,10 @@
|
||||
<TableHead class="min-w-[100px] h-12 font-semibold">
|
||||
时间
|
||||
</TableHead>
|
||||
<TableHead class="min-w-[100px] h-12 font-semibold text-center">
|
||||
<TableHead
|
||||
v-if="canManageTokens"
|
||||
class="min-w-[100px] h-12 font-semibold text-center"
|
||||
>
|
||||
操作
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
@@ -140,6 +160,7 @@
|
||||
{{ token.token_display }}
|
||||
</code>
|
||||
<Button
|
||||
v-if="canManageTokens"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-6 w-6"
|
||||
@@ -151,6 +172,39 @@
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
v-if="isAdminPage"
|
||||
class="py-4"
|
||||
>
|
||||
<div class="text-sm font-medium truncate">
|
||||
{{ token.user?.username || token.user?.email || token.user_id }}
|
||||
</div>
|
||||
<div
|
||||
v-if="token.user?.email"
|
||||
class="text-xs text-muted-foreground truncate"
|
||||
>
|
||||
{{ token.user.email }}
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell class="py-4">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class="font-medium px-2 py-1"
|
||||
>
|
||||
{{ token.permission_summary || permissionModeText(token.permission_mode) }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
|
||||
<TableCell class="py-4 text-xs text-muted-foreground">
|
||||
<div class="truncate">
|
||||
{{ token.allowed_ips?.length ? token.allowed_ips.join(', ') : '不限制' }}
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
{{ token.last_used_ip ? `最后 IP ${token.last_used_ip}` : '暂无最后 IP' }}
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<!-- 使用次数 -->
|
||||
<TableCell class="py-4 text-center">
|
||||
<span class="text-sm font-medium">
|
||||
@@ -179,7 +233,10 @@
|
||||
</TableCell>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<TableCell class="py-4">
|
||||
<TableCell
|
||||
v-if="canManageTokens"
|
||||
class="py-4"
|
||||
>
|
||||
<div class="flex justify-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -240,7 +297,10 @@
|
||||
{{ getStatusText(token) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="flex items-center gap-0.5 flex-shrink-0">
|
||||
<div
|
||||
v-if="canManageTokens"
|
||||
class="flex items-center gap-0.5 flex-shrink-0"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -275,6 +335,7 @@
|
||||
<div class="flex items-center gap-2 text-xs mb-2">
|
||||
<code class="font-mono text-muted-foreground">{{ token.token_display }}</code>
|
||||
<Button
|
||||
v-if="canManageTokens"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-5 w-5"
|
||||
@@ -289,6 +350,8 @@
|
||||
<div class="flex items-center gap-3 text-xs text-muted-foreground">
|
||||
<span>{{ formatNumber(token.usage_count || 0) }} 次使用</span>
|
||||
<span>·</span>
|
||||
<span>{{ token.permission_summary || permissionModeText(token.permission_mode) }}</span>
|
||||
<span>·</span>
|
||||
<span>{{ token.last_used_at ? formatRelativeTime(token.last_used_at) : '从未使用' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -397,6 +460,104 @@
|
||||
留空表示永不过期
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 权限 -->
|
||||
<div class="space-y-3">
|
||||
<Label class="text-sm font-semibold">权限</Label>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
:variant="permissionMode === 'full' ? 'default' : 'outline'"
|
||||
class="h-9"
|
||||
@click="setPermissionMode('full')"
|
||||
>
|
||||
全权
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
:variant="permissionMode === 'read_only' ? 'default' : 'outline'"
|
||||
class="h-9"
|
||||
@click="setPermissionMode('read_only')"
|
||||
>
|
||||
只读
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
:variant="permissionMode === 'custom' ? 'default' : 'outline'"
|
||||
class="h-9"
|
||||
@click="setPermissionMode('custom')"
|
||||
>
|
||||
自定义
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="permissionMode === 'custom'"
|
||||
class="flex flex-wrap items-center gap-2"
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
class="h-8 px-3 text-xs"
|
||||
@click="setCustomPermissions('none')"
|
||||
>
|
||||
全部禁用
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
class="h-8 px-3 text-xs"
|
||||
@click="setCustomPermissions('read_only')"
|
||||
>
|
||||
全部只读
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
class="h-8 px-3 text-xs"
|
||||
@click="setCustomPermissions('full')"
|
||||
>
|
||||
全部全权
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="permissionMode === 'custom'"
|
||||
class="max-h-72 overflow-y-auto rounded-md border border-border/60"
|
||||
>
|
||||
<div
|
||||
v-for="group in permissionGroups"
|
||||
:key="group.scope"
|
||||
class="border-b border-border/50 last:border-b-0 px-3 py-2"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="text-sm font-medium">
|
||||
{{ group.label }}
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="inline-flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Checkbox
|
||||
:checked="isPermissionGroupDenied(group)"
|
||||
@update:checked="togglePermissionGroupDenied(group, $event)"
|
||||
/>
|
||||
<span>禁止</span>
|
||||
</label>
|
||||
<label
|
||||
v-for="item in group.items"
|
||||
:key="item.key"
|
||||
class="inline-flex items-center gap-1.5 text-xs text-muted-foreground"
|
||||
>
|
||||
<Checkbox
|
||||
:checked="selectedPermissions.includes(item.key)"
|
||||
@update:checked="togglePermission(item.key, $event)"
|
||||
/>
|
||||
<span>{{ item.access_label }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
@@ -512,15 +673,20 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, reactive, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import {
|
||||
adminManagementTokenApi,
|
||||
managementTokenApi,
|
||||
type ManagementToken
|
||||
type ManagementToken,
|
||||
type ManagementTokenPermissionCatalogItem
|
||||
} from '@/api/management-tokens'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import Card from '@/components/ui/card.vue'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import Input from '@/components/ui/input.vue'
|
||||
import Label from '@/components/ui/label.vue'
|
||||
import Badge from '@/components/ui/badge.vue'
|
||||
import Checkbox from '@/components/ui/checkbox.vue'
|
||||
import { Dialog, Pagination } from '@/components/ui'
|
||||
import { LoadingState, AlertDialog, EmptyState } from '@/components/common'
|
||||
import {
|
||||
@@ -548,6 +714,11 @@ import { log } from '@/utils/logger'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
|
||||
const { success, error: showError } = useToast()
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const isAdminPage = computed(() => route.path.startsWith('/admin'))
|
||||
const canManageTokens = computed(() => authStore.isAdmin)
|
||||
|
||||
// 数据
|
||||
const tokens = ref<ManagementToken[]>([])
|
||||
@@ -566,6 +737,29 @@ const pageSize = ref(10)
|
||||
|
||||
const paginatedTokens = computed(() => tokens.value)
|
||||
|
||||
const permissionCatalog = ref<ManagementTokenPermissionCatalogItem[]>([])
|
||||
const allPermissionKeys = ref<string[]>([])
|
||||
const readOnlyPermissionKeys = ref<string[]>([])
|
||||
const permissionMode = ref<'full' | 'read_only' | 'custom'>('full')
|
||||
const selectedPermissions = ref<string[]>([])
|
||||
|
||||
const permissionGroups = computed(() => {
|
||||
const groups = new Map<string, { scope: string; label: string; items: ManagementTokenPermissionCatalogItem[] }>()
|
||||
for (const item of permissionCatalog.value) {
|
||||
const group = groups.get(item.scope)
|
||||
if (group) {
|
||||
group.items.push(item)
|
||||
} else {
|
||||
groups.set(item.scope, {
|
||||
scope: item.scope,
|
||||
label: item.scope_label,
|
||||
items: [item]
|
||||
})
|
||||
}
|
||||
}
|
||||
return Array.from(groups.values())
|
||||
})
|
||||
|
||||
// 监听分页变化
|
||||
watch([currentPage, pageSize], () => {
|
||||
loadTokens()
|
||||
@@ -598,7 +792,11 @@ const tokenToRegenerate = ref<ManagementToken | null>(null)
|
||||
|
||||
// 表单验证
|
||||
const isFormValid = computed(() => {
|
||||
return formData.name.trim().length > 0
|
||||
if (formData.name.trim().length === 0) return false
|
||||
if (permissionMode.value === 'custom' && selectedPermissions.value.length === 0) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
function getStatusVariant(token: ManagementToken): 'success' | 'secondary' | 'destructive' {
|
||||
@@ -624,18 +822,28 @@ onMounted(() => {
|
||||
loadTokens()
|
||||
})
|
||||
|
||||
watch(
|
||||
canManageTokens,
|
||||
(allowed) => {
|
||||
if (allowed) {
|
||||
void loadPermissionCatalog()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
async function loadTokens() {
|
||||
loading.value = true
|
||||
try {
|
||||
const skip = (currentPage.value - 1) * pageSize.value
|
||||
const response = await managementTokenApi.listTokens({ skip, limit: pageSize.value })
|
||||
const response = isAdminPage.value
|
||||
? await adminManagementTokenApi.listAllTokens({ skip, limit: pageSize.value })
|
||||
: await managementTokenApi.listTokens({ skip, limit: pageSize.value })
|
||||
|
||||
tokens.value = response.items
|
||||
totalTokens.value = response.total
|
||||
|
||||
if (response.quota) {
|
||||
quota.value = response.quota
|
||||
}
|
||||
quota.value = response.quota ?? null
|
||||
|
||||
// 如果当前页超出范围,重置到第一页
|
||||
if (tokens.value.length === 0 && currentPage.value > 1) {
|
||||
@@ -649,8 +857,34 @@ async function loadTokens() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPermissionCatalog() {
|
||||
if (permissionCatalog.value.length > 0) return
|
||||
try {
|
||||
const response = await adminManagementTokenApi.getPermissionCatalog()
|
||||
permissionCatalog.value = response.items
|
||||
allPermissionKeys.value = response.all_permissions
|
||||
readOnlyPermissionKeys.value = response.read_only_permissions
|
||||
if (selectedPermissions.value.length === 0) {
|
||||
selectedPermissions.value = [...response.all_permissions]
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
log.error('加载 Management Token 权限目录失败:', err)
|
||||
showError(parseApiError(err, '权限目录加载失败'))
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
if (!canManageTokens.value) return
|
||||
resetForm()
|
||||
permissionMode.value = 'full'
|
||||
selectedPermissions.value = [...allPermissionKeys.value]
|
||||
void loadPermissionCatalog()
|
||||
showCreateDialog.value = true
|
||||
}
|
||||
|
||||
// 打开编辑对话框
|
||||
function openEditDialog(token: ManagementToken) {
|
||||
if (!canManageTokens.value) return
|
||||
editingToken.value = token
|
||||
formData.name = token.name
|
||||
formData.description = token.description || ''
|
||||
@@ -660,17 +894,114 @@ function openEditDialog(token: ManagementToken) {
|
||||
formData.expiresAt = token.expires_at
|
||||
? toLocalDatetimeString(new Date(token.expires_at))
|
||||
: ''
|
||||
const mode = token.permission_mode === 'read_only' || token.permission_mode === 'custom'
|
||||
? token.permission_mode
|
||||
: 'full'
|
||||
permissionMode.value = mode
|
||||
selectedPermissions.value = token.permissions?.length
|
||||
? [...token.permissions]
|
||||
: (mode === 'read_only' ? [...readOnlyPermissionKeys.value] : [...allPermissionKeys.value])
|
||||
void loadPermissionCatalog()
|
||||
showCreateDialog.value = true
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
function closeDialog() {
|
||||
showCreateDialog.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
editingToken.value = null
|
||||
formData.name = ''
|
||||
formData.description = ''
|
||||
formData.allowedIpsText = ''
|
||||
formData.expiresAt = ''
|
||||
permissionMode.value = 'full'
|
||||
selectedPermissions.value = [...allPermissionKeys.value]
|
||||
}
|
||||
|
||||
function setPermissionMode(mode: 'full' | 'read_only' | 'custom') {
|
||||
permissionMode.value = mode
|
||||
if (mode === 'full') {
|
||||
selectedPermissions.value = [...allPermissionKeys.value]
|
||||
} else if (mode === 'read_only') {
|
||||
selectedPermissions.value = [...readOnlyPermissionKeys.value]
|
||||
} else if (selectedPermissions.value.length === 0) {
|
||||
selectedPermissions.value = [...readOnlyPermissionKeys.value]
|
||||
}
|
||||
}
|
||||
|
||||
function setCustomPermissions(mode: 'none' | 'read_only' | 'full') {
|
||||
if (mode === 'none') {
|
||||
selectedPermissions.value = []
|
||||
} else if (mode === 'read_only') {
|
||||
selectedPermissions.value = [...readOnlyPermissionKeys.value]
|
||||
} else {
|
||||
selectedPermissions.value = [...allPermissionKeys.value]
|
||||
}
|
||||
}
|
||||
|
||||
function togglePermission(key: string, checked: boolean) {
|
||||
const next = new Set(selectedPermissions.value)
|
||||
if (checked) {
|
||||
next.add(key)
|
||||
} else {
|
||||
next.delete(key)
|
||||
}
|
||||
selectedPermissions.value = Array.from(next).sort()
|
||||
}
|
||||
|
||||
function isPermissionGroupDenied(group: { items: ManagementTokenPermissionCatalogItem[] }): boolean {
|
||||
return group.items.every(item => !selectedPermissions.value.includes(item.key))
|
||||
}
|
||||
|
||||
function togglePermissionGroupDenied(
|
||||
group: { items: ManagementTokenPermissionCatalogItem[] },
|
||||
checked: boolean
|
||||
) {
|
||||
const next = new Set(selectedPermissions.value)
|
||||
for (const item of group.items) {
|
||||
next.delete(item.key)
|
||||
}
|
||||
if (!checked) {
|
||||
const readPermission = group.items.find(item => item.access === 'read')
|
||||
if (readPermission) {
|
||||
next.add(readPermission.key)
|
||||
}
|
||||
}
|
||||
selectedPermissions.value = Array.from(next).sort()
|
||||
}
|
||||
|
||||
async function resolveFormPermissions(): Promise<string[]> {
|
||||
if (permissionCatalog.value.length === 0) {
|
||||
await loadPermissionCatalog()
|
||||
}
|
||||
if (allPermissionKeys.value.length === 0) {
|
||||
throw new Error('权限目录不可用')
|
||||
}
|
||||
if (permissionMode.value === 'full') {
|
||||
return [...allPermissionKeys.value]
|
||||
}
|
||||
if (permissionMode.value === 'read_only') {
|
||||
return [...readOnlyPermissionKeys.value]
|
||||
}
|
||||
return [...selectedPermissions.value]
|
||||
}
|
||||
|
||||
function permissionModeText(mode?: ManagementToken['permission_mode']): string {
|
||||
switch (mode) {
|
||||
case 'legacy_full':
|
||||
return '旧版全权限'
|
||||
case 'full':
|
||||
return '全权限'
|
||||
case 'read_only':
|
||||
return '只读'
|
||||
case 'custom':
|
||||
return '自定义'
|
||||
default:
|
||||
return '未配置'
|
||||
}
|
||||
}
|
||||
|
||||
// 保存 Token
|
||||
@@ -688,16 +1019,26 @@ async function saveToken() {
|
||||
const expiresAtUtc = formData.expiresAt
|
||||
? new Date(formData.expiresAt).toISOString()
|
||||
: null
|
||||
const permissions = await resolveFormPermissions()
|
||||
|
||||
if (editingToken.value) {
|
||||
// 更新
|
||||
const tokenId = editingToken.value.id
|
||||
const result = await managementTokenApi.updateToken(tokenId, {
|
||||
name: formData.name,
|
||||
description: formData.description.trim() || null,
|
||||
allowed_ips: allowedIps.length > 0 ? allowedIps : null,
|
||||
expires_at: expiresAtUtc
|
||||
})
|
||||
const result = isAdminPage.value
|
||||
? await adminManagementTokenApi.updateToken(tokenId, {
|
||||
name: formData.name,
|
||||
description: formData.description.trim() || null,
|
||||
allowed_ips: allowedIps.length > 0 ? allowedIps : null,
|
||||
permissions,
|
||||
expires_at: expiresAtUtc
|
||||
})
|
||||
: await managementTokenApi.updateToken(tokenId, {
|
||||
name: formData.name,
|
||||
description: formData.description.trim() || null,
|
||||
allowed_ips: allowedIps.length > 0 ? allowedIps : null,
|
||||
permissions,
|
||||
expires_at: expiresAtUtc
|
||||
})
|
||||
// 局部更新:直接替换列表中对应的记录
|
||||
const index = tokens.value.findIndex(t => t.id === tokenId)
|
||||
if (index !== -1) {
|
||||
@@ -706,12 +1047,16 @@ async function saveToken() {
|
||||
success('令牌更新成功')
|
||||
} else {
|
||||
// 创建
|
||||
const result = await managementTokenApi.createToken({
|
||||
const payload = {
|
||||
name: formData.name,
|
||||
description: formData.description || undefined,
|
||||
allowed_ips: allowedIps.length > 0 ? allowedIps : undefined,
|
||||
permissions,
|
||||
expires_at: expiresAtUtc
|
||||
})
|
||||
}
|
||||
const result = isAdminPage.value
|
||||
? await adminManagementTokenApi.createToken(payload)
|
||||
: await managementTokenApi.createToken(payload)
|
||||
newTokenValue.value = result.token
|
||||
isRegenerating.value = false
|
||||
showTokenDialog.value = true
|
||||
@@ -730,8 +1075,11 @@ async function saveToken() {
|
||||
|
||||
// 切换状态
|
||||
async function toggleToken(token: ManagementToken) {
|
||||
if (!canManageTokens.value) return
|
||||
try {
|
||||
const result = await managementTokenApi.toggleToken(token.id)
|
||||
const result = isAdminPage.value
|
||||
? await adminManagementTokenApi.toggleToken(token.id)
|
||||
: await managementTokenApi.toggleToken(token.id)
|
||||
|
||||
const index = tokens.value.findIndex(t => t.id === token.id)
|
||||
if (index !== -1) {
|
||||
@@ -746,6 +1094,7 @@ async function toggleToken(token: ManagementToken) {
|
||||
|
||||
// 删除
|
||||
function confirmDelete(token: ManagementToken) {
|
||||
if (!canManageTokens.value) return
|
||||
tokenToDelete.value = token
|
||||
showDeleteDialog.value = true
|
||||
}
|
||||
@@ -755,7 +1104,11 @@ async function deleteToken() {
|
||||
|
||||
deleting.value = true
|
||||
try {
|
||||
await managementTokenApi.deleteToken(tokenToDelete.value.id)
|
||||
if (isAdminPage.value) {
|
||||
await adminManagementTokenApi.deleteToken(tokenToDelete.value.id)
|
||||
} else {
|
||||
await managementTokenApi.deleteToken(tokenToDelete.value.id)
|
||||
}
|
||||
|
||||
showDeleteDialog.value = false
|
||||
success('令牌已删除')
|
||||
@@ -771,6 +1124,7 @@ async function deleteToken() {
|
||||
|
||||
// 重新生成
|
||||
function confirmRegenerate(token: ManagementToken) {
|
||||
if (!canManageTokens.value) return
|
||||
tokenToRegenerate.value = token
|
||||
showRegenerateDialog.value = true
|
||||
}
|
||||
@@ -780,7 +1134,9 @@ async function regenerateToken() {
|
||||
|
||||
regenerating.value = true
|
||||
try {
|
||||
const result = await managementTokenApi.regenerateToken(tokenToRegenerate.value.id)
|
||||
const result = isAdminPage.value
|
||||
? await adminManagementTokenApi.regenerateToken(tokenToRegenerate.value.id)
|
||||
: await managementTokenApi.regenerateToken(tokenToRegenerate.value.id)
|
||||
newTokenValue.value = result.token
|
||||
isRegenerating.value = true
|
||||
showRegenerateDialog.value = false
|
||||
|
||||
Reference in New Issue
Block a user