feat: refine api key permissions and admin routes

This commit is contained in:
2026-05-10 23:22:26 +08:00
parent 0fc23d7eb8
commit d86651ff55
23 changed files with 1683 additions and 532 deletions
+66 -4
View File
@@ -22,6 +22,7 @@ import type {
} from '@easyai-ai-gateway/contracts';
import {
batchAccessRules,
batchApiKeyAccessRules,
createAccessRule,
createApiKey,
createGatewayUser,
@@ -29,6 +30,7 @@ import {
createTenant,
createUserGroup,
deleteAccessRule,
deleteApiKey,
deleteGatewayUser,
deletePlatform,
deleteTenant,
@@ -36,6 +38,7 @@ import {
getHealth,
getTask,
listAccessRules,
listApiKeyAccessRules,
listApiKeys,
listBaseModels,
listCatalogProviders,
@@ -148,7 +151,7 @@ export function App() {
const [users, setUsers] = useState<GatewayUser[]>([]);
const [userGroups, setUserGroups] = useState<UserGroup[]>([]);
const [apiKeys, setApiKeys] = useState<GatewayApiKey[]>([]);
const [apiKeyForm, setApiKeyForm] = useState<ApiKeyForm>({ name: 'Local smoke key' });
const [apiKeyForm, setApiKeyForm] = useState<ApiKeyForm>({ name: 'Local smoke key', expiresAt: '' });
const [apiKeySecret, setApiKeySecret] = useState('');
const [apiKeySecretsById, setApiKeySecretsById] = useState<Record<string, string>>({});
const [selectedPlaygroundApiKeyId, setSelectedPlaygroundApiKeyId] = useState('');
@@ -234,6 +237,10 @@ export function App() {
await ensureRouteData(nextToken, true);
}
function invalidateDataKeys(...keys: DataKey[]) {
keys.forEach((key) => loadedDataKeysRef.current.delete(key));
}
async function ensureRouteData(nextToken = token, force = false) {
await ensureData(dataKeysForRoute(activePage, adminSection, workspaceSection, Boolean(nextToken)), nextToken, force);
}
@@ -321,7 +328,9 @@ export function App() {
setUserGroups((await listUserGroups(nextToken)).items);
return;
case 'accessRules':
setAccessRules((await listAccessRules(nextToken)).items);
setAccessRules((await (activePage === 'workspace' && workspaceSection === 'apiKeys'
? listApiKeyAccessRules(nextToken)
: listAccessRules(nextToken))).items);
return;
case 'apiKeys':
setApiKeys((await listApiKeys(nextToken)).items);
@@ -369,16 +378,22 @@ export function App() {
setCoreState('loading');
setCoreMessage('');
try {
const response = await createApiKey(token, { name: apiKeyForm.name, scopes: ['chat', 'image', 'video'] });
const response = await createApiKey(token, {
name: apiKeyForm.name,
scopes: ['chat', 'image', 'video'],
expiresAt: apiKeyForm.expiresAt ? new Date(apiKeyForm.expiresAt).toISOString() : undefined,
});
setApiKeySecret(response.secret);
setApiKeySecretsById((current) => ({ ...current, [response.apiKey.id]: response.secret }));
setSelectedPlaygroundApiKeyId(response.apiKey.id);
setApiKeys((current) => [response.apiKey, ...current.filter((item) => item.id !== response.apiKey.id)]);
setApiKeyForm({ name: '', expiresAt: '' });
setCoreState('ready');
setCoreMessage('API Key 已创建,secret 仅展示一次。');
} catch (err) {
setCoreState('error');
setCoreMessage(err instanceof Error ? err.message : '创建 API Key 失败');
throw err;
}
}
@@ -461,6 +476,7 @@ export function App() {
try {
const item = userId ? await updateGatewayUser(token, userId, input) : await createGatewayUser(token, input);
setUsers((current) => [item, ...current.filter((user) => user.id !== item.id)]);
invalidateDataKeys('playgroundModels');
setCoreState('ready');
setCoreMessage(userId ? '用户已更新。' : '用户已创建。');
} catch (err) {
@@ -508,6 +524,7 @@ export function App() {
setUserGroups((current) => current.filter((group) => group.id !== groupId));
setTenants((current) => current.map((tenant) => tenant.defaultUserGroupId === groupId ? { ...tenant, defaultUserGroupId: undefined } : tenant));
setUsers((current) => current.map((user) => user.defaultUserGroupId === groupId ? { ...user, defaultUserGroupId: undefined } : user));
invalidateDataKeys('playgroundModels');
setCoreState('ready');
setCoreMessage('用户组已删除。');
} catch (err) {
@@ -517,12 +534,35 @@ export function App() {
}
}
async function removeAPIKey(apiKeyId: string) {
setCoreState('loading');
setCoreMessage('');
try {
await deleteApiKey(token, apiKeyId);
setApiKeys((current) => current.filter((item) => item.id !== apiKeyId));
setAccessRules((current) => current.filter((rule) => !(rule.subjectType === 'api_key' && rule.subjectId === apiKeyId)));
setApiKeySecretsById((current) => {
const next = { ...current };
delete next[apiKeyId];
return next;
});
if (selectedPlaygroundApiKeyId === apiKeyId) setSelectedPlaygroundApiKeyId('');
setCoreState('ready');
setCoreMessage('API Key 已删除,对应权限策略已同步清理。');
} catch (err) {
setCoreState('error');
setCoreMessage(err instanceof Error ? err.message : '删除 API Key 失败');
throw err;
}
}
async function saveAccessRule(input: GatewayAccessRuleUpsertRequest, ruleId?: string) {
setCoreState('loading');
setCoreMessage('');
try {
const item = ruleId ? await updateAccessRule(token, ruleId, input) : await createAccessRule(token, input);
setAccessRules((current) => [item, ...current.filter((rule) => rule.id !== item.id)]);
invalidateDataKeys('playgroundModels');
setCoreState('ready');
setCoreMessage(ruleId ? '访问权限规则已更新。' : '访问权限规则已创建。');
} catch (err) {
@@ -538,6 +578,7 @@ export function App() {
try {
await deleteAccessRule(token, ruleId);
setAccessRules((current) => current.filter((rule) => rule.id !== ruleId));
invalidateDataKeys('playgroundModels');
setCoreState('ready');
setCoreMessage('访问权限规则已删除。');
} catch (err) {
@@ -553,6 +594,7 @@ export function App() {
try {
const response = await batchAccessRules(token, input);
setAccessRules(response.items);
invalidateDataKeys('playgroundModels');
setCoreState('ready');
setCoreMessage('访问权限已更新。');
} catch (err) {
@@ -562,6 +604,21 @@ export function App() {
}
}
async function batchSaveAPIKeyAccessRules(input: GatewayAccessRuleBatchRequest) {
setCoreState('loading');
setCoreMessage('');
try {
const response = await batchApiKeyAccessRules(token, input);
setAccessRules(response.items);
setCoreState('ready');
setCoreMessage('API Key 权限已更新。');
} catch (err) {
setCoreState('error');
setCoreMessage(err instanceof Error ? err.message : '批量更新 API Key 权限失败');
throw err;
}
}
async function submitTask(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const credential = apiKeySecret || token;
@@ -695,9 +752,14 @@ export function App() {
<WorkspacePage
apiKeyForm={apiKeyForm}
apiKeySecret={apiKeySecret}
apiKeySecretsById={apiKeySecretsById}
apiKeyPolicyModels={playgroundModels}
data={data}
message={coreMessage}
section={workspaceSection}
state={coreState}
onBatchAccessRules={batchSaveAPIKeyAccessRules}
onDeleteApiKey={removeAPIKey}
onApiKeyFormChange={setApiKeyForm}
onSectionChange={navigateWorkspaceSection}
onSubmitApiKey={submitAPIKey}
@@ -861,7 +923,7 @@ function dataKeysForRoute(
if (activePage === 'workspace') {
if (workspaceSection === 'overview') return ['users', 'userGroups', 'apiKeys'];
if (workspaceSection === 'apiKeys') return ['apiKeys'];
if (workspaceSection === 'apiKeys') return ['apiKeys', 'accessRules', 'playgroundModels'];
return [];
}