mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
feat: 添加 API Key 锁定功能和请求头规则系统
1. API Key 锁定功能 - 管理员可锁定/解锁用户的 API Key - 锁定后用户无法使用、修改或删除该 Key - 前端显示锁定状态并禁用相关操作 2. 请求头规则系统 - 将 endpoint.headers 升级为 header_rules - 支持 set(设置)、drop(删除)、rename(重命名)操作 - 前端提供可视化规则编辑界面 - 包含数据迁移脚本 Close #37 Close #86 Close #88
This commit is contained in:
@@ -252,12 +252,21 @@
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="py-4 text-center">
|
||||
<Badge
|
||||
:variant="apiKey.is_active ? 'success' : 'destructive'"
|
||||
class="font-medium"
|
||||
>
|
||||
{{ apiKey.is_active ? '活跃' : '禁用' }}
|
||||
</Badge>
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<Badge
|
||||
:variant="apiKey.is_active ? 'success' : 'destructive'"
|
||||
class="font-medium"
|
||||
>
|
||||
{{ apiKey.is_active ? '活跃' : '禁用' }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="apiKey.is_locked"
|
||||
variant="secondary"
|
||||
class="text-xs"
|
||||
>
|
||||
已锁定
|
||||
</Badge>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="py-4">
|
||||
<div class="flex justify-center gap-1">
|
||||
@@ -279,6 +288,22 @@
|
||||
>
|
||||
<DollarSign class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
:title="apiKey.is_locked ? '解锁' : '锁定'"
|
||||
@click="toggleLockApiKey(apiKey)"
|
||||
>
|
||||
<Lock
|
||||
v-if="apiKey.is_locked"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
<LockOpen
|
||||
v-else
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -343,12 +368,21 @@
|
||||
{{ apiKey.name || '未命名 Key' }}
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
:variant="apiKey.is_active ? 'success' : 'destructive'"
|
||||
class="text-xs flex-shrink-0"
|
||||
>
|
||||
{{ apiKey.is_active ? '活跃' : '禁用' }}
|
||||
</Badge>
|
||||
<div class="flex flex-col items-end gap-1">
|
||||
<Badge
|
||||
:variant="apiKey.is_active ? 'success' : 'destructive'"
|
||||
class="text-xs flex-shrink-0"
|
||||
>
|
||||
{{ apiKey.is_active ? '活跃' : '禁用' }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="apiKey.is_locked"
|
||||
variant="secondary"
|
||||
class="text-xs"
|
||||
>
|
||||
已锁定
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2 text-[11px] text-muted-foreground">
|
||||
@@ -455,6 +489,21 @@
|
||||
<DollarSign class="h-3.5 w-3.5 mr-1.5" />
|
||||
调整
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="toggleLockApiKey(apiKey)"
|
||||
>
|
||||
<Lock
|
||||
v-if="apiKey.is_locked"
|
||||
class="h-3.5 w-3.5 mr-1.5"
|
||||
/>
|
||||
<LockOpen
|
||||
v-else
|
||||
class="h-3.5 w-3.5 mr-1.5"
|
||||
/>
|
||||
{{ apiKey.is_locked ? '解锁' : '锁定' }}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -466,7 +515,7 @@
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="text-rose-600"
|
||||
class="text-rose-600 col-span-2"
|
||||
@click="deleteApiKey(apiKey)"
|
||||
>
|
||||
<Trash2 class="h-3.5 w-3.5 mr-1.5" />
|
||||
@@ -685,7 +734,9 @@ import {
|
||||
Copy,
|
||||
CheckCircle,
|
||||
SquarePen,
|
||||
Search
|
||||
Search,
|
||||
Lock,
|
||||
LockOpen
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
import { StandaloneKeyFormDialog, type StandaloneKeyFormData } from '@/features/api-keys'
|
||||
@@ -830,6 +881,20 @@ async function toggleApiKey(apiKey: AdminApiKey) {
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleLockApiKey(apiKey: AdminApiKey) {
|
||||
try {
|
||||
const response = await adminApi.toggleLockApiKey(apiKey.id)
|
||||
const index = apiKeys.value.findIndex(k => k.id === apiKey.id)
|
||||
if (index !== -1) {
|
||||
apiKeys.value[index].is_locked = response.is_locked
|
||||
}
|
||||
success(response.message)
|
||||
} catch (err: any) {
|
||||
log.error('切换密钥锁定状态失败:', err)
|
||||
error(err.response?.data?.detail || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteApiKey(apiKey: AdminApiKey) {
|
||||
const confirmed = await confirmDanger(
|
||||
`确定要删除这个独立余额 Key 吗?\n\n${apiKey.name || apiKey.key_display || 'sk-****'}\n\n此操作无法撤销。`,
|
||||
|
||||
@@ -556,6 +556,13 @@
|
||||
>
|
||||
{{ apiKey.is_active ? '活跃' : '已禁用' }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="apiKey.is_locked"
|
||||
variant="secondary"
|
||||
class="text-xs"
|
||||
>
|
||||
已锁定
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="apiKey.is_standalone"
|
||||
variant="default"
|
||||
@@ -588,6 +595,22 @@
|
||||
${{ (apiKey.total_cost_usd || 0).toFixed(4) }}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
:title="apiKey.is_locked ? '解锁' : '锁定'"
|
||||
@click="toggleLockApiKey(apiKey)"
|
||||
>
|
||||
<Lock
|
||||
v-if="apiKey.is_locked"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
<LockOpen
|
||||
v-else
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -740,7 +763,9 @@ import {
|
||||
Trash2,
|
||||
Copy,
|
||||
Search,
|
||||
CheckCircle
|
||||
CheckCircle,
|
||||
Lock,
|
||||
LockOpen
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
// 功能组件
|
||||
@@ -1024,6 +1049,21 @@ async function deleteApiKey(apiKey: any) {
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleLockApiKey(apiKey: any) {
|
||||
try {
|
||||
const response = await adminApi.toggleLockApiKey(apiKey.id)
|
||||
// 更新本地状态
|
||||
const index = userApiKeys.value.findIndex(k => k.id === apiKey.id)
|
||||
if (index !== -1) {
|
||||
userApiKeys.value[index].is_locked = response.is_locked
|
||||
}
|
||||
success(response.message)
|
||||
} catch (err: any) {
|
||||
log.error('切换密钥锁定状态失败:', err)
|
||||
error(err.response?.data?.error?.message || err.response?.data?.detail || '操作失败', '锁定/解锁失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function copyFullKey(apiKey: any) {
|
||||
try {
|
||||
// 调用后端 API 获取完整密钥
|
||||
|
||||
@@ -129,12 +129,14 @@
|
||||
:key="cap.name"
|
||||
class="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium transition-all"
|
||||
:class="[
|
||||
apiKey.is_locked ? 'opacity-50 cursor-not-allowed' : '',
|
||||
isCapabilityEnabled(apiKey, cap.name)
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-transparent text-muted-foreground border border-dashed border-muted-foreground/50 hover:border-primary/50 hover:text-foreground'
|
||||
]"
|
||||
:title="getCapabilityTooltip(cap, isCapabilityEnabled(apiKey, cap.name))"
|
||||
@click.stop="toggleCapability(apiKey, cap.name)"
|
||||
:title="apiKey.is_locked ? '已锁定' : getCapabilityTooltip(cap, isCapabilityEnabled(apiKey, cap.name))"
|
||||
:disabled="apiKey.is_locked"
|
||||
@click.stop="!apiKey.is_locked && toggleCapability(apiKey, cap.name)"
|
||||
>
|
||||
<Check
|
||||
v-if="isCapabilityEnabled(apiKey, cap.name)"
|
||||
@@ -191,12 +193,21 @@
|
||||
|
||||
<!-- 状态 -->
|
||||
<TableCell class="py-4 text-center">
|
||||
<Badge
|
||||
:variant="apiKey.is_active ? 'success' : 'secondary'"
|
||||
class="font-medium px-3 py-1"
|
||||
>
|
||||
{{ apiKey.is_active ? '活跃' : '禁用' }}
|
||||
</Badge>
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<Badge
|
||||
:variant="apiKey.is_active ? 'success' : 'secondary'"
|
||||
class="font-medium px-3 py-1"
|
||||
>
|
||||
{{ apiKey.is_active ? '活跃' : '禁用' }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="apiKey.is_locked"
|
||||
variant="warning"
|
||||
class="font-medium text-[10px]"
|
||||
>
|
||||
已锁定
|
||||
</Badge>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<!-- 最后使用时间 -->
|
||||
@@ -211,7 +222,8 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
:title="apiKey.is_active ? '禁用' : '启用'"
|
||||
:title="apiKey.is_locked ? '已锁定' : (apiKey.is_active ? '禁用' : '启用')"
|
||||
:disabled="apiKey.is_locked"
|
||||
@click="toggleApiKey(apiKey)"
|
||||
>
|
||||
<Power class="h-4 w-4" />
|
||||
@@ -220,7 +232,8 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
title="删除"
|
||||
:title="apiKey.is_locked ? '已锁定' : '删除'"
|
||||
:disabled="apiKey.is_locked"
|
||||
@click="confirmDelete(apiKey)"
|
||||
>
|
||||
<Trash2 class="h-4 w-4" />
|
||||
@@ -256,6 +269,13 @@
|
||||
>
|
||||
{{ apiKey.is_active ? '活跃' : '禁用' }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="apiKey.is_locked"
|
||||
variant="warning"
|
||||
class="text-[10px] px-1.5 py-0"
|
||||
>
|
||||
已锁定
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="flex items-center gap-0.5 flex-shrink-0">
|
||||
<Button
|
||||
@@ -271,7 +291,8 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
:title="apiKey.is_active ? '禁用' : '启用'"
|
||||
:title="apiKey.is_locked ? '已锁定' : (apiKey.is_active ? '禁用' : '启用')"
|
||||
:disabled="apiKey.is_locked"
|
||||
@click="toggleApiKey(apiKey)"
|
||||
>
|
||||
<Power class="h-3.5 w-3.5" />
|
||||
@@ -280,7 +301,8 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
title="删除"
|
||||
:title="apiKey.is_locked ? '已锁定' : '删除'"
|
||||
:disabled="apiKey.is_locked"
|
||||
@click="confirmDelete(apiKey)"
|
||||
>
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
|
||||
Reference in New Issue
Block a user