feat: add Ark API Key management (list/create/toggle/delete)
- New VolcengineClient.call_json() for POST+JSON signing (Ark API) - ArkService for API Key CRUD operations - Backend views: list/create/toggle/delete ark keys per project - Frontend: ArkKeysView with project selector, key table, create dialog - Created key value shown once with copy button Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8e564ed640
commit
0ac2ef1f27
@ -47,4 +47,10 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Projects
|
# Projects
|
||||||
path('projects/', views.project_list_view),
|
path('projects/', views.project_list_view),
|
||||||
|
|
||||||
|
# Ark API Key management
|
||||||
|
path('ark-keys/<str:project_name>/', views.ark_key_list_view),
|
||||||
|
path('ark-keys/<str:project_name>/create/', views.ark_key_create_view),
|
||||||
|
path('ark-keys/<int:key_id>/toggle/', views.ark_key_toggle_view),
|
||||||
|
path('ark-keys/<int:key_id>/delete/', views.ark_key_delete_view),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -12,6 +12,7 @@ from rest_framework.response import Response
|
|||||||
from utils.crypto import encrypt, decrypt, make_hint
|
from utils.crypto import encrypt, decrypt, make_hint
|
||||||
from utils.iam_service import IAMService, ProjectService
|
from utils.iam_service import IAMService, ProjectService
|
||||||
from utils.billing_service import BillingService
|
from utils.billing_service import BillingService
|
||||||
|
from utils.ark_service import ArkService
|
||||||
from utils.volcengine_client import VolcengineAPIError
|
from utils.volcengine_client import VolcengineAPIError
|
||||||
|
|
||||||
from .models import VolcAccount, IAMUser, IAMUserProject, GlobalConfig, AlertRecord, SpendingRecord, QuotaAllocation
|
from .models import VolcAccount, IAMUser, IAMUserProject, GlobalConfig, AlertRecord, SpendingRecord, QuotaAllocation
|
||||||
@ -921,3 +922,104 @@ def project_list_view(request):
|
|||||||
except VolcengineAPIError as e:
|
except VolcengineAPIError as e:
|
||||||
return Response({'error': 'api_error', 'message': str(e)},
|
return Response({'error': 'api_error', 'message': str(e)},
|
||||||
status=status.HTTP_502_BAD_GATEWAY)
|
status=status.HTTP_502_BAD_GATEWAY)
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== Ark API Key Management ====================
|
||||||
|
|
||||||
|
def _get_ark_service():
|
||||||
|
"""获取 ArkService 实例"""
|
||||||
|
account, ak, sk = _get_volc_account()
|
||||||
|
if not ak:
|
||||||
|
return None, None
|
||||||
|
return ArkService(ak, sk), account
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(['GET'])
|
||||||
|
def ark_key_list_view(request, project_name):
|
||||||
|
"""列出项目下的方舟 API Key"""
|
||||||
|
svc, _ = _get_ark_service()
|
||||||
|
if not svc:
|
||||||
|
return Response({'error': 'no_account', 'message': '请先配置火山主账号'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
try:
|
||||||
|
resp = svc.list_api_keys(project_name)
|
||||||
|
items = resp.get("Result", {}).get("Items", [])
|
||||||
|
return Response({
|
||||||
|
'total': resp.get("Result", {}).get("TotalCount", 0),
|
||||||
|
'keys': items,
|
||||||
|
})
|
||||||
|
except VolcengineAPIError as e:
|
||||||
|
return Response({'error': 'api_error', 'message': str(e)},
|
||||||
|
status=status.HTTP_502_BAD_GATEWAY)
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(['POST'])
|
||||||
|
def ark_key_create_view(request, project_name):
|
||||||
|
"""在项目下创建方舟 API Key"""
|
||||||
|
name = request.data.get('name', '')
|
||||||
|
if not name:
|
||||||
|
return Response({'error': 'missing_name', 'message': '请输入 Key 名称'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
svc, _ = _get_ark_service()
|
||||||
|
if not svc:
|
||||||
|
return Response({'error': 'no_account'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
try:
|
||||||
|
resp = svc.create_api_key(project_name, name)
|
||||||
|
key_data = resp.get("Result", {})
|
||||||
|
AlertRecord.objects.create(
|
||||||
|
alert_type=AlertRecord.AlertType.MANUAL,
|
||||||
|
title=f"创建方舟 API Key: {name}",
|
||||||
|
content=f"操作人: {request.user.username},项目: {project_name}",
|
||||||
|
)
|
||||||
|
return Response({
|
||||||
|
'message': f'API Key "{name}" 创建成功',
|
||||||
|
'key': key_data,
|
||||||
|
}, status=status.HTTP_201_CREATED)
|
||||||
|
except VolcengineAPIError as e:
|
||||||
|
return Response({'error': 'api_error', 'message': str(e)},
|
||||||
|
status=status.HTTP_502_BAD_GATEWAY)
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(['POST'])
|
||||||
|
def ark_key_toggle_view(request, key_id):
|
||||||
|
"""启用/停用方舟 API Key"""
|
||||||
|
new_status = request.data.get('status', '')
|
||||||
|
if new_status not in ('Active', 'Inactive'):
|
||||||
|
return Response({'error': 'invalid_status', 'message': 'status 必须是 Active 或 Inactive'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
svc, _ = _get_ark_service()
|
||||||
|
if not svc:
|
||||||
|
return Response({'error': 'no_account'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
try:
|
||||||
|
svc.update_api_key_status(key_id, new_status)
|
||||||
|
action = '启用' if new_status == 'Active' else '停用'
|
||||||
|
AlertRecord.objects.create(
|
||||||
|
alert_type=AlertRecord.AlertType.MANUAL,
|
||||||
|
title=f"{action}方舟 API Key (ID: {key_id})",
|
||||||
|
content=f"操作人: {request.user.username}",
|
||||||
|
)
|
||||||
|
return Response({'message': f'API Key 已{action}'})
|
||||||
|
except VolcengineAPIError as e:
|
||||||
|
return Response({'error': 'api_error', 'message': str(e)},
|
||||||
|
status=status.HTTP_502_BAD_GATEWAY)
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(['DELETE'])
|
||||||
|
def ark_key_delete_view(request, key_id):
|
||||||
|
"""删除方舟 API Key"""
|
||||||
|
svc, _ = _get_ark_service()
|
||||||
|
if not svc:
|
||||||
|
return Response({'error': 'no_account'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
try:
|
||||||
|
svc.delete_api_key(key_id)
|
||||||
|
AlertRecord.objects.create(
|
||||||
|
alert_type=AlertRecord.AlertType.MANUAL,
|
||||||
|
title=f"删除方舟 API Key (ID: {key_id})",
|
||||||
|
content=f"操作人: {request.user.username}",
|
||||||
|
)
|
||||||
|
return Response({'message': 'API Key 已删除'})
|
||||||
|
except VolcengineAPIError as e:
|
||||||
|
return Response({'error': 'api_error', 'message': str(e)},
|
||||||
|
status=status.HTTP_502_BAD_GATEWAY)
|
||||||
|
|||||||
42
backend/utils/ark_service.py
Normal file
42
backend/utils/ark_service.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
"""方舟(Ark)API Key 管理服务"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from .volcengine_client import VolcengineClient, VolcengineAPIError, get_ark_client
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ArkService:
|
||||||
|
"""方舟 API Key 管理"""
|
||||||
|
|
||||||
|
def __init__(self, ak: str, sk: str):
|
||||||
|
self.client = get_ark_client(ak, sk)
|
||||||
|
|
||||||
|
def list_api_keys(self, project_name: str, page_size: int = 100, page_number: int = 1) -> dict:
|
||||||
|
"""列出项目下的 API Key"""
|
||||||
|
return self.client.call_json("ListApiKeys", {
|
||||||
|
"ProjectName": project_name,
|
||||||
|
"PageSize": page_size,
|
||||||
|
"PageNumber": page_number,
|
||||||
|
})
|
||||||
|
|
||||||
|
def create_api_key(self, project_name: str, name: str, resource_type: str = "all") -> dict:
|
||||||
|
"""在项目下创建 API Key"""
|
||||||
|
return self.client.call_json("CreateApiKey", {
|
||||||
|
"ProjectName": project_name,
|
||||||
|
"Name": name,
|
||||||
|
"ResourceInstances": [{"ResourceId": "*", "ResourceType": resource_type}],
|
||||||
|
})
|
||||||
|
|
||||||
|
def delete_api_key(self, api_key_id: int) -> dict:
|
||||||
|
"""删除 API Key"""
|
||||||
|
return self.client.call_json("DeleteApiKey", {
|
||||||
|
"Id": api_key_id,
|
||||||
|
})
|
||||||
|
|
||||||
|
def update_api_key_status(self, api_key_id: int, status: str) -> dict:
|
||||||
|
"""启用/停用 API Key (status: Active / Inactive)"""
|
||||||
|
return self.client.call_json("UpdateApiKey", {
|
||||||
|
"Id": api_key_id,
|
||||||
|
"Status": status,
|
||||||
|
})
|
||||||
@ -46,25 +46,25 @@ class VolcengineClient:
|
|||||||
def _hash_sha256(self, content: str) -> str:
|
def _hash_sha256(self, content: str) -> str:
|
||||||
return hashlib.sha256(content.encode("utf-8")).hexdigest()
|
return hashlib.sha256(content.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
def call(self, action: str, params: dict = None, body: str = "", extra_headers: dict = None) -> dict:
|
def _sign_and_call(self, action: str, method: str, content_type: str,
|
||||||
params = params or {}
|
query_params: dict, body_bytes: bytes) -> dict:
|
||||||
|
"""统一签名并调用"""
|
||||||
now = datetime.datetime.now(datetime.timezone.utc)
|
now = datetime.datetime.now(datetime.timezone.utc)
|
||||||
x_date = now.strftime("%Y%m%dT%H%M%SZ")
|
x_date = now.strftime("%Y%m%dT%H%M%SZ")
|
||||||
short_date = x_date[:8]
|
short_date = x_date[:8]
|
||||||
|
|
||||||
x_content_sha256 = self._hash_sha256(body)
|
x_content_sha256 = hashlib.sha256(body_bytes).hexdigest()
|
||||||
all_params = {"Action": action, "Version": self.version, **params}
|
query_string = self._norm_query(query_params)
|
||||||
|
|
||||||
signed_headers_str = "content-type;host;x-content-sha256;x-date"
|
signed_headers_str = "content-type;host;x-content-sha256;x-date"
|
||||||
canonical_headers = (
|
canonical_headers = (
|
||||||
f"content-type:application/x-www-form-urlencoded\n"
|
f"content-type:{content_type}\n"
|
||||||
f"host:{self.host}\n"
|
f"host:{self.host}\n"
|
||||||
f"x-content-sha256:{x_content_sha256}\n"
|
f"x-content-sha256:{x_content_sha256}\n"
|
||||||
f"x-date:{x_date}"
|
f"x-date:{x_date}"
|
||||||
)
|
)
|
||||||
query_string = self._norm_query(all_params)
|
|
||||||
canonical_request = "\n".join([
|
canonical_request = "\n".join([
|
||||||
"GET", "/", query_string,
|
method, "/", query_string,
|
||||||
canonical_headers, "", signed_headers_str, x_content_sha256
|
canonical_headers, "", signed_headers_str, x_content_sha256
|
||||||
])
|
])
|
||||||
|
|
||||||
@ -84,19 +84,19 @@ class VolcengineClient:
|
|||||||
"Host": self.host,
|
"Host": self.host,
|
||||||
"X-Date": x_date,
|
"X-Date": x_date,
|
||||||
"X-Content-Sha256": x_content_sha256,
|
"X-Content-Sha256": x_content_sha256,
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
"Content-Type": content_type,
|
||||||
"Authorization": (
|
"Authorization": (
|
||||||
f"HMAC-SHA256 Credential={self.ak}/{credential_scope}, "
|
f"HMAC-SHA256 Credential={self.ak}/{credential_scope}, "
|
||||||
f"SignedHeaders={signed_headers_str}, Signature={signature}"
|
f"SignedHeaders={signed_headers_str}, Signature={signature}"
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
if extra_headers:
|
|
||||||
headers.update(extra_headers)
|
|
||||||
|
|
||||||
url = f"https://{self.host}/?{query_string}"
|
url = f"https://{self.host}/?{query_string}"
|
||||||
try:
|
try:
|
||||||
r = requests.get(url, headers=headers, timeout=30)
|
if method == "GET":
|
||||||
|
r = requests.get(url, headers=headers, timeout=30)
|
||||||
|
else:
|
||||||
|
r = requests.post(url, headers=headers, data=body_bytes, timeout=30)
|
||||||
resp = r.json()
|
resp = r.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise VolcengineAPIError(action, "NetworkError", str(e))
|
raise VolcengineAPIError(action, "NetworkError", str(e))
|
||||||
@ -108,6 +108,21 @@ class VolcengineClient:
|
|||||||
)
|
)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
def call(self, action: str, params: dict = None, body: str = "", extra_headers: dict = None) -> dict:
|
||||||
|
"""GET 方式调用(IAM / Billing 等传统接口)"""
|
||||||
|
params = params or {}
|
||||||
|
all_params = {"Action": action, "Version": self.version, **params}
|
||||||
|
return self._sign_and_call(action, "GET", "application/x-www-form-urlencoded",
|
||||||
|
all_params, body.encode("utf-8") if body else b"")
|
||||||
|
|
||||||
|
def call_json(self, action: str, body: dict = None) -> dict:
|
||||||
|
"""POST + JSON body 方式调用(方舟 Ark 等新接口)"""
|
||||||
|
import json
|
||||||
|
query_params = {"Action": action, "Version": self.version}
|
||||||
|
body_bytes = json.dumps(body or {}).encode("utf-8")
|
||||||
|
return self._sign_and_call(action, "POST", "application/json",
|
||||||
|
query_params, body_bytes)
|
||||||
|
|
||||||
|
|
||||||
def get_iam_client(ak: str, sk: str) -> VolcengineClient:
|
def get_iam_client(ak: str, sk: str) -> VolcengineClient:
|
||||||
return VolcengineClient(ak, sk, "iam", "iam.volcengineapi.com")
|
return VolcengineClient(ak, sk, "iam", "iam.volcengineapi.com")
|
||||||
@ -121,3 +136,9 @@ def get_billing_client(ak: str, sk: str) -> VolcengineClient:
|
|||||||
def get_resource_client(ak: str, sk: str) -> VolcengineClient:
|
def get_resource_client(ak: str, sk: str) -> VolcengineClient:
|
||||||
return VolcengineClient(ak, sk, "iam", "iam.volcengineapi.com",
|
return VolcengineClient(ak, sk, "iam", "iam.volcengineapi.com",
|
||||||
version="2021-08-01")
|
version="2021-08-01")
|
||||||
|
|
||||||
|
|
||||||
|
def get_ark_client(ak: str, sk: str) -> VolcengineClient:
|
||||||
|
"""方舟 API 客户端(使用 POST + JSON body)"""
|
||||||
|
return VolcengineClient(ak, sk, "ark", "open.volcengineapi.com",
|
||||||
|
region="cn-beijing", version="2024-01-01")
|
||||||
|
|||||||
@ -12,6 +12,10 @@
|
|||||||
<el-icon><User /></el-icon>
|
<el-icon><User /></el-icon>
|
||||||
<span>子账号管理</span>
|
<span>子账号管理</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/ark-keys">
|
||||||
|
<el-icon><Key /></el-icon>
|
||||||
|
<span>API Key 管理</span>
|
||||||
|
</el-menu-item>
|
||||||
<el-menu-item index="/billing">
|
<el-menu-item index="/billing">
|
||||||
<el-icon><Wallet /></el-icon>
|
<el-icon><Wallet /></el-icon>
|
||||||
<span>消费监控</span>
|
<span>消费监控</span>
|
||||||
@ -25,7 +29,7 @@
|
|||||||
<span>系统设置</span>
|
<span>系统设置</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="/admin">
|
<el-menu-item index="/admin">
|
||||||
<el-icon><Key /></el-icon>
|
<el-icon><Tools /></el-icon>
|
||||||
<span>系统管理</span>
|
<span>系统管理</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
|
|||||||
@ -16,6 +16,7 @@ const routes = [
|
|||||||
{ path: 'iam-users', name: 'IAMUsers', component: () => import('../views/iam/IAMUserList.vue') },
|
{ path: 'iam-users', name: 'IAMUsers', component: () => import('../views/iam/IAMUserList.vue') },
|
||||||
{ path: 'billing', name: 'Billing', component: () => import('../views/billing/BillingView.vue') },
|
{ path: 'billing', name: 'Billing', component: () => import('../views/billing/BillingView.vue') },
|
||||||
{ path: 'alerts', name: 'Alerts', component: () => import('../views/alerts/AlertList.vue') },
|
{ path: 'alerts', name: 'Alerts', component: () => import('../views/alerts/AlertList.vue') },
|
||||||
|
{ path: 'ark-keys', name: 'ArkKeys', component: () => import('../views/ark/ArkKeysView.vue') },
|
||||||
{ path: 'settings', name: 'Settings', component: () => import('../views/settings/SettingsView.vue') },
|
{ path: 'settings', name: 'Settings', component: () => import('../views/settings/SettingsView.vue') },
|
||||||
{ path: 'admin', name: 'Admin', component: () => import('../views/admin/AdminView.vue') },
|
{ path: 'admin', name: 'Admin', component: () => import('../views/admin/AdminView.vue') },
|
||||||
],
|
],
|
||||||
|
|||||||
217
frontend/src/views/ark/ArkKeysView.vue
Normal file
217
frontend/src/views/ark/ArkKeysView.vue
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
<template>
|
||||||
|
<div style="max-width: 1400px; margin: 0 auto;">
|
||||||
|
<h2 style="margin-bottom: 16px;">API Key 管理</h2>
|
||||||
|
|
||||||
|
<!-- Project selector -->
|
||||||
|
<div style="margin-bottom: 20px; display: flex; gap: 12px; align-items: center;">
|
||||||
|
<span style="color: #606266;">选择项目:</span>
|
||||||
|
<el-select v-model="selectedProject" placeholder="选择火山项目" filterable
|
||||||
|
style="width: 300px;" @change="loadKeys">
|
||||||
|
<el-option v-for="p in projects" :key="p.name"
|
||||||
|
:label="p.display_name || p.name" :value="p.name" />
|
||||||
|
</el-select>
|
||||||
|
<el-button @click="loadProjects" :loading="projectsLoading" text>
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" @click="showCreateDialog = true"
|
||||||
|
:disabled="!selectedProject">
|
||||||
|
创建 API Key
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Keys table -->
|
||||||
|
<el-table :data="keys" stripe v-loading="keysLoading" style="width: 100%;"
|
||||||
|
empty-text="请先选择项目">
|
||||||
|
<el-table-column prop="Name" label="名称" min-width="200" />
|
||||||
|
<el-table-column label="API Key" min-width="300">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<code style="font-size: 13px; color: #409eff;">{{ row.Key }}</code>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.Status === 'Active' ? 'success' : 'danger'" size="small">
|
||||||
|
{{ row.Status === 'Active' ? '启用' : '停用' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="创建者" min-width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ getCreator(row) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="创建时间" min-width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.CreateTime ? new Date(row.CreateTime).toLocaleString('zh-CN') : '' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button v-if="row.Status === 'Active'" size="small" text type="warning"
|
||||||
|
@click="handleToggle(row, 'Inactive')">停用</el-button>
|
||||||
|
<el-button v-else size="small" text type="success"
|
||||||
|
@click="handleToggle(row, 'Active')">启用</el-button>
|
||||||
|
<el-button size="small" text type="danger"
|
||||||
|
@click="handleDelete(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- Create dialog -->
|
||||||
|
<el-dialog v-model="showCreateDialog" title="创建 API Key" width="90%" style="max-width: 500px;">
|
||||||
|
<el-form label-width="100px">
|
||||||
|
<el-form-item label="所属项目">
|
||||||
|
<el-input :model-value="selectedProject" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Key 名称">
|
||||||
|
<el-input v-model="createName" placeholder="例如:production-key" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showCreateDialog = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleCreate" :loading="creating">创建</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- Show created key dialog -->
|
||||||
|
<el-dialog v-model="showCreatedKey" title="API Key 创建成功" width="90%" style="max-width: 600px;"
|
||||||
|
:close-on-click-modal="false">
|
||||||
|
<el-alert type="warning" :closable="false" show-icon style="margin-bottom: 16px;">
|
||||||
|
<template #title>API Key 仅显示一次,请立即复制保存!</template>
|
||||||
|
</el-alert>
|
||||||
|
<div style="background: #f5f7fa; padding: 16px; border-radius: 8px; word-break: break-all;">
|
||||||
|
<p style="margin-bottom: 8px;"><strong>Key:</strong></p>
|
||||||
|
<code style="font-size: 14px; color: #409eff;">{{ createdKeyValue }}</code>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" @click="copyCreatedKey">复制 Key</el-button>
|
||||||
|
<el-button @click="showCreatedKey = false">关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import api from '../../api'
|
||||||
|
|
||||||
|
const projects = ref([])
|
||||||
|
const projectsLoading = ref(false)
|
||||||
|
const selectedProject = ref('')
|
||||||
|
const keys = ref([])
|
||||||
|
const keysLoading = ref(false)
|
||||||
|
|
||||||
|
const showCreateDialog = ref(false)
|
||||||
|
const createName = ref('')
|
||||||
|
const creating = ref(false)
|
||||||
|
|
||||||
|
const showCreatedKey = ref(false)
|
||||||
|
const createdKeyValue = ref('')
|
||||||
|
|
||||||
|
async function loadProjects() {
|
||||||
|
projectsLoading.value = true
|
||||||
|
try {
|
||||||
|
const { data } = await api.get('/api/v1/projects/')
|
||||||
|
projects.value = data
|
||||||
|
} catch (e) {
|
||||||
|
projects.value = []
|
||||||
|
} finally {
|
||||||
|
projectsLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadKeys() {
|
||||||
|
if (!selectedProject.value) {
|
||||||
|
keys.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keysLoading.value = true
|
||||||
|
try {
|
||||||
|
const { data } = await api.get(`/api/v1/ark-keys/${selectedProject.value}/`)
|
||||||
|
keys.value = data.keys || []
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e.response?.data?.message || '获取 Key 列表失败')
|
||||||
|
keys.value = []
|
||||||
|
} finally {
|
||||||
|
keysLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCreator(row) {
|
||||||
|
const tag = (row.Tags || []).find(t => t.Key === 'sys:ark:createdBy')
|
||||||
|
if (tag) {
|
||||||
|
const parts = tag.Value.split('/')
|
||||||
|
return parts[parts.length - 1] // e.g. "IAMUser/76804896/zyc" → "zyc"
|
||||||
|
}
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCreate() {
|
||||||
|
if (!createName.value) {
|
||||||
|
ElMessage.warning('请输入 Key 名称')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
creating.value = true
|
||||||
|
try {
|
||||||
|
const { data } = await api.post(`/api/v1/ark-keys/${selectedProject.value}/create/`, {
|
||||||
|
name: createName.value,
|
||||||
|
})
|
||||||
|
ElMessage.success(data.message)
|
||||||
|
showCreateDialog.value = false
|
||||||
|
createName.value = ''
|
||||||
|
|
||||||
|
// Show the created key (full key is only shown once)
|
||||||
|
const keyValue = data.key?.PrimaryKey || data.key?.Key || ''
|
||||||
|
if (keyValue) {
|
||||||
|
createdKeyValue.value = keyValue
|
||||||
|
showCreatedKey.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadKeys()
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e.response?.data?.message || '创建失败')
|
||||||
|
} finally {
|
||||||
|
creating.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyCreatedKey() {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(createdKeyValue.value)
|
||||||
|
ElMessage.success('已复制到剪贴板')
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('复制失败,请手动复制')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleToggle(row, newStatus) {
|
||||||
|
const action = newStatus === 'Active' ? '启用' : '停用'
|
||||||
|
await ElMessageBox.confirm(`确定${action} "${row.Name}" 吗?`, '确认', { type: 'warning' })
|
||||||
|
try {
|
||||||
|
await api.post(`/api/v1/ark-keys/${row.Id}/toggle/`, { status: newStatus })
|
||||||
|
ElMessage.success(`已${action}`)
|
||||||
|
await loadKeys()
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e.response?.data?.message || `${action}失败`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(row) {
|
||||||
|
await ElMessageBox.confirm(`确定删除 "${row.Name}" 吗?此操作不可恢复!`, '确认删除', {
|
||||||
|
type: 'error',
|
||||||
|
confirmButtonText: '确定删除',
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
await api.delete(`/api/v1/ark-keys/${row.Id}/delete/`)
|
||||||
|
ElMessage.success('已删除')
|
||||||
|
await loadKeys()
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e.response?.data?.message || '删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadProjects()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
Loading…
x
Reference in New Issue
Block a user