feat(identity): 支持用户和用户组批量管理
增加原子批量启用、禁用和删除接口及管理端多选操作,目标缺失时整批回滚。\n\n拆分管理端与 API Key 权限缓存并在弹窗保存后刷新候选;补齐失效规则一键清理样式、固定右侧操作列和 OpenAPI 契约。
This commit is contained in:
+89
-21
@@ -24,6 +24,7 @@ import type {
|
||||
GatewayUser,
|
||||
GatewayWalletAccount,
|
||||
GatewayWalletTransaction,
|
||||
IdentityBatchAction,
|
||||
IntegrationPlatform,
|
||||
ModelCatalogResponse,
|
||||
ModelRateLimitStatus,
|
||||
@@ -42,6 +43,8 @@ import type {
|
||||
import {
|
||||
batchAccessRules,
|
||||
batchApiKeyAccessRules,
|
||||
batchGatewayUsers,
|
||||
batchUserGroups,
|
||||
createAccessRule,
|
||||
createApiKey,
|
||||
createFileStorageChannel,
|
||||
@@ -179,7 +182,7 @@ import type {
|
||||
WorkspaceSection,
|
||||
} from './types';
|
||||
|
||||
type DataKey =
|
||||
export type DataKey =
|
||||
| 'health'
|
||||
| 'currentUser'
|
||||
| 'currentUserGroups'
|
||||
@@ -210,7 +213,8 @@ type DataKey =
|
||||
| 'tasks'
|
||||
| 'wallet'
|
||||
| 'walletTransactions'
|
||||
| 'accessRules'
|
||||
| 'adminAccessRules'
|
||||
| 'apiKeyAccessRules'
|
||||
| 'auditLogs'
|
||||
| 'apiKeys';
|
||||
|
||||
@@ -251,7 +255,8 @@ export function App() {
|
||||
const [pricingRuleSets, setPricingRuleSets] = useState<PricingRuleSet[]>([]);
|
||||
const [runnerPolicy, setRunnerPolicy] = useState<GatewayRunnerPolicy | null>(null);
|
||||
const [runtimePolicySets, setRuntimePolicySets] = useState<RuntimePolicySet[]>([]);
|
||||
const [accessRules, setAccessRules] = useState<GatewayAccessRule[]>([]);
|
||||
const [adminAccessRules, setAdminAccessRules] = useState<GatewayAccessRule[]>([]);
|
||||
const [apiKeyAccessRules, setApiKeyAccessRules] = useState<GatewayAccessRule[]>([]);
|
||||
const [auditLogs, setAuditLogs] = useState<GatewayAuditLog[]>([]);
|
||||
const [rateLimitWindows, setRateLimitWindows] = useState<RateLimitWindow[]>([]);
|
||||
const [modelRateLimits, setModelRateLimits] = useState<ModelRateLimitStatus[]>([]);
|
||||
@@ -424,13 +429,13 @@ export function App() {
|
||||
{ label: 'Provider', value: activeProviders || providers.length, tone: 'amber' },
|
||||
{ label: '定价规则', value: pricingRules.length, tone: 'cyan' },
|
||||
{ label: '运行策略', value: runtimePolicySets.length, tone: 'slate' },
|
||||
{ label: '访问规则', value: accessRules.length, tone: 'amber' },
|
||||
{ label: '访问规则', value: adminAccessRules.length, tone: 'amber' },
|
||||
{ label: '限流窗口', value: activeRateWindows, tone: 'rose' },
|
||||
];
|
||||
}, [accessRules.length, models, platforms, pricingRules.length, providers, rateLimitWindows, runtimePolicySets.length, tenants.length, userGroups.length, users.length]);
|
||||
}, [adminAccessRules.length, models, platforms, pricingRules.length, providers, rateLimitWindows, runtimePolicySets.length, tenants.length, userGroups.length, users.length]);
|
||||
|
||||
const data = useMemo<ConsoleData>(() => ({
|
||||
accessRules,
|
||||
accessRules: activePage === 'workspace' ? apiKeyAccessRules : adminAccessRules,
|
||||
adminTasks,
|
||||
auditLogs,
|
||||
apiKeys,
|
||||
@@ -461,7 +466,7 @@ export function App() {
|
||||
users,
|
||||
walletAccounts,
|
||||
walletTransactions,
|
||||
}), [accessRules, adminTasks, apiKeys, auditLogs, baseModels, clientCustomizationSettings, currentUser, currentUserGroups, fileStorageChannels, fileStorageSettings, modelCatalog, modelRateLimits, modelRateLimitsUpdatedAt, models, networkProxyConfig, platforms, pricingRuleSets, pricingRules, providers, rateLimitWindows, runnerPolicy, runtimePolicySets, securityEventConnection, taskResult, tasks, tenants, userGroups, users, walletAccounts, walletTransactions, workerClusterRuntime]);
|
||||
}), [activePage, adminAccessRules, adminTasks, apiKeyAccessRules, apiKeys, auditLogs, baseModels, clientCustomizationSettings, currentUser, currentUserGroups, fileStorageChannels, fileStorageSettings, modelCatalog, modelRateLimits, modelRateLimitsUpdatedAt, models, networkProxyConfig, platforms, pricingRuleSets, pricingRules, providers, rateLimitWindows, runnerPolicy, runtimePolicySets, securityEventConnection, taskResult, tasks, tenants, userGroups, users, walletAccounts, walletTransactions, workerClusterRuntime]);
|
||||
|
||||
async function refresh(nextToken = token) {
|
||||
await ensureRouteData(nextToken, true);
|
||||
@@ -653,10 +658,11 @@ export function App() {
|
||||
loadedTransactionQueryKeyRef.current = requestKey;
|
||||
return;
|
||||
}
|
||||
case 'accessRules':
|
||||
setAccessRules((await (activePage === 'workspace' && workspaceSection === 'apiKeys'
|
||||
? listApiKeyAccessRules(nextToken)
|
||||
: listAccessRules(nextToken))).items);
|
||||
case 'adminAccessRules':
|
||||
setAdminAccessRules((await listAccessRules(nextToken)).items);
|
||||
return;
|
||||
case 'apiKeyAccessRules':
|
||||
setApiKeyAccessRules((await listApiKeyAccessRules(nextToken)).items);
|
||||
return;
|
||||
case 'auditLogs':
|
||||
setAuditLogs((await listAuditLogs(nextToken)).items);
|
||||
@@ -957,6 +963,28 @@ export function App() {
|
||||
}
|
||||
}
|
||||
|
||||
async function batchOperateUsers(ids: string[], action: IdentityBatchAction) {
|
||||
setCoreState('loading');
|
||||
setCoreMessage('');
|
||||
try {
|
||||
const response = await batchGatewayUsers(token, { ids, action });
|
||||
const affected = new Set(response.ids);
|
||||
if (action === 'delete') {
|
||||
setUsers((current) => current.filter((user) => !affected.has(user.id)));
|
||||
} else {
|
||||
const status = action === 'enable' ? 'active' : 'disabled';
|
||||
setUsers((current) => current.map((user) => affected.has(user.id) ? { ...user, status } : user));
|
||||
}
|
||||
invalidateDataKeys('playgroundModels');
|
||||
setCoreState('ready');
|
||||
setCoreMessage(`已批量${identityBatchActionLabel(action)} ${response.affectedCount} 个用户。`);
|
||||
} catch (err) {
|
||||
setCoreState('error');
|
||||
setCoreMessage(err instanceof Error ? err.message : '批量操作用户失败');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveUserGroup(input: UserGroupUpsertRequest, groupId?: string) {
|
||||
setCoreState('loading');
|
||||
setCoreMessage('');
|
||||
@@ -981,6 +1009,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));
|
||||
setAdminAccessRules((current) => current.filter((rule) => !(rule.subjectType === 'user_group' && rule.subjectId === groupId)));
|
||||
invalidateDataKeys('modelCatalog', 'playgroundModels');
|
||||
setCoreState('ready');
|
||||
setCoreMessage('用户组已删除。');
|
||||
@@ -991,13 +1020,42 @@ export function App() {
|
||||
}
|
||||
}
|
||||
|
||||
async function batchOperateUserGroups(ids: string[], action: IdentityBatchAction) {
|
||||
setCoreState('loading');
|
||||
setCoreMessage('');
|
||||
try {
|
||||
const response = await batchUserGroups(token, { ids, action });
|
||||
const affected = new Set(response.ids);
|
||||
if (action === 'delete') {
|
||||
setUserGroups((current) => current.filter((group) => !affected.has(group.id)));
|
||||
setTenants((current) => current.map((tenant) => tenant.defaultUserGroupId && affected.has(tenant.defaultUserGroupId)
|
||||
? { ...tenant, defaultUserGroupId: undefined }
|
||||
: tenant));
|
||||
setUsers((current) => current.map((user) => user.defaultUserGroupId && affected.has(user.defaultUserGroupId)
|
||||
? { ...user, defaultUserGroupId: undefined }
|
||||
: user));
|
||||
setAdminAccessRules((current) => current.filter((rule) => !(rule.subjectType === 'user_group' && affected.has(rule.subjectId))));
|
||||
} else {
|
||||
const status = action === 'enable' ? 'active' : 'disabled';
|
||||
setUserGroups((current) => current.map((group) => affected.has(group.id) ? { ...group, status } : group));
|
||||
}
|
||||
invalidateDataKeys('modelCatalog', 'playgroundModels');
|
||||
setCoreState('ready');
|
||||
setCoreMessage(`已批量${identityBatchActionLabel(action)} ${response.affectedCount} 个用户组。`);
|
||||
} catch (err) {
|
||||
setCoreState('error');
|
||||
setCoreMessage(err instanceof Error ? err.message : '批量操作用户组失败');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
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)));
|
||||
setApiKeyAccessRules((current) => current.filter((rule) => !(rule.subjectType === 'api_key' && rule.subjectId === apiKeyId)));
|
||||
setApiKeySecretsById((current) => {
|
||||
const next = { ...current };
|
||||
delete next[apiKeyId];
|
||||
@@ -1034,7 +1092,7 @@ export function App() {
|
||||
setCoreMessage('');
|
||||
try {
|
||||
const item = ruleId ? await updateAccessRule(token, ruleId, input) : await createAccessRule(token, input);
|
||||
setAccessRules((current) => [item, ...current.filter((rule) => rule.id !== item.id)]);
|
||||
setAdminAccessRules((current) => [item, ...current.filter((rule) => rule.id !== item.id)]);
|
||||
invalidateDataKeys('playgroundModels', 'modelCatalog');
|
||||
setCoreState('ready');
|
||||
setCoreMessage(ruleId ? '访问权限规则已更新。' : '访问权限规则已创建。');
|
||||
@@ -1050,7 +1108,7 @@ export function App() {
|
||||
setCoreMessage('');
|
||||
try {
|
||||
await deleteAccessRule(token, ruleId);
|
||||
setAccessRules((current) => current.filter((rule) => rule.id !== ruleId));
|
||||
setAdminAccessRules((current) => current.filter((rule) => rule.id !== ruleId));
|
||||
invalidateDataKeys('playgroundModels', 'modelCatalog');
|
||||
setCoreState('ready');
|
||||
setCoreMessage('访问权限规则已删除。');
|
||||
@@ -1066,7 +1124,7 @@ export function App() {
|
||||
setCoreMessage('');
|
||||
try {
|
||||
const response = await batchAccessRules(token, input);
|
||||
setAccessRules(response.items);
|
||||
setAdminAccessRules(response.items);
|
||||
invalidateDataKeys('playgroundModels', 'modelCatalog');
|
||||
setCoreState('ready');
|
||||
setCoreMessage('访问权限已更新。');
|
||||
@@ -1192,7 +1250,7 @@ export function App() {
|
||||
setCoreMessage('');
|
||||
try {
|
||||
const response = await batchApiKeyAccessRules(token, input);
|
||||
setAccessRules(response.items);
|
||||
setApiKeyAccessRules(response.items);
|
||||
setCoreState('ready');
|
||||
setCoreMessage('API Key 权限已更新。');
|
||||
} catch (err) {
|
||||
@@ -1258,7 +1316,8 @@ export function App() {
|
||||
setPricingRuleSets([]);
|
||||
setRunnerPolicy(null);
|
||||
setRuntimePolicySets([]);
|
||||
setAccessRules([]);
|
||||
setAdminAccessRules([]);
|
||||
setApiKeyAccessRules([]);
|
||||
setAuditLogs([]);
|
||||
setRateLimitWindows([]);
|
||||
setModelRateLimits([]);
|
||||
@@ -1466,6 +1525,7 @@ export function App() {
|
||||
transactionQuery={workspaceTransactionQuery}
|
||||
transactionTotal={walletTransactionTotal}
|
||||
onBatchAccessRules={batchSaveAPIKeyAccessRules}
|
||||
onRefreshAccessRules={() => ensureData(['apiKeyAccessRules'], token, true)}
|
||||
onDeleteApiKey={removeAPIKey}
|
||||
onApiKeyFormChange={setApiKeyForm}
|
||||
onSectionChange={navigateWorkspaceSection}
|
||||
@@ -1517,6 +1577,8 @@ export function App() {
|
||||
onDeleteTenant={removeTenant}
|
||||
onDeleteUser={removeUser}
|
||||
onDeleteUserGroup={removeUserGroup}
|
||||
onBatchUsers={batchOperateUsers}
|
||||
onBatchUserGroups={batchOperateUserGroups}
|
||||
onSaveBaseModel={saveBaseModel}
|
||||
onResetAllBaseModels={resetAllBaseModelsToDefault}
|
||||
onResetBaseModel={resetBaseModelToDefault}
|
||||
@@ -1710,7 +1772,13 @@ function clampTransactionPageSize(value: number) {
|
||||
return Math.min(100, Math.max(1, normalized));
|
||||
}
|
||||
|
||||
function dataKeysForRoute(
|
||||
function identityBatchActionLabel(action: IdentityBatchAction) {
|
||||
if (action === 'enable') return '启用';
|
||||
if (action === 'disable') return '禁用';
|
||||
return '删除';
|
||||
}
|
||||
|
||||
export function dataKeysForRoute(
|
||||
activePage: PageKey,
|
||||
adminSection: AdminSection,
|
||||
workspaceSection: WorkspaceSection,
|
||||
@@ -1729,7 +1797,7 @@ function dataKeysForRoute(
|
||||
if (activePage === 'workspace') {
|
||||
if (workspaceSection === 'overview') return ['currentUser', 'currentUserGroups', 'apiKeys'];
|
||||
if (workspaceSection === 'billing') return ['wallet'];
|
||||
if (workspaceSection === 'apiKeys') return ['apiKeys', 'accessRules'];
|
||||
if (workspaceSection === 'apiKeys') return ['apiKeys', 'apiKeyAccessRules'];
|
||||
if (workspaceSection === 'tasks') return ['tasks'];
|
||||
if (workspaceSection === 'transactions') return ['wallet', 'walletTransactions'];
|
||||
return [];
|
||||
@@ -1738,7 +1806,7 @@ function dataKeysForRoute(
|
||||
if (activePage !== 'admin') return [];
|
||||
switch (adminSection) {
|
||||
case 'overview':
|
||||
return ['platforms', 'models', 'providers', 'pricingRules', 'runtimePolicySets', 'rateLimitWindows', 'modelRateLimits', 'tenants', 'users', 'userGroups', 'accessRules'];
|
||||
return ['platforms', 'models', 'providers', 'pricingRules', 'runtimePolicySets', 'rateLimitWindows', 'modelRateLimits', 'tenants', 'users', 'userGroups', 'adminAccessRules'];
|
||||
case 'globalModels':
|
||||
return ['providers'];
|
||||
case 'pricing':
|
||||
@@ -1760,7 +1828,7 @@ function dataKeysForRoute(
|
||||
case 'users':
|
||||
return ['users', 'tenants', 'userGroups'];
|
||||
case 'userGroups':
|
||||
return ['userGroups', 'accessRules', 'platforms', 'models'];
|
||||
return ['userGroups', 'adminAccessRules', 'platforms', 'models'];
|
||||
case 'auditLogs':
|
||||
return ['auditLogs'];
|
||||
case 'systemSettings':
|
||||
|
||||
Reference in New Issue
Block a user