mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
refactor(server): permission (#10449)
This commit is contained in:
@@ -12,3 +12,8 @@ export interface Doc {
|
||||
}
|
||||
|
||||
export type DocEditor = Pick<User, 'id' | 'name' | 'avatarUrl'>;
|
||||
|
||||
export enum PublicDocMode {
|
||||
Page,
|
||||
Edgeless,
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from './doc';
|
||||
export * from './feature';
|
||||
export * from './page';
|
||||
export * from './role';
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export enum PublicPageMode {
|
||||
Page,
|
||||
Edgeless,
|
||||
}
|
||||
14
packages/backend/server/src/models/common/role.ts
Normal file
14
packages/backend/server/src/models/common/role.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export enum DocRole {
|
||||
External = 0,
|
||||
Reader = 10,
|
||||
Editor = 20,
|
||||
Manager = 30,
|
||||
Owner = 99,
|
||||
}
|
||||
|
||||
export enum WorkspaceRole {
|
||||
External = -99,
|
||||
Collaborator = 1,
|
||||
Admin = 10,
|
||||
Owner = 99,
|
||||
}
|
||||
203
packages/backend/server/src/models/doc-user.ts
Normal file
203
packages/backend/server/src/models/doc-user.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import assert from 'node:assert';
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Transactional } from '@nestjs-cls/transactional';
|
||||
import { WorkspaceDocUserRole } from '@prisma/client';
|
||||
|
||||
import { CanNotBatchGrantDocOwnerPermissions, PaginationInput } from '../base';
|
||||
import { BaseModel } from './base';
|
||||
import { DocRole } from './common';
|
||||
|
||||
@Injectable()
|
||||
export class DocUserModel extends BaseModel {
|
||||
/**
|
||||
* Set or update the [Owner] of a doc.
|
||||
* The old [Owner] will be changed to [Manager] if there is already an [Owner].
|
||||
*/
|
||||
@Transactional()
|
||||
async setOwner(workspaceId: string, docId: string, userId: string) {
|
||||
const oldOwner = await this.db.workspaceDocUserRole.findFirst({
|
||||
where: {
|
||||
workspaceId,
|
||||
docId,
|
||||
type: DocRole.Owner,
|
||||
},
|
||||
});
|
||||
|
||||
if (oldOwner) {
|
||||
await this.db.workspaceDocUserRole.update({
|
||||
where: {
|
||||
workspaceId_docId_userId: {
|
||||
workspaceId,
|
||||
docId,
|
||||
userId: oldOwner.userId,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
type: DocRole.Manager,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await this.db.workspaceDocUserRole.upsert({
|
||||
where: {
|
||||
workspaceId_docId_userId: {
|
||||
workspaceId,
|
||||
docId,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
type: DocRole.Owner,
|
||||
},
|
||||
create: {
|
||||
workspaceId,
|
||||
docId,
|
||||
userId,
|
||||
type: DocRole.Owner,
|
||||
},
|
||||
});
|
||||
|
||||
if (oldOwner) {
|
||||
this.logger.log(
|
||||
`Transfer doc owner of [${workspaceId}/${docId}] from [${oldOwner.userId}] to [${userId}]`
|
||||
);
|
||||
} else {
|
||||
this.logger.log(
|
||||
`Set doc owner of [${workspaceId}/${docId}] to [${userId}]`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set or update the Role of a user in a doc.
|
||||
*
|
||||
* NOTE: do not use this method to set the [Owner] of a doc. Use {@link setOwner} instead.
|
||||
*/
|
||||
@Transactional()
|
||||
async set(workspaceId: string, docId: string, userId: string, role: DocRole) {
|
||||
// internal misuse, throw directly
|
||||
assert(role !== DocRole.Owner, 'Cannot set Owner role of a doc to a user.');
|
||||
|
||||
const oldRole = await this.get(workspaceId, docId, userId);
|
||||
|
||||
if (oldRole && oldRole.type === role) {
|
||||
return oldRole;
|
||||
}
|
||||
|
||||
const newRole = await this.db.workspaceDocUserRole.upsert({
|
||||
where: {
|
||||
workspaceId_docId_userId: {
|
||||
workspaceId,
|
||||
docId,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
type: role,
|
||||
},
|
||||
create: {
|
||||
workspaceId,
|
||||
docId,
|
||||
userId,
|
||||
type: role,
|
||||
},
|
||||
});
|
||||
|
||||
return newRole;
|
||||
}
|
||||
|
||||
async batchSetUserRoles(
|
||||
workspaceId: string,
|
||||
docId: string,
|
||||
userIds: string[],
|
||||
role: DocRole
|
||||
) {
|
||||
if (userIds.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (role === DocRole.Owner) {
|
||||
throw new CanNotBatchGrantDocOwnerPermissions();
|
||||
}
|
||||
|
||||
const result = await this.db.workspaceDocUserRole.createMany({
|
||||
skipDuplicates: true,
|
||||
data: userIds.map(userId => ({
|
||||
workspaceId,
|
||||
docId,
|
||||
userId,
|
||||
type: role,
|
||||
})),
|
||||
});
|
||||
|
||||
return result.count;
|
||||
}
|
||||
|
||||
async delete(workspaceId: string, docId: string, userId: string) {
|
||||
await this.db.workspaceDocUserRole.deleteMany({
|
||||
where: {
|
||||
workspaceId,
|
||||
docId,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getOwner(workspaceId: string, docId: string) {
|
||||
return await this.db.workspaceDocUserRole.findFirst({
|
||||
where: {
|
||||
workspaceId,
|
||||
docId,
|
||||
type: DocRole.Owner,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async get(workspaceId: string, docId: string, userId: string) {
|
||||
return await this.db.workspaceDocUserRole.findUnique({
|
||||
where: {
|
||||
workspaceId_docId_userId: {
|
||||
workspaceId,
|
||||
docId,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
count(workspaceId: string, docId: string) {
|
||||
return this.db.workspaceDocUserRole.count({
|
||||
where: {
|
||||
workspaceId,
|
||||
docId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async paginate(
|
||||
workspaceId: string,
|
||||
docId: string,
|
||||
pagination: PaginationInput
|
||||
): Promise<[WorkspaceDocUserRole[], number]> {
|
||||
return await Promise.all([
|
||||
this.db.workspaceDocUserRole.findMany({
|
||||
where: {
|
||||
workspaceId,
|
||||
docId,
|
||||
createdAt: pagination.after
|
||||
? {
|
||||
gte: pagination.after,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'asc',
|
||||
},
|
||||
take: pagination.first,
|
||||
skip: pagination.offset + (pagination.after ? 1 : 0),
|
||||
}),
|
||||
this.count(workspaceId, docId),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { ModuleRef } from '@nestjs/core';
|
||||
|
||||
import { ApplyType } from '../base';
|
||||
import { DocModel } from './doc';
|
||||
import { DocUserModel } from './doc-user';
|
||||
import { FeatureModel } from './feature';
|
||||
import { PageModel } from './page';
|
||||
import { MODELS_SYMBOL } from './provider';
|
||||
@@ -18,6 +19,7 @@ import { UserFeatureModel } from './user-feature';
|
||||
import { VerificationTokenModel } from './verification-token';
|
||||
import { WorkspaceModel } from './workspace';
|
||||
import { WorkspaceFeatureModel } from './workspace-feature';
|
||||
import { WorkspaceUserModel } from './workspace-user';
|
||||
|
||||
const MODELS = {
|
||||
user: UserModel,
|
||||
@@ -30,6 +32,8 @@ const MODELS = {
|
||||
workspaceFeature: WorkspaceFeatureModel,
|
||||
doc: DocModel,
|
||||
userDoc: UserDocModel,
|
||||
workspaceUser: WorkspaceUserModel,
|
||||
docUser: DocUserModel,
|
||||
};
|
||||
|
||||
type ModelsType = {
|
||||
@@ -83,6 +87,7 @@ export class ModelsModule {}
|
||||
|
||||
export * from './common';
|
||||
export * from './doc';
|
||||
export * from './doc-user';
|
||||
export * from './feature';
|
||||
export * from './page';
|
||||
export * from './session';
|
||||
@@ -92,3 +97,4 @@ export * from './user-feature';
|
||||
export * from './verification-token';
|
||||
export * from './workspace';
|
||||
export * from './workspace-feature';
|
||||
export * from './workspace-user';
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Transactional } from '@nestjs-cls/transactional';
|
||||
import {
|
||||
type WorkspaceDoc as Page,
|
||||
type WorkspaceDocUserPermission as PageUserPermission,
|
||||
} from '@prisma/client';
|
||||
import { type WorkspaceDoc as Page } from '@prisma/client';
|
||||
|
||||
import { WorkspaceRole } from '../core/permission';
|
||||
import { BaseModel } from './base';
|
||||
import { PublicPageMode } from './common';
|
||||
import { PublicDocMode } from './common';
|
||||
export type { Page };
|
||||
export type UpdatePageInput = {
|
||||
mode?: PublicPageMode;
|
||||
mode?: PublicDocMode;
|
||||
public?: boolean;
|
||||
};
|
||||
|
||||
@@ -82,118 +77,4 @@ export class PageModel extends BaseModel {
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region page member and permission
|
||||
|
||||
/**
|
||||
* Grant the page member with the given permission.
|
||||
*/
|
||||
@Transactional()
|
||||
async grantMember(
|
||||
workspaceId: string,
|
||||
docId: string,
|
||||
userId: string,
|
||||
permission: WorkspaceRole = WorkspaceRole.Collaborator
|
||||
): Promise<PageUserPermission> {
|
||||
let data = await this.db.workspaceDocUserPermission.findUnique({
|
||||
where: {
|
||||
workspaceId_docId_userId: {
|
||||
workspaceId,
|
||||
docId,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// If the user is already accepted and the new permission is owner, we need to revoke old owner
|
||||
if (!data || data.type !== permission) {
|
||||
if (data) {
|
||||
// Update the permission
|
||||
data = await this.db.workspaceDocUserPermission.update({
|
||||
where: {
|
||||
workspaceId_docId_userId: {
|
||||
workspaceId,
|
||||
docId,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
data: { type: permission },
|
||||
});
|
||||
} else {
|
||||
// Create a new permission
|
||||
data = await this.db.workspaceDocUserPermission.create({
|
||||
data: {
|
||||
workspaceId,
|
||||
docId,
|
||||
userId,
|
||||
type: permission,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// If the new permission is owner, we need to revoke old owner
|
||||
if (permission === WorkspaceRole.Owner) {
|
||||
await this.db.workspaceDocUserPermission.updateMany({
|
||||
where: {
|
||||
workspaceId,
|
||||
docId,
|
||||
type: WorkspaceRole.Owner,
|
||||
userId: { not: userId },
|
||||
},
|
||||
data: { type: WorkspaceRole.Admin },
|
||||
});
|
||||
this.logger.log(
|
||||
`Change owner of workspace ${workspaceId} doc ${docId} to user ${userId}`
|
||||
);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// nothing to do
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a given user is a member of a workspace and has the given or higher permission.
|
||||
* Default to read permission.
|
||||
*/
|
||||
async isMember(
|
||||
workspaceId: string,
|
||||
docId: string,
|
||||
userId: string,
|
||||
permission: WorkspaceRole = WorkspaceRole.Collaborator
|
||||
) {
|
||||
const count = await this.db.workspaceDocUserPermission.count({
|
||||
where: {
|
||||
workspaceId,
|
||||
docId,
|
||||
userId,
|
||||
type: {
|
||||
gte: permission,
|
||||
},
|
||||
},
|
||||
});
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a page member
|
||||
* Except the owner, the owner can't be deleted.
|
||||
*/
|
||||
async deleteMember(workspaceId: string, docId: string, userId: string) {
|
||||
const { count } = await this.db.workspaceDocUserPermission.deleteMany({
|
||||
where: {
|
||||
workspaceId,
|
||||
docId,
|
||||
userId,
|
||||
type: {
|
||||
// We shouldn't revoke owner permission, should auto deleted by workspace/user delete cascading
|
||||
not: WorkspaceRole.Owner,
|
||||
},
|
||||
},
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
// #endregion
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
WrongSignInMethod,
|
||||
} from '../base';
|
||||
import { BaseModel } from './base';
|
||||
import { WorkspaceRole } from './common';
|
||||
import type { Workspace } from './workspace';
|
||||
|
||||
const publicUserSelect = {
|
||||
@@ -215,12 +216,17 @@ export class UserModel extends BaseModel {
|
||||
}
|
||||
|
||||
async delete(id: string) {
|
||||
const ownedWorkspaceIds = await this.models.workspace.findOwnedIds(id);
|
||||
const ownedWorkspaces = await this.models.workspaceUser.getUserActiveRoles(
|
||||
id,
|
||||
{
|
||||
role: WorkspaceRole.Owner,
|
||||
}
|
||||
);
|
||||
const user = await this.db.user.delete({ where: { id } });
|
||||
|
||||
this.event.emit('user.deleted', {
|
||||
...user,
|
||||
ownedWorkspaces: ownedWorkspaceIds,
|
||||
ownedWorkspaces: ownedWorkspaces.map(r => r.workspaceId),
|
||||
});
|
||||
|
||||
return user;
|
||||
|
||||
385
packages/backend/server/src/models/workspace-user.ts
Normal file
385
packages/backend/server/src/models/workspace-user.ts
Normal file
@@ -0,0 +1,385 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Transactional } from '@nestjs-cls/transactional';
|
||||
import { WorkspaceMemberStatus } from '@prisma/client';
|
||||
import { groupBy } from 'lodash-es';
|
||||
|
||||
import { EventBus, PaginationInput } from '../base';
|
||||
import { BaseModel } from './base';
|
||||
import { WorkspaceRole } from './common';
|
||||
|
||||
export { WorkspaceMemberStatus };
|
||||
|
||||
declare global {
|
||||
interface Events {
|
||||
'workspace.owner.changed': {
|
||||
workspaceId: string;
|
||||
from: string;
|
||||
to: string;
|
||||
};
|
||||
'workspace.members.roleChanged': {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
role: WorkspaceRole;
|
||||
};
|
||||
// below are business events, should be declare somewhere else
|
||||
'workspace.members.updated': {
|
||||
workspaceId: string;
|
||||
count: number;
|
||||
};
|
||||
'workspace.members.reviewRequested': {
|
||||
inviteId: string;
|
||||
};
|
||||
'workspace.members.requestApproved': {
|
||||
inviteId: string;
|
||||
};
|
||||
'workspace.members.requestDeclined': {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
'workspace.members.removed': {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
'workspace.members.leave': {
|
||||
workspaceId: string;
|
||||
user: {
|
||||
id: string;
|
||||
email: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceUserModel extends BaseModel {
|
||||
constructor(private readonly event: EventBus) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set or update the [Owner] of a workspace.
|
||||
* The old [Owner] will be changed to [Admin] if there is already an [Owner].
|
||||
*/
|
||||
@Transactional()
|
||||
async setOwner(workspaceId: string, userId: string) {
|
||||
const oldOwner = await this.db.workspaceUserRole.findFirst({
|
||||
where: {
|
||||
workspaceId,
|
||||
type: WorkspaceRole.Owner,
|
||||
},
|
||||
});
|
||||
|
||||
// If there is already an owner, we need to change the old owner to admin
|
||||
if (oldOwner) {
|
||||
await this.db.workspaceUserRole.update({
|
||||
where: {
|
||||
id: oldOwner.id,
|
||||
},
|
||||
data: {
|
||||
type: WorkspaceRole.Admin,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await this.db.workspaceUserRole.upsert({
|
||||
where: {
|
||||
workspaceId_userId: {
|
||||
workspaceId,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
type: WorkspaceRole.Owner,
|
||||
},
|
||||
create: {
|
||||
workspaceId,
|
||||
userId,
|
||||
type: WorkspaceRole.Owner,
|
||||
status: WorkspaceMemberStatus.Accepted,
|
||||
},
|
||||
});
|
||||
|
||||
if (oldOwner) {
|
||||
this.event.emit('workspace.owner.changed', {
|
||||
workspaceId,
|
||||
from: oldOwner.userId,
|
||||
to: userId,
|
||||
});
|
||||
this.logger.log(
|
||||
`Transfer workspace owner of [${workspaceId}] from [${oldOwner.userId}] to [${userId}]`
|
||||
);
|
||||
} else {
|
||||
this.logger.log(`Set workspace owner of [${workspaceId}] to [${userId}]`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set or update the Role of a user in a workspace.
|
||||
*
|
||||
* NOTE: do not use this method to set the [Owner] of a workspace. Use {@link setOwner} instead.
|
||||
*/
|
||||
@Transactional()
|
||||
async set(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
role: WorkspaceRole,
|
||||
defaultStatus: WorkspaceMemberStatus = WorkspaceMemberStatus.Pending
|
||||
) {
|
||||
if (role === WorkspaceRole.Owner) {
|
||||
throw new Error('Cannot grant Owner role of a workspace to a user.');
|
||||
}
|
||||
|
||||
const oldRole = await this.get(workspaceId, userId);
|
||||
|
||||
if (oldRole) {
|
||||
if (oldRole.type === role) {
|
||||
return oldRole;
|
||||
}
|
||||
|
||||
const newRole = await this.db.workspaceUserRole.update({
|
||||
where: { id: oldRole.id },
|
||||
data: { type: role },
|
||||
});
|
||||
|
||||
if (oldRole.status === WorkspaceMemberStatus.Accepted) {
|
||||
this.event.emit('workspace.members.roleChanged', {
|
||||
userId,
|
||||
workspaceId,
|
||||
role: newRole.type,
|
||||
});
|
||||
}
|
||||
|
||||
return newRole;
|
||||
} else {
|
||||
return await this.db.workspaceUserRole.create({
|
||||
data: {
|
||||
workspaceId,
|
||||
userId,
|
||||
type: role,
|
||||
status: defaultStatus,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async setStatus(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
status: WorkspaceMemberStatus
|
||||
) {
|
||||
return await this.db.workspaceUserRole.update({
|
||||
where: {
|
||||
workspaceId_userId: {
|
||||
workspaceId,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
status,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async accept(id: string) {
|
||||
await this.db.workspaceUserRole.update({
|
||||
where: { id },
|
||||
data: { status: WorkspaceMemberStatus.Accepted },
|
||||
});
|
||||
}
|
||||
|
||||
async delete(workspaceId: string, userId: string) {
|
||||
await this.db.workspaceUserRole.deleteMany({
|
||||
where: {
|
||||
workspaceId,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async get(workspaceId: string, userId: string) {
|
||||
return await this.db.workspaceUserRole.findUnique({
|
||||
where: {
|
||||
workspaceId_userId: {
|
||||
workspaceId,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getById(id: string) {
|
||||
return await this.db.workspaceUserRole.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the **accepted** Role of a user in a workspace.
|
||||
*/
|
||||
async getActive(workspaceId: string, userId: string) {
|
||||
return await this.db.workspaceUserRole.findUnique({
|
||||
where: {
|
||||
workspaceId_userId: { workspaceId, userId },
|
||||
status: WorkspaceMemberStatus.Accepted,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getOwner(workspaceId: string) {
|
||||
const role = await this.db.workspaceUserRole.findFirst({
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
where: {
|
||||
workspaceId,
|
||||
type: WorkspaceRole.Owner,
|
||||
},
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
throw new Error('Workspace owner not found');
|
||||
}
|
||||
|
||||
return role.user;
|
||||
}
|
||||
|
||||
async getAdmins(workspaceId: string) {
|
||||
const list = await this.db.workspaceUserRole.findMany({
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
where: {
|
||||
workspaceId,
|
||||
type: WorkspaceRole.Admin,
|
||||
status: WorkspaceMemberStatus.Accepted,
|
||||
},
|
||||
});
|
||||
|
||||
return list.map(l => l.user);
|
||||
}
|
||||
|
||||
async count(workspaceId: string) {
|
||||
return this.db.workspaceUserRole.count({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getUserActiveRoles(
|
||||
userId: string,
|
||||
filter: { role?: WorkspaceRole } = {}
|
||||
) {
|
||||
return await this.db.workspaceUserRole.findMany({
|
||||
where: {
|
||||
userId,
|
||||
status: WorkspaceMemberStatus.Accepted,
|
||||
type: filter.role,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async paginate(workspaceId: string, pagination: PaginationInput) {
|
||||
return await Promise.all([
|
||||
this.db.workspaceUserRole.findMany({
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
where: {
|
||||
workspaceId,
|
||||
createdAt: pagination.after
|
||||
? {
|
||||
gte: pagination.after,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'asc',
|
||||
},
|
||||
take: pagination.first,
|
||||
skip: pagination.offset + (pagination.after ? 1 : 0),
|
||||
}),
|
||||
this.count(workspaceId),
|
||||
]);
|
||||
}
|
||||
|
||||
async search(
|
||||
workspaceId: string,
|
||||
query: string,
|
||||
pagination: PaginationInput
|
||||
) {
|
||||
return await this.db.workspaceUserRole.findMany({
|
||||
include: { user: true },
|
||||
where: {
|
||||
workspaceId,
|
||||
status: WorkspaceMemberStatus.Accepted,
|
||||
user: {
|
||||
OR: [
|
||||
{
|
||||
email: {
|
||||
contains: query,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: {
|
||||
contains: query,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
take: pagination.first,
|
||||
skip: pagination.offset + (pagination.after ? 1 : 0),
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
async refresh(workspaceId: string, memberLimit: number) {
|
||||
const usedCount = await this.db.workspaceUserRole.count({
|
||||
where: { workspaceId, status: WorkspaceMemberStatus.Accepted },
|
||||
});
|
||||
|
||||
const availableCount = memberLimit - usedCount;
|
||||
|
||||
if (availableCount <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const members = await this.db.workspaceUserRole.findMany({
|
||||
select: { id: true, status: true },
|
||||
where: {
|
||||
workspaceId,
|
||||
status: {
|
||||
in: [
|
||||
WorkspaceMemberStatus.NeedMoreSeat,
|
||||
WorkspaceMemberStatus.NeedMoreSeatAndReview,
|
||||
],
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
|
||||
const needChange = members.slice(0, availableCount);
|
||||
const { NeedMoreSeat, NeedMoreSeatAndReview } = groupBy(
|
||||
needChange,
|
||||
m => m.status
|
||||
);
|
||||
|
||||
const toPendings = NeedMoreSeat ?? [];
|
||||
if (toPendings.length > 0) {
|
||||
await this.db.workspaceUserRole.updateMany({
|
||||
where: { id: { in: toPendings.map(m => m.id) } },
|
||||
data: { status: WorkspaceMemberStatus.Pending },
|
||||
});
|
||||
}
|
||||
|
||||
const toUnderReviewUserIds = NeedMoreSeatAndReview ?? [];
|
||||
if (toUnderReviewUserIds.length > 0) {
|
||||
await this.db.workspaceUserRole.updateMany({
|
||||
where: { id: { in: toUnderReviewUserIds.map(m => m.id) } },
|
||||
data: { status: WorkspaceMemberStatus.UnderReview },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,78 +1,25 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Transactional } from '@nestjs-cls/transactional';
|
||||
import {
|
||||
type Workspace,
|
||||
WorkspaceMemberStatus,
|
||||
type WorkspaceUserPermission,
|
||||
} from '@prisma/client';
|
||||
import { groupBy } from 'lodash-es';
|
||||
import { type Workspace } from '@prisma/client';
|
||||
|
||||
import { EventBus } from '../base';
|
||||
import { WorkspaceRole } from '../core/permission';
|
||||
import { DocIsNotPublic, EventBus } from '../base';
|
||||
import { BaseModel } from './base';
|
||||
import { DocRole, PublicDocMode } from './common';
|
||||
|
||||
declare global {
|
||||
interface Events {
|
||||
'workspace.members.reviewRequested': { inviteId: string };
|
||||
'workspace.members.requestDeclined': {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
'workspace.members.requestApproved': { inviteId: string };
|
||||
'workspace.members.roleChanged': {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
permission: number;
|
||||
};
|
||||
'workspace.members.ownershipTransferred': {
|
||||
from: string;
|
||||
to: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
'workspace.members.updated': {
|
||||
workspaceId: string;
|
||||
count: number;
|
||||
};
|
||||
'workspace.members.leave': {
|
||||
user: {
|
||||
id: string;
|
||||
email: string;
|
||||
};
|
||||
workspaceId: string;
|
||||
};
|
||||
'workspace.members.removed': {
|
||||
workspaceId: string;
|
||||
userId: string;
|
||||
};
|
||||
'workspace.deleted': {
|
||||
id: string;
|
||||
};
|
||||
'workspace.blob.delete': {
|
||||
workspaceId: string;
|
||||
key: string;
|
||||
};
|
||||
'workspace.blob.sync': {
|
||||
workspaceId: string;
|
||||
key: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export { WorkspaceMemberStatus };
|
||||
export type { Workspace };
|
||||
export type UpdateWorkspaceInput = Pick<
|
||||
Partial<Workspace>,
|
||||
'public' | 'enableAi' | 'enableUrlPreview'
|
||||
>;
|
||||
|
||||
export interface FindWorkspaceMembersOptions {
|
||||
skip?: number;
|
||||
/**
|
||||
* Default to `8`
|
||||
*/
|
||||
take?: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceModel extends BaseModel {
|
||||
constructor(private readonly event: EventBus) {
|
||||
@@ -80,25 +27,16 @@ export class WorkspaceModel extends BaseModel {
|
||||
}
|
||||
|
||||
// #region workspace
|
||||
|
||||
/**
|
||||
* Create a new workspace for the user, default to private.
|
||||
*/
|
||||
@Transactional()
|
||||
async create(userId: string) {
|
||||
const workspace = await this.db.workspace.create({
|
||||
data: {
|
||||
public: false,
|
||||
permissions: {
|
||||
create: {
|
||||
type: WorkspaceRole.Owner,
|
||||
userId: userId,
|
||||
accepted: true,
|
||||
status: WorkspaceMemberStatus.Accepted,
|
||||
},
|
||||
},
|
||||
},
|
||||
data: { public: false },
|
||||
});
|
||||
this.logger.log(`Created workspace ${workspace.id} for user ${userId}`);
|
||||
this.logger.log(`Workspace created with id ${workspace.id}`);
|
||||
await this.models.workspaceUser.setOwner(workspace.id, userId);
|
||||
return workspace;
|
||||
}
|
||||
|
||||
@@ -106,7 +44,7 @@ export class WorkspaceModel extends BaseModel {
|
||||
* Update the workspace with the given data.
|
||||
*/
|
||||
async update(workspaceId: string, data: UpdateWorkspaceInput) {
|
||||
await this.db.workspace.update({
|
||||
const workspace = await this.db.workspace.update({
|
||||
where: {
|
||||
id: workspaceId,
|
||||
},
|
||||
@@ -115,6 +53,7 @@ export class WorkspaceModel extends BaseModel {
|
||||
this.logger.log(
|
||||
`Updated workspace ${workspaceId} with data ${JSON.stringify(data)}`
|
||||
);
|
||||
return workspace;
|
||||
}
|
||||
|
||||
async get(workspaceId: string) {
|
||||
@@ -125,422 +64,104 @@ export class WorkspaceModel extends BaseModel {
|
||||
});
|
||||
}
|
||||
|
||||
async findMany(ids: string[]) {
|
||||
return await this.db.workspace.findMany({
|
||||
where: {
|
||||
id: { in: ids },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async delete(workspaceId: string) {
|
||||
await this.db.workspace.deleteMany({
|
||||
const rawResult = await this.db.workspace.deleteMany({
|
||||
where: {
|
||||
id: workspaceId,
|
||||
},
|
||||
});
|
||||
this.logger.log(`Deleted workspace ${workspaceId}`);
|
||||
|
||||
if (rawResult.count > 0) {
|
||||
this.event.emit('workspace.deleted', { id: workspaceId });
|
||||
this.logger.log(`Workspace [${workspaceId}] deleted`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the workspace ids that the user is a member of owner.
|
||||
*/
|
||||
async findOwnedIds(userId: string) {
|
||||
const rows = await this.db.workspaceUserPermission.findMany({
|
||||
where: {
|
||||
userId,
|
||||
type: WorkspaceRole.Owner,
|
||||
OR: this.acceptedCondition,
|
||||
},
|
||||
select: {
|
||||
workspaceId: true,
|
||||
},
|
||||
});
|
||||
return rows.map(row => row.workspaceId);
|
||||
async allowUrlPreview(workspaceId: string) {
|
||||
const workspace = await this.get(workspaceId);
|
||||
return workspace?.enableUrlPreview ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the accessible workspaces for the user.
|
||||
*/
|
||||
async findAccessibleWorkspaces(userId: string) {
|
||||
return await this.db.workspaceUserPermission.findMany({
|
||||
where: {
|
||||
userId,
|
||||
OR: this.acceptedCondition,
|
||||
},
|
||||
include: {
|
||||
workspace: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region workspace member and permission
|
||||
|
||||
/**
|
||||
* Grant the workspace member with the given permission and status.
|
||||
*/
|
||||
@Transactional()
|
||||
async grantMember(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
permission: WorkspaceRole = WorkspaceRole.Collaborator,
|
||||
status: WorkspaceMemberStatus = WorkspaceMemberStatus.Pending
|
||||
): Promise<WorkspaceUserPermission> {
|
||||
const data = await this.db.workspaceUserPermission.findUnique({
|
||||
// #region doc
|
||||
async getDoc(workspaceId: string, docId: string) {
|
||||
return await this.db.workspaceDoc.findUnique({
|
||||
where: {
|
||||
workspaceId_userId: {
|
||||
workspaceId,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
// Create a new permission
|
||||
const created = await this.db.workspaceUserPermission.create({
|
||||
data: {
|
||||
workspaceId,
|
||||
userId,
|
||||
type: permission,
|
||||
status:
|
||||
permission === WorkspaceRole.Owner
|
||||
? WorkspaceMemberStatus.Accepted
|
||||
: status,
|
||||
},
|
||||
});
|
||||
this.logger.log(
|
||||
`Granted workspace ${workspaceId} member ${userId} with permission ${WorkspaceRole[permission]}`
|
||||
);
|
||||
await this.notifyMembersUpdated(workspaceId);
|
||||
return created;
|
||||
}
|
||||
|
||||
// If the user is already accepted and the new permission is owner, we need to revoke old owner
|
||||
if (data.status === WorkspaceMemberStatus.Accepted || data.accepted) {
|
||||
const updated = await this.db.workspaceUserPermission.update({
|
||||
where: {
|
||||
workspaceId_userId: { workspaceId, userId },
|
||||
},
|
||||
data: { type: permission },
|
||||
});
|
||||
// If the new permission is owner, we need to revoke old owner
|
||||
if (permission === WorkspaceRole.Owner) {
|
||||
await this.db.workspaceUserPermission.updateMany({
|
||||
where: {
|
||||
workspaceId,
|
||||
type: WorkspaceRole.Owner,
|
||||
userId: { not: userId },
|
||||
},
|
||||
data: { type: WorkspaceRole.Admin },
|
||||
});
|
||||
this.logger.log(
|
||||
`Change owner of workspace ${workspaceId} to ${userId}`
|
||||
);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
// If the user is not accepted, we can update the status directly
|
||||
const allowedStatus = this.getAllowedStatusSource(data.status);
|
||||
if (allowedStatus.includes(status)) {
|
||||
const updated = await this.db.workspaceUserPermission.update({
|
||||
where: { workspaceId_userId: { workspaceId, userId } },
|
||||
data: {
|
||||
status,
|
||||
// TODO(fengmk2): should we update the permission here?
|
||||
// type: permission,
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
}
|
||||
|
||||
// nothing to do
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the workspace member invitation.
|
||||
*/
|
||||
async getMemberInvitation(invitationId: string) {
|
||||
return await this.db.workspaceUserPermission.findUnique({
|
||||
where: {
|
||||
id: invitationId,
|
||||
workspaceId_docId: { workspaceId, docId },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept the workspace member invitation.
|
||||
* @param status: the status to update to, default to `Accepted`. Can be `Accepted` or `UnderReview`.
|
||||
*/
|
||||
async acceptMemberInvitation(
|
||||
invitationId: string,
|
||||
async isPublicPage(workspaceId: string, docId: string) {
|
||||
const doc = await this.getDoc(workspaceId, docId);
|
||||
if (doc?.public) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const workspace = await this.get(workspaceId);
|
||||
return workspace?.public ?? false;
|
||||
}
|
||||
|
||||
async publishDoc(
|
||||
workspaceId: string,
|
||||
status: WorkspaceMemberStatus = WorkspaceMemberStatus.Accepted
|
||||
docId: string,
|
||||
mode: PublicDocMode = PublicDocMode.Page
|
||||
) {
|
||||
const { count } = await this.db.workspaceUserPermission.updateMany({
|
||||
where: {
|
||||
id: invitationId,
|
||||
workspaceId: workspaceId,
|
||||
// TODO(fengmk2): should we check the status here?
|
||||
AND: [{ accepted: false }, { status: WorkspaceMemberStatus.Pending }],
|
||||
},
|
||||
data: { accepted: true, status },
|
||||
return await this.db.workspaceDoc.upsert({
|
||||
where: { workspaceId_docId: { workspaceId, docId } },
|
||||
update: { public: true, mode },
|
||||
create: { workspaceId, docId, public: true, mode },
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
async revokePublicDoc(workspaceId: string, docId: string) {
|
||||
const doc = await this.getDoc(workspaceId, docId);
|
||||
|
||||
if (!doc?.public) {
|
||||
throw new DocIsNotPublic();
|
||||
}
|
||||
|
||||
return await this.db.workspaceDoc.update({
|
||||
where: { workspaceId_docId: { workspaceId, docId } },
|
||||
data: { public: false },
|
||||
});
|
||||
}
|
||||
|
||||
async hasPublicDoc(workspaceId: string) {
|
||||
const count = await this.db.workspaceDoc.count({
|
||||
where: {
|
||||
workspaceId,
|
||||
public: true,
|
||||
},
|
||||
});
|
||||
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a workspace member in accepted status by workspace id and user id.
|
||||
*/
|
||||
async getMember(workspaceId: string, userId: string) {
|
||||
return await this.db.workspaceUserPermission.findUnique({
|
||||
where: {
|
||||
workspaceId_userId: {
|
||||
workspaceId,
|
||||
userId,
|
||||
},
|
||||
OR: this.acceptedCondition,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a workspace member in any status by workspace id and user id.
|
||||
*/
|
||||
async getMemberInAnyStatus(workspaceId: string, userId: string) {
|
||||
return await this.db.workspaceUserPermission.findUnique({
|
||||
where: {
|
||||
workspaceId_userId: {
|
||||
workspaceId,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a given user is a member of a workspace and has the given or higher permission.
|
||||
* Default to read permission.
|
||||
*/
|
||||
async isMember(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
permission: WorkspaceRole = WorkspaceRole.Collaborator
|
||||
) {
|
||||
const count = await this.db.workspaceUserPermission.count({
|
||||
async getPublicDocs(workspaceId: string) {
|
||||
return await this.db.workspaceDoc.findMany({
|
||||
where: {
|
||||
workspaceId,
|
||||
userId,
|
||||
OR: this.acceptedCondition,
|
||||
type: {
|
||||
gte: permission,
|
||||
},
|
||||
},
|
||||
});
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the workspace owner.
|
||||
*/
|
||||
async getOwner(workspaceId: string) {
|
||||
return await this.db.workspaceUserPermission.findFirst({
|
||||
where: {
|
||||
workspaceId,
|
||||
type: WorkspaceRole.Owner,
|
||||
OR: this.acceptedCondition,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
public: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the workspace admins.
|
||||
*/
|
||||
async findAdmins(workspaceId: string) {
|
||||
return await this.db.workspaceUserPermission.findMany({
|
||||
where: {
|
||||
workspaceId,
|
||||
type: WorkspaceRole.Admin,
|
||||
OR: this.acceptedCondition,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
async setDocDefaultRole(workspaceId: string, docId: string, role: DocRole) {
|
||||
await this.db.workspaceDoc.upsert({
|
||||
where: { workspaceId_docId: { workspaceId, docId } },
|
||||
update: { defaultRole: role },
|
||||
create: { workspaceId, docId, defaultRole: role },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the workspace members.
|
||||
*/
|
||||
async findMembers(
|
||||
workspaceId: string,
|
||||
options: FindWorkspaceMembersOptions = {}
|
||||
) {
|
||||
return await this.db.workspaceUserPermission.findMany({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
skip: options.skip,
|
||||
take: options.take || 8,
|
||||
orderBy: [{ createdAt: 'asc' }, { type: 'desc' }],
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a workspace member by workspace id and user id.
|
||||
* Except the owner, the owner can't be deleted.
|
||||
*/
|
||||
async deleteMember(workspaceId: string, userId: string) {
|
||||
const member = await this.getMemberInAnyStatus(workspaceId, userId);
|
||||
|
||||
// We shouldn't revoke owner permission
|
||||
// should auto deleted by workspace/user delete cascading
|
||||
if (!member || member.type === WorkspaceRole.Owner) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.db.workspaceUserPermission.deleteMany({
|
||||
where: {
|
||||
workspaceId,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
this.logger.log(
|
||||
`Deleted workspace member ${userId} from workspace ${workspaceId}`
|
||||
);
|
||||
|
||||
await this.notifyMembersUpdated(workspaceId);
|
||||
|
||||
if (
|
||||
member.status === WorkspaceMemberStatus.UnderReview ||
|
||||
member.status === WorkspaceMemberStatus.NeedMoreSeatAndReview
|
||||
) {
|
||||
this.event.emit('workspace.members.requestDeclined', {
|
||||
workspaceId,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async notifyMembersUpdated(workspaceId: string) {
|
||||
const count = await this.getMemberTotalCount(workspaceId);
|
||||
this.event.emit('workspace.members.updated', {
|
||||
workspaceId,
|
||||
count,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the workspace member total count, including pending and accepted.
|
||||
*/
|
||||
async getMemberTotalCount(workspaceId: string) {
|
||||
return await this.db.workspaceUserPermission.count({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the workspace member used count, only count the accepted member
|
||||
*/
|
||||
async getMemberUsedCount(workspaceId: string) {
|
||||
return await this.db.workspaceUserPermission.count({
|
||||
where: {
|
||||
workspaceId,
|
||||
OR: this.acceptedCondition,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the workspace member seat status.
|
||||
*/
|
||||
@Transactional()
|
||||
async refreshMemberSeatStatus(workspaceId: string, memberLimit: number) {
|
||||
const usedCount = await this.getMemberUsedCount(workspaceId);
|
||||
const availableCount = memberLimit - usedCount;
|
||||
if (availableCount <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const members = await this.db.workspaceUserPermission.findMany({
|
||||
select: { id: true, status: true },
|
||||
where: {
|
||||
workspaceId,
|
||||
status: {
|
||||
in: [
|
||||
WorkspaceMemberStatus.NeedMoreSeat,
|
||||
WorkspaceMemberStatus.NeedMoreSeatAndReview,
|
||||
],
|
||||
},
|
||||
},
|
||||
// find the oldest members first
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
|
||||
const needChange = members.slice(0, availableCount);
|
||||
const groups = groupBy(needChange, m => m.status);
|
||||
|
||||
const toPendings = groups.NeedMoreSeat;
|
||||
if (toPendings) {
|
||||
// NeedMoreSeat => Pending
|
||||
await this.db.workspaceUserPermission.updateMany({
|
||||
where: { id: { in: toPendings.map(m => m.id) } },
|
||||
data: { status: WorkspaceMemberStatus.Pending },
|
||||
});
|
||||
}
|
||||
|
||||
const toUnderReviews = groups.NeedMoreSeatAndReview;
|
||||
if (toUnderReviews) {
|
||||
// NeedMoreSeatAndReview => UnderReview
|
||||
await this.db.workspaceUserPermission.updateMany({
|
||||
where: { id: { in: toUnderReviews.map(m => m.id) } },
|
||||
data: { status: WorkspaceMemberStatus.UnderReview },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepted condition for workspace member.
|
||||
*/
|
||||
private get acceptedCondition() {
|
||||
return [
|
||||
{
|
||||
// keep compatibility with old data
|
||||
accepted: true,
|
||||
},
|
||||
{
|
||||
status: WorkspaceMemberStatus.Accepted,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* NeedMoreSeat => Pending
|
||||
*
|
||||
* NeedMoreSeatAndReview => UnderReview
|
||||
*
|
||||
* Pending | UnderReview => Accepted
|
||||
*/
|
||||
private getAllowedStatusSource(
|
||||
to: WorkspaceMemberStatus
|
||||
): WorkspaceMemberStatus[] {
|
||||
switch (to) {
|
||||
case WorkspaceMemberStatus.NeedMoreSeat:
|
||||
return [WorkspaceMemberStatus.Pending];
|
||||
case WorkspaceMemberStatus.NeedMoreSeatAndReview:
|
||||
return [WorkspaceMemberStatus.UnderReview];
|
||||
case WorkspaceMemberStatus.Pending:
|
||||
case WorkspaceMemberStatus.UnderReview: // need admin to review in team workspace
|
||||
return [WorkspaceMemberStatus.Accepted];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// #endregion
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user