import { Alert, Button, Card, Empty, Form, Input, InputNumber, Message, Modal, Select, Space, Switch, Table, Tag, Typography } from '@arco-design/web-react' import { useCallback, useEffect, useState } from 'react' import { createApiKey, listApiKeys, revokeApiKey, toggleApiKey, type ApiKeyCreateInput, type ApiKeySummary } from '../../services/api-keys' import type { UserRole } from '../../services/users' import { useAuthStore } from '../../stores/auth' import { resolveErrorMessage } from '../../utils/error' import { isAdmin, roleLabel } from '../../utils/permissions' import { formatDateTime } from '../../utils/format' const roleOptions = [ { label: '管理员 (admin)', value: 'admin' }, { label: '运维 (operator)', value: 'operator' }, { label: '只读 (viewer)', value: 'viewer' }, ] // ApiKeysPage API Key 管理(admin 专属)。 // 新创建的 Key 明文只返回一次,需要用户立即保存。 export function ApiKeysPage() { const user = useAuthStore((s) => s.user) const [items, setItems] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState('') const [modalVisible, setModalVisible] = useState(false) const [draft, setDraft] = useState({ name: '', role: 'viewer', ttlHours: 0 }) const [submitting, setSubmitting] = useState(false) const [plainKey, setPlainKey] = useState('') const load = useCallback(async () => { setLoading(true) try { setItems(await listApiKeys()) setError('') } catch (e) { setError(resolveErrorMessage(e, '加载 API Key 失败')) } finally { setLoading(false) } }, []) useEffect(() => { void load() }, [load]) function openCreate() { setDraft({ name: '', role: 'viewer', ttlHours: 0 }) setPlainKey('') setModalVisible(true) } async function handleSubmit() { if (!draft.name.trim()) { Message.error('名称不能为空') return } setSubmitting(true) try { const result = await createApiKey(draft) setPlainKey(result.plainKey) await load() } catch (e) { Message.error(resolveErrorMessage(e, '创建失败')) } finally { setSubmitting(false) } } async function handleToggle(item: ApiKeySummary) { try { await toggleApiKey(item.id, !item.disabled) Message.success(item.disabled ? '已启用' : '已停用') await load() } catch (e) { Message.error(resolveErrorMessage(e, '操作失败')) } } async function handleRevoke(item: ApiKeySummary) { if (!window.confirm(`确定撤销 API Key「${item.name}」?操作不可撤销。`)) return try { await revokeApiKey(item.id) Message.success('已撤销') await load() } catch (e) { Message.error(resolveErrorMessage(e, '撤销失败')) } } async function copyPlainKey() { if (!plainKey) return try { await navigator.clipboard.writeText(plainKey) Message.success('已复制到剪贴板') } catch { Message.info('请手动选择文本复制') } } if (!isAdmin(user)) { return } return (
API Key 签发 API Key 供 CI/CD、监控脚本等非交互式场景访问 BackupX。在请求头加 Authorization: Bearer bax_xxxX-Api-Key: bax_xxx 即可。
{error ? {error} : null} } columns={[ { title: '名称', dataIndex: 'name' }, { title: '角色', dataIndex: 'role', render: (v: string) => {roleLabel(v)} }, { title: 'Key 前缀', dataIndex: 'prefix', render: (v: string) => {v}… }, { title: '创建者', dataIndex: 'createdBy', render: (v: string) => v || '-' }, { title: '最近使用', dataIndex: 'lastUsedAt', render: (v?: string) => v ? formatDateTime(v) : '从未使用' }, { title: '过期', dataIndex: 'expiresAt', render: (v?: string) => v ? formatDateTime(v) : '永不过期' }, { title: '状态', dataIndex: 'disabled', render: (disabled: boolean) => disabled ? 已停用 : 启用 }, { title: '操作', width: 180, render: (_: unknown, row: ApiKeySummary) => ( ) }, ]} /> { setModalVisible(false); setPlainKey('') }} onOk={plainKey ? () => { setModalVisible(false); setPlainKey('') } : handleSubmit} okText={plainKey ? '完成' : '生成'} confirmLoading={submitting} unmountOnExit > {plainKey ? ( ) : (
setDraft({ ...draft, name: v })} placeholder="例如:ci-deploy-script" />