Files
coze-studio/frontend/packages/arch/bot-store/src/auth/index.tsx

468 lines
14 KiB
TypeScript

/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @coze-arch/max-line-per-function */
import { devtools } from 'zustand/middleware';
import { create } from 'zustand';
import { uniqBy, isObject } from 'lodash-es';
import { reporter } from '@coze-arch/logger';
import { CustomError } from '@coze-arch/bot-error';
import { type Creator } from '@coze-arch/bot-api/playground_api';
import {
type ResourceIdentifier,
ResourceType,
PrincipalType,
} from '@coze-arch/bot-api/permission_authz';
import { type CollaboratorType } from '@coze-arch/bot-api/pat_permission_api';
import {
PlaygroundApi,
patPermissionApi,
workflowApi,
intelligenceApi,
type BotAPIRequestConfig,
} from '@coze-arch/bot-api';
interface AuthStoreState {
/* Two layer map
{
Resource Type: {
Resource ID: Collaborator
}
}
*/
collaboratorsMap: Record<ResourceType, Record<string, Creator[]>>;
}
interface AuthStoreAction {
getCachedCollaborators: (resource: ResourceIdentifier) => Creator[];
fetchCollaborators: (params: {
spaceId: string;
resource: ResourceIdentifier;
}) => Promise<Creator[]>;
removeCollaborators: (
resource: ResourceIdentifier,
userId: string,
options?: BotAPIRequestConfig,
) => Promise<void>;
batchRemoveCollaborators: (
resource: ResourceIdentifier,
userIds: string[],
options?: BotAPIRequestConfig,
) => Promise<[string[], string[]]>;
addCollaborator: (params: {
resource: ResourceIdentifier;
user: Creator;
options?: BotAPIRequestConfig;
roles?: Array<CollaboratorType>;
}) => Promise<void>;
editCollaborator: (params: {
resource: ResourceIdentifier;
user: Creator;
options?: BotAPIRequestConfig;
roles?: Array<CollaboratorType>;
}) => Promise<void>;
batchAddCollaborators: (params: {
resource: ResourceIdentifier;
users: Creator[];
options?: BotAPIRequestConfig;
// The third argument is the error code
roles?: Array<CollaboratorType>;
}) => Promise<[Creator[], Creator[], number]>;
// New batch addition interface for permission service
batchAddCollaboratorsServer: (params: {
resource: ResourceIdentifier;
users: Creator[];
options?: BotAPIRequestConfig;
// The third argument is the error code
roles?: Array<CollaboratorType>;
}) => Promise<boolean>;
}
const defaultState: AuthStoreState = {
collaboratorsMap: Object.values(ResourceType).reduce(
(r, val) => ({ ...r, [val]: {} }),
{},
) as AuthStoreState['collaboratorsMap'],
};
export const useAuthStore = create<AuthStoreState & AuthStoreAction>()(
// eslint-disable-next-line @coze-arch/zustand/devtools-config, max-lines-per-function
devtools((set, get) => ({
...defaultState,
getCachedCollaborators: resource =>
get().collaboratorsMap[resource.type][resource.id],
//
fetchCollaborators: async ({ spaceId, resource }) => {
switch (resource.type) {
case ResourceType.Bot: {
const {
data: { creator, collaboration_list, collaborator_roles },
} = await PlaygroundApi.DraftBotCollaboration({
space_id: spaceId,
bot_id: resource.id,
});
const res: Creator[] = [
creator as Creator,
...(collaboration_list
? collaboration_list.map(item => ({
...item,
roles: collaborator_roles?.[item.id as string] ?? undefined,
}))
: []),
];
set(({ collaboratorsMap }) => ({
collaboratorsMap: {
...collaboratorsMap,
[resource.type]: {
...collaboratorsMap[resource.type],
[resource.id]: res,
},
},
}));
return res;
}
case ResourceType.Workflow: {
const result = await workflowApi.ListCollaborators({
space_id: spaceId,
workflow_id: resource.id,
});
const data = result.data as { owner: boolean; user: Creator }[];
const creator = (data ?? []).find(it => it.owner === true)?.user;
const collaborationList = (data ?? [])
.filter(it => it?.user?.id !== creator?.id)
.map(item => item.user);
const res: Creator[] = [
// @ts-expect-error -- linter-disable-autofix
creator,
...(collaborationList ? collaborationList : []),
];
set(({ collaboratorsMap }) => ({
collaboratorsMap: {
...collaboratorsMap,
[resource.type]: {
...collaboratorsMap[resource.type],
[resource.id]: res,
},
},
}));
return res;
}
case ResourceType.Project: {
const result = await intelligenceApi.ListIntelligenceCollaboration({
intelligence_id: resource.id,
intelligence_type: 2, // 1-Bot, 2-Project
});
const creator = result.data.owner_info;
const collaborators =
result.data.collaborator_info?.filter(
user => user.user_id !== creator?.user_id,
) ?? [];
const res: Creator[] = [creator, ...collaborators]
.filter(user => !!user)
.map(user => ({
id: user?.user_id,
name: user?.nickname,
avatar_url: user?.avatar_url,
user_name: user?.user_unique_name,
user_label: user?.user_label,
}));
set(({ collaboratorsMap }) => ({
collaboratorsMap: {
...collaboratorsMap,
[resource.type]: {
...collaboratorsMap[resource.type],
[resource.id]: res,
},
},
}));
return [];
}
default:
throw new CustomError(
'',
'unhandled resource type calling fetchCollaborators',
);
}
},
removeCollaborators: async (resource, userId, options) => {
await patPermissionApi.RemoveCollaborator(
{
resource,
principal: {
id: userId,
type: PrincipalType.User,
},
},
options,
);
set(({ collaboratorsMap, getCachedCollaborators }) => ({
collaboratorsMap: {
...collaboratorsMap,
[resource.type]: {
...collaboratorsMap[resource.type],
[resource.id]: getCachedCollaborators(resource).filter(
c => c.id !== userId,
),
},
},
}));
},
// Temporarily batch processed by the front end
batchRemoveCollaborators: async (resource, userIds, options) => {
const resultArr = await Promise.all(
userIds.map(
userId =>
new Promise<boolean>(r => {
patPermissionApi
.RemoveCollaborator(
{
resource,
principal: {
id: userId,
type: PrincipalType.User,
},
},
options,
)
.then(() => {
r(true);
})
.catch(() => {
r(false);
});
}),
),
);
const [removedUserIds, failedUserIds] = resultArr.reduce<
[string[], string[]]
>(
([r, f], success, index) => {
const currentId = userIds[index];
return success ? [[...r, currentId], f] : [r, [...f, currentId]];
},
[[], []],
);
set(({ collaboratorsMap, getCachedCollaborators }) => ({
collaboratorsMap: {
...collaboratorsMap,
[resource.type]: {
...collaboratorsMap[resource.type],
[resource.id]: getCachedCollaborators(resource).filter(
c => !removedUserIds.includes(c.id ?? ''),
),
},
},
}));
return [removedUserIds, failedUserIds];
},
addCollaborator: async ({ resource, user, options, roles }) => {
await patPermissionApi.AddCollaborator(
{
resource,
principal: {
id: user.id ?? '',
type: PrincipalType.User,
},
collaborator_types: roles,
},
options,
);
set(({ collaboratorsMap, getCachedCollaborators }) => ({
collaboratorsMap: {
...collaboratorsMap,
[resource.type]: {
...collaboratorsMap[resource.type],
[resource.id]: uniqBy(
[
...getCachedCollaborators(resource),
{
...user,
roles,
},
],
'id',
),
},
},
}));
},
batchAddCollaborators: async ({ resource, users, options, roles }) => {
const resultArr = await Promise.all(
users.map(
user =>
new Promise<{ result: true } | { result: false; error: unknown }>(
r => {
patPermissionApi
.AddCollaborator(
{
resource,
principal: {
id: user.id ?? '',
type: PrincipalType.User,
},
collaborator_types: roles,
},
options,
)
.then(() => {
r({ result: true });
})
.catch(error => {
reporter.error({
namespace: 'collaborator',
error,
message: 'batchAddCollaborators error',
meta: {
resource,
principal: {
id: user.id ?? '',
type: PrincipalType.User,
},
},
});
r({ result: false, error });
});
},
),
),
);
// Current batch implementations need to sort the code of individual added interfaces to get the highest priority message to reveal
let errorCode = 0;
const [addedUsers, failedUsers] = resultArr.reduce<
[Creator[], Creator[]]
>(
([r, f], finish, index) => {
const user = users[index];
// This is written so that ts can derive the correct type. ts@5.0.4
if (finish.result === true) {
return [[...r, user], f];
}
if (isObject(finish.error)) {
const error = finish.error as {
code: number | string;
message?: string;
msg?: string;
};
// Comparison code
if (Number(error.code) > errorCode) {
errorCode = Number(error.code);
}
}
// Error, you need to compare the code and then copy the message
return [r, [...f, user]];
},
[[], []],
);
set(({ collaboratorsMap, getCachedCollaborators }) => ({
collaboratorsMap: {
...collaboratorsMap,
[resource.type]: {
...collaboratorsMap[resource.type],
[resource.id]: uniqBy(
[
...getCachedCollaborators(resource),
...addedUsers.map(item => ({
...item,
roles,
})),
],
'id',
),
},
},
}));
return [addedUsers, failedUsers, errorCode];
},
batchAddCollaboratorsServer: async ({
resource,
users,
options,
roles,
}) => {
const { code } = await patPermissionApi.BatchAddCollaborator(
{
principal_type: 1,
resource,
principal_ids: users.map(user => user.id).filter(Boolean) as string[],
},
options,
);
if (code === 0) {
set(({ collaboratorsMap, getCachedCollaborators }) => ({
collaboratorsMap: {
...collaboratorsMap,
[resource.type]: {
...collaboratorsMap[resource.type],
[resource.id]: uniqBy(
[
...getCachedCollaborators(resource),
...users.map(item => ({
...item,
roles,
})),
],
'id',
),
},
},
}));
}
return code === 0;
},
editCollaborator: async ({ resource, user, options, roles }) => {
await patPermissionApi.ModifyCollaborator(
{
resource,
principal: {
id: user.id ?? '',
type: PrincipalType.User,
},
collaborator_types: roles,
},
options,
);
set(({ collaboratorsMap, getCachedCollaborators }) => ({
collaboratorsMap: {
...collaboratorsMap,
[resource.type]: {
...collaboratorsMap[resource.type],
[resource.id]: uniqBy(
[
...getCachedCollaborators(resource).map(item => {
if (item.id === user.id) {
return {
...item,
roles,
};
}
return item;
}),
],
'id',
),
},
},
}));
},
})),
);