mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 20:08:37 +00:00
feat(server): allow to set default role in page (#9963)
This commit is contained in:
@@ -115,13 +115,15 @@ model Workspace {
|
||||
// NOTE:
|
||||
// We won't make sure every page has a corresponding record in this table.
|
||||
// Only the ones that have ever changed will have records here,
|
||||
// and for others we will make sure it's has a default value return in our bussiness logic.
|
||||
// and for others we will make sure it's has a default value return in our business logic.
|
||||
model WorkspacePage {
|
||||
workspaceId String @map("workspace_id") @db.VarChar
|
||||
pageId String @map("page_id") @db.VarChar
|
||||
public Boolean @default(false)
|
||||
workspaceId String @map("workspace_id") @db.VarChar
|
||||
pageId String @map("page_id") @db.VarChar
|
||||
public Boolean @default(false)
|
||||
// Workspace user's default role in this page, default is `Manager`
|
||||
defaultRole Int @default(30) @db.SmallInt
|
||||
// Page/Edgeless
|
||||
mode Int @default(0) @db.SmallInt
|
||||
mode Int @default(0) @db.SmallInt
|
||||
|
||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@ -277,7 +279,7 @@ model Snapshot {
|
||||
|
||||
// user snapshots are special snapshots for user storage like personal app settings, distinguished from workspace snapshots
|
||||
// basically they share the same structure with workspace snapshots
|
||||
// but for convenience, we don't fork the updates queue and hisotry for user snapshots, until we have to
|
||||
// but for convenience, we don't fork the updates queue and history for user snapshots, until we have to
|
||||
// which means all operation on user snapshot will happen in-pace
|
||||
model UserSnapshot {
|
||||
userId String @map("user_id") @db.VarChar
|
||||
@@ -299,7 +301,7 @@ model Update {
|
||||
createdAt DateTime @map("created_at") @db.Timestamptz(3)
|
||||
createdBy String? @map("created_by") @db.VarChar
|
||||
|
||||
// will delete createor record if createor's account is deleted
|
||||
// will delete creator record if creator's account is deleted
|
||||
createdByUser User? @relation(name: "createdUpdate", fields: [createdBy], references: [id], onDelete: SetNull)
|
||||
|
||||
// @deprecated use createdAt only
|
||||
@@ -318,7 +320,7 @@ model SnapshotHistory {
|
||||
expiredAt DateTime @map("expired_at") @db.Timestamptz(3)
|
||||
createdBy String? @map("created_by") @db.VarChar
|
||||
|
||||
// will delete createor record if creator's account is deleted
|
||||
// will delete creator record if creator's account is deleted
|
||||
createdByUser User? @relation(name: "createdHistory", fields: [createdBy], references: [id], onDelete: SetNull)
|
||||
|
||||
@@id([workspaceId, id, timestamp])
|
||||
|
||||
@@ -6,7 +6,9 @@ import { getCurrentMailMessageCount } from '@affine-test/kit/utils/cloud';
|
||||
import { WorkspaceMemberStatus } from '@prisma/client';
|
||||
import type { TestFn } from 'ava';
|
||||
import ava from 'ava';
|
||||
import { nanoid } from 'nanoid';
|
||||
import Sinon from 'sinon';
|
||||
import request from 'supertest';
|
||||
|
||||
import { AppModule } from '../app.module';
|
||||
import { EventBus } from '../base';
|
||||
@@ -199,6 +201,11 @@ const init = async (
|
||||
WorkspaceRole.Collaborator
|
||||
);
|
||||
|
||||
const external = await invite(
|
||||
`${prefix}external@affine.pro`,
|
||||
WorkspaceRole.External
|
||||
);
|
||||
|
||||
return {
|
||||
invite,
|
||||
inviteBatch,
|
||||
@@ -209,12 +216,13 @@ const init = async (
|
||||
admin,
|
||||
write,
|
||||
read,
|
||||
external,
|
||||
};
|
||||
};
|
||||
|
||||
test('should be able to invite multiple users', async t => {
|
||||
const { app } = t.context;
|
||||
const { teamWorkspace: ws, owner, admin, write, read } = await init(app, 4);
|
||||
const { teamWorkspace: ws, owner, admin, write, read } = await init(app, 5);
|
||||
|
||||
{
|
||||
// no permission
|
||||
@@ -265,7 +273,7 @@ test('should be able to invite multiple users', async t => {
|
||||
|
||||
test('should be able to check seat limit', async t => {
|
||||
const { app, permissions, models } = t.context;
|
||||
const { invite, inviteBatch, teamWorkspace: ws } = await init(app, 4);
|
||||
const { invite, inviteBatch, teamWorkspace: ws } = await init(app, 5);
|
||||
|
||||
{
|
||||
// invite
|
||||
@@ -275,7 +283,7 @@ test('should be able to check seat limit', async t => {
|
||||
'should throw error if exceed member limit'
|
||||
);
|
||||
models.workspaceFeature.add(ws.id, 'team_plan_v1', 'test', {
|
||||
memberLimit: 5,
|
||||
memberLimit: 6,
|
||||
});
|
||||
await t.notThrowsAsync(
|
||||
invite('member4@affine.pro', WorkspaceRole.Collaborator),
|
||||
@@ -303,7 +311,7 @@ test('should be able to check seat limit', async t => {
|
||||
// refresh seat, fifo
|
||||
sleep(1000);
|
||||
const [[members2]] = await inviteBatch(['member6@affine.pro']);
|
||||
await permissions.refreshSeatStatus(ws.id, 6);
|
||||
await permissions.refreshSeatStatus(ws.id, 7);
|
||||
|
||||
t.is(
|
||||
await permissions.getWorkspaceMemberStatus(
|
||||
@@ -471,7 +479,7 @@ test('should be able to manage invite link', async t => {
|
||||
admin,
|
||||
write,
|
||||
read,
|
||||
} = await init(app, 4);
|
||||
} = await init(app);
|
||||
|
||||
for (const [workspace, managers] of [
|
||||
[ws, [owner]],
|
||||
@@ -519,7 +527,7 @@ test('should be able to manage invite link', async t => {
|
||||
|
||||
test('should be able to approve team member', async t => {
|
||||
const { app } = t.context;
|
||||
const { teamWorkspace: tws, owner, admin, write, read } = await init(app, 5);
|
||||
const { teamWorkspace: tws, owner, admin, write, read } = await init(app, 6);
|
||||
|
||||
{
|
||||
const { link } = await createInviteLink(
|
||||
@@ -577,7 +585,7 @@ test('should be able to invite by link', async t => {
|
||||
owner,
|
||||
workspace: ws,
|
||||
teamWorkspace: tws,
|
||||
} = await init(app, 4);
|
||||
} = await init(app, 5);
|
||||
const [inviteId, invite] = await createInviteLink(ws);
|
||||
const [teamInviteId, teamInvite, acceptTeamInvite] =
|
||||
await createInviteLink(tws);
|
||||
@@ -594,7 +602,7 @@ test('should be able to invite by link', async t => {
|
||||
|
||||
{
|
||||
// invite link
|
||||
for (const [i] of Array.from({ length: 6 }).entries()) {
|
||||
for (const [i] of Array.from({ length: 5 }).entries()) {
|
||||
const user = await invite(`test${i}@affine.pro`);
|
||||
const status = await permissions.getWorkspaceMemberStatus(ws.id, user.id);
|
||||
t.is(
|
||||
@@ -632,9 +640,9 @@ test('should be able to invite by link', async t => {
|
||||
);
|
||||
|
||||
models.workspaceFeature.add(tws.id, 'team_plan_v1', 'test', {
|
||||
memberLimit: 5,
|
||||
memberLimit: 6,
|
||||
});
|
||||
await permissions.refreshSeatStatus(tws.id, 5);
|
||||
await permissions.refreshSeatStatus(tws.id, 6);
|
||||
t.is(
|
||||
await permissions.getWorkspaceMemberStatus(tws.id, m3.id),
|
||||
WorkspaceMemberStatus.UnderReview,
|
||||
@@ -647,9 +655,9 @@ test('should be able to invite by link', async t => {
|
||||
);
|
||||
|
||||
models.workspaceFeature.add(tws.id, 'team_plan_v1', 'test', {
|
||||
memberLimit: 6,
|
||||
memberLimit: 7,
|
||||
});
|
||||
await permissions.refreshSeatStatus(tws.id, 6);
|
||||
await permissions.refreshSeatStatus(tws.id, 7);
|
||||
t.is(
|
||||
await permissions.getWorkspaceMemberStatus(tws.id, m4.id),
|
||||
WorkspaceMemberStatus.UnderReview,
|
||||
@@ -669,7 +677,7 @@ test('should be able to invite by link', async t => {
|
||||
|
||||
test('should be able to send mails', async t => {
|
||||
const { app } = t.context;
|
||||
const { inviteBatch } = await init(app, 4);
|
||||
const { inviteBatch } = await init(app, 5);
|
||||
const primitiveMailCount = await getCurrentMailMessageCount();
|
||||
|
||||
{
|
||||
@@ -682,7 +690,7 @@ test('should be able to emit events', async t => {
|
||||
const { app, event } = t.context;
|
||||
|
||||
{
|
||||
const { teamWorkspace: tws, inviteBatch } = await init(app, 4);
|
||||
const { teamWorkspace: tws, inviteBatch } = await init(app, 5);
|
||||
|
||||
await inviteBatch(['m1@affine.pro', 'm2@affine.pro']);
|
||||
const [membersUpdated] = event.emit
|
||||
@@ -693,7 +701,7 @@ test('should be able to emit events', async t => {
|
||||
'workspace.members.updated',
|
||||
{
|
||||
workspaceId: tws.id,
|
||||
count: 6,
|
||||
count: 7,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -787,7 +795,7 @@ test('should be able to emit events', async t => {
|
||||
[
|
||||
'workspace.members.updated',
|
||||
{
|
||||
count: 3,
|
||||
count: 4,
|
||||
workspaceId: tws.id,
|
||||
},
|
||||
],
|
||||
@@ -795,3 +803,225 @@ test('should be able to emit events', async t => {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('should be able to change the default role in page', async t => {
|
||||
const { app } = t.context;
|
||||
const { teamWorkspace: ws, admin } = await init(app, 5);
|
||||
const pageId = nanoid();
|
||||
const res = await request(app.getHttpServer())
|
||||
.post('/graphql')
|
||||
.auth(admin.token.token, { type: 'bearer' })
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
updatePageDefaultRole(input: {
|
||||
workspaceId: "${ws.id}",
|
||||
docId: "${pageId}",
|
||||
role: Reader,
|
||||
})
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
t.deepEqual(res.body, {
|
||||
data: {
|
||||
updatePageDefaultRole: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Default page role should be able to override the workspace role', async t => {
|
||||
const { app } = t.context;
|
||||
const {
|
||||
teamWorkspace: workspace,
|
||||
admin,
|
||||
read,
|
||||
external,
|
||||
} = await init(app, 5);
|
||||
|
||||
const pageId = nanoid();
|
||||
|
||||
const res = await request(app.getHttpServer())
|
||||
.post('/graphql')
|
||||
.auth(admin.token.token, { type: 'bearer' })
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
updatePageDefaultRole(input: {
|
||||
workspaceId: "${workspace.id}",
|
||||
docId: "${pageId}",
|
||||
role: Manager,
|
||||
})
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
t.deepEqual(res.body, {
|
||||
data: {
|
||||
updatePageDefaultRole: true,
|
||||
},
|
||||
});
|
||||
|
||||
// reader can manage the page if the page default role is Manager
|
||||
{
|
||||
const readerRes = await request(app.getHttpServer())
|
||||
.post('/graphql')
|
||||
.auth(read.token.token, { type: 'bearer' })
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
updatePageDefaultRole(input: {
|
||||
workspaceId: "${workspace.id}",
|
||||
docId: "${pageId}",
|
||||
role: Manager,
|
||||
})
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
t.deepEqual(readerRes.body, {
|
||||
data: {
|
||||
updatePageDefaultRole: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// external can't manage the page even if the page default role is Manager
|
||||
{
|
||||
const externalRes = await request(app.getHttpServer())
|
||||
.post('/graphql')
|
||||
.auth(external.token.token, { type: 'bearer' })
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
updatePageDefaultRole(input: {
|
||||
workspaceId: "${workspace.id}",
|
||||
docId: "${pageId}",
|
||||
role: Manager,
|
||||
})
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
t.like(externalRes.body, {
|
||||
errors: [
|
||||
{
|
||||
message: `You do not have permission to access doc ${pageId} under Space ${workspace.id}.`,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('should be able to grant and revoke doc user role', async t => {
|
||||
const { app } = t.context;
|
||||
const { teamWorkspace: ws, admin, read, external } = await init(app, 5);
|
||||
const pageId = nanoid();
|
||||
const res = await request(app.getHttpServer())
|
||||
.post('/graphql')
|
||||
.auth(admin.token.token, { type: 'bearer' })
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
grantDocUserRoles(input: {
|
||||
workspaceId: "${ws.id}",
|
||||
docId: "${pageId}",
|
||||
role: Manager,
|
||||
userIds: ["${external.id}"]
|
||||
})
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
t.deepEqual(res.body, {
|
||||
data: {
|
||||
grantDocUserRoles: true,
|
||||
},
|
||||
});
|
||||
|
||||
// external user now can manage the page
|
||||
{
|
||||
const externalRes = await request(app.getHttpServer())
|
||||
.post('/graphql')
|
||||
.auth(external.token.token, { type: 'bearer' })
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
grantDocUserRoles(input: {
|
||||
workspaceId: "${ws.id}",
|
||||
docId: "${pageId}",
|
||||
role: Manager,
|
||||
userIds: ["${read.id}"]
|
||||
})
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
t.deepEqual(externalRes.body, {
|
||||
data: {
|
||||
grantDocUserRoles: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// revoke the role of the external user
|
||||
{
|
||||
const revokeRes = await request(app.getHttpServer())
|
||||
.post('/graphql')
|
||||
.auth(admin.token.token, { type: 'bearer' })
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
revokeDocUserRoles(input: {
|
||||
workspaceId: "${ws.id}",
|
||||
docId: "${pageId}",
|
||||
userIds: ["${external.id}"]
|
||||
})
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
t.deepEqual(revokeRes.body, {
|
||||
data: {
|
||||
revokeDocUserRoles: true,
|
||||
},
|
||||
});
|
||||
|
||||
// external user can't manage the page
|
||||
const externalRes = await request(app.getHttpServer())
|
||||
.post('/graphql')
|
||||
.auth(external.token.token, { type: 'bearer' })
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
revokeDocUserRoles(input: {
|
||||
workspaceId: "${ws.id}",
|
||||
docId: "${pageId}",
|
||||
userIds: ["${read.id}"]
|
||||
})
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
t.like(externalRes.body, {
|
||||
errors: [
|
||||
{
|
||||
message: `You do not have permission to access Space ${ws.id}.`,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -54,7 +54,7 @@ test('should create a workspace', async t => {
|
||||
t.is(typeof workspace.id, 'string', 'workspace.id is not a string');
|
||||
});
|
||||
|
||||
test('should can publish workspace', async t => {
|
||||
test('should be able to publish workspace', async t => {
|
||||
const { app } = t.context;
|
||||
const user = await signUp(app, 'u1', 'u1@affine.pro', '1');
|
||||
const workspace = await createWorkspace(app, user.token.token);
|
||||
|
||||
@@ -470,6 +470,10 @@ export const USER_FRIENDLY_ERRORS = {
|
||||
type: 'action_forbidden',
|
||||
message: 'A Team workspace is required to perform this action.',
|
||||
},
|
||||
page_default_role_can_not_be_owner: {
|
||||
type: 'invalid_input',
|
||||
message: 'Page default role can not be owner.',
|
||||
},
|
||||
|
||||
// Subscription Errors
|
||||
unsupported_subscription_plan: {
|
||||
|
||||
@@ -405,6 +405,12 @@ export class ActionForbiddenOnNonTeamWorkspace extends UserFriendlyError {
|
||||
super('action_forbidden', 'action_forbidden_on_non_team_workspace', message);
|
||||
}
|
||||
}
|
||||
|
||||
export class PageDefaultRoleCanNotBeOwner extends UserFriendlyError {
|
||||
constructor(message?: string) {
|
||||
super('invalid_input', 'page_default_role_can_not_be_owner', message);
|
||||
}
|
||||
}
|
||||
@ObjectType()
|
||||
class UnsupportedSubscriptionPlanDataType {
|
||||
@Field() plan!: string
|
||||
@@ -761,6 +767,7 @@ export enum ErrorNames {
|
||||
FAILED_TO_SAVE_UPDATES,
|
||||
FAILED_TO_UPSERT_SNAPSHOT,
|
||||
ACTION_FORBIDDEN_ON_NON_TEAM_WORKSPACE,
|
||||
PAGE_DEFAULT_ROLE_CAN_NOT_BE_OWNER,
|
||||
UNSUPPORTED_SUBSCRIPTION_PLAN,
|
||||
FAILED_TO_CHECKOUT,
|
||||
INVALID_CHECKOUT_PARAMETERS,
|
||||
|
||||
@@ -571,30 +571,69 @@ export class PermissionService {
|
||||
}
|
||||
|
||||
if (user) {
|
||||
const count = await this.prisma.workspacePageUserPermission.count({
|
||||
where: {
|
||||
workspaceId: ws,
|
||||
pageId: page,
|
||||
userId: user,
|
||||
type: {
|
||||
gte: role,
|
||||
const [roleEntity, pageEntity, workspaceRoleEntity] = await Promise.all([
|
||||
this.prisma.workspacePageUserPermission.findFirst({
|
||||
where: {
|
||||
workspaceId: ws,
|
||||
pageId: page,
|
||||
userId: user,
|
||||
},
|
||||
},
|
||||
});
|
||||
select: {
|
||||
type: true,
|
||||
},
|
||||
}),
|
||||
this.prisma.workspacePage.findFirst({
|
||||
where: {
|
||||
workspaceId: ws,
|
||||
pageId: page,
|
||||
},
|
||||
select: {
|
||||
defaultRole: true,
|
||||
},
|
||||
}),
|
||||
this.prisma.workspaceUserPermission.findFirst({
|
||||
where: {
|
||||
workspaceId: ws,
|
||||
userId: user,
|
||||
OR: this.acceptedCondition,
|
||||
},
|
||||
select: {
|
||||
type: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// page shared to user
|
||||
// accessible
|
||||
if (count > 0) {
|
||||
if (
|
||||
// Page role exists, check it first
|
||||
(roleEntity && roleEntity.type >= role) ||
|
||||
// if
|
||||
// - page has a default role
|
||||
// - the user is in this workspace
|
||||
// - the user is not an external user in this workspace
|
||||
// then use the max of the two
|
||||
(workspaceRoleEntity &&
|
||||
workspaceRoleEntity.type !== WorkspaceRole.External &&
|
||||
Math.max(
|
||||
roleEntity?.type ?? Number.MIN_SAFE_INTEGER,
|
||||
pageEntity?.defaultRole ?? Number.MIN_SAFE_INTEGER
|
||||
) >= role)
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
this.logger.log("User's PageRole is lower than required", {
|
||||
workspaceId: ws,
|
||||
pageId: page,
|
||||
userId: user,
|
||||
requiredRole: DocRole[role],
|
||||
action,
|
||||
});
|
||||
}
|
||||
this.logger.log("User's role is lower than required", {
|
||||
workspaceId: ws,
|
||||
docId: page,
|
||||
userId: user,
|
||||
workspaceRole: workspaceRoleEntity
|
||||
? WorkspaceRole[workspaceRoleEntity.type]
|
||||
: undefined,
|
||||
pageRole: roleEntity ? DocRole[roleEntity.type] : undefined,
|
||||
pageDefaultRole: pageEntity
|
||||
? DocRole[pageEntity.defaultRole]
|
||||
: undefined,
|
||||
requiredRole: DocRole[role],
|
||||
action,
|
||||
});
|
||||
}
|
||||
|
||||
// check whether user has workspace related permission
|
||||
|
||||
@@ -240,7 +240,7 @@ export function mapDocRoleToPermissions(docRole: DocRole) {
|
||||
export function fixupDocRole(
|
||||
workspaceRole: WorkspaceRole = WorkspaceRole.External,
|
||||
docRole: DocRole = DocRole.External
|
||||
) {
|
||||
): DocRole {
|
||||
switch (workspaceRole) {
|
||||
case WorkspaceRole.External:
|
||||
// Workspace External user won't be able to have any high permission doc role
|
||||
|
||||
@@ -14,11 +14,13 @@ import type { WorkspacePage as PrismaWorkspacePage } from '@prisma/client';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import {
|
||||
DocAccessDenied,
|
||||
ExpectToGrantDocUserRoles,
|
||||
ExpectToPublishPage,
|
||||
ExpectToRevokeDocUserRoles,
|
||||
ExpectToRevokePublicPage,
|
||||
ExpectToUpdateDocUserRole,
|
||||
PageDefaultRoleCanNotBeOwner,
|
||||
PageIsNotPublic,
|
||||
paginate,
|
||||
Paginated,
|
||||
@@ -74,6 +76,45 @@ class GrantDocUserRolesInput {
|
||||
userIds!: string[];
|
||||
}
|
||||
|
||||
@InputType()
|
||||
class UpdateDocUserRoleInput {
|
||||
@Field(() => String)
|
||||
docId!: string;
|
||||
|
||||
@Field(() => String)
|
||||
workspaceId!: string;
|
||||
|
||||
@Field(() => String)
|
||||
userId!: string;
|
||||
|
||||
@Field(() => DocRole)
|
||||
role!: DocRole;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
class RevokeDocUserRolesInput {
|
||||
@Field(() => String)
|
||||
docId!: string;
|
||||
|
||||
@Field(() => String)
|
||||
workspaceId!: string;
|
||||
|
||||
@Field(() => [String])
|
||||
userIds!: string[];
|
||||
}
|
||||
|
||||
@InputType()
|
||||
class UpdatePageDefaultRoleInput {
|
||||
@Field(() => String)
|
||||
docId!: string;
|
||||
|
||||
@Field(() => String)
|
||||
workspaceId!: string;
|
||||
|
||||
@Field(() => DocRole)
|
||||
role!: DocRole;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
class GrantedDocUserType {
|
||||
@Field(() => String)
|
||||
@@ -413,12 +454,11 @@ export class PagePermissionResolver {
|
||||
@Mutation(() => Boolean)
|
||||
async revokeDocUserRoles(
|
||||
@CurrentUser() user: CurrentUser,
|
||||
@Args('docId') docId: string,
|
||||
@Args('userIds', { type: () => [String] }) userIds: string[]
|
||||
@Args('input') input: RevokeDocUserRolesInput
|
||||
): Promise<boolean> {
|
||||
const doc = new DocID(docId);
|
||||
const doc = new DocID(input.docId, input.workspaceId);
|
||||
const pairs = {
|
||||
spaceId: doc.workspace,
|
||||
spaceId: input.workspaceId,
|
||||
docId: doc.guid,
|
||||
};
|
||||
if (doc.isWorkspace) {
|
||||
@@ -436,10 +476,10 @@ export class PagePermissionResolver {
|
||||
user.id,
|
||||
WorkspaceRole.Collaborator
|
||||
);
|
||||
await this.permission.revokePage(doc.workspace, doc.guid, userIds);
|
||||
await this.permission.revokePage(doc.workspace, doc.guid, input.userIds);
|
||||
this.logger.log('Revoke doc user roles', {
|
||||
...pairs,
|
||||
userIds: userIds,
|
||||
userIds: input.userIds,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
@@ -447,11 +487,9 @@ export class PagePermissionResolver {
|
||||
@Mutation(() => Boolean)
|
||||
async updateDocUserRole(
|
||||
@CurrentUser() user: CurrentUser,
|
||||
@Args('docId') docId: string,
|
||||
@Args('userId') userId: string,
|
||||
@Args('role', { type: () => DocRole }) role: DocRole
|
||||
@Args('input') input: UpdateDocUserRoleInput
|
||||
): Promise<boolean> {
|
||||
const doc = new DocID(docId);
|
||||
const doc = new DocID(input.docId, input.workspaceId);
|
||||
const pairs = {
|
||||
spaceId: doc.workspace,
|
||||
docId: doc.guid,
|
||||
@@ -471,32 +509,94 @@ export class PagePermissionResolver {
|
||||
user.id,
|
||||
WorkspaceRole.Collaborator
|
||||
);
|
||||
if (role === DocRole.Owner) {
|
||||
if (input.role === DocRole.Owner) {
|
||||
const ret = await this.permission.grantPagePermission(
|
||||
doc.workspace,
|
||||
doc.guid,
|
||||
[userId],
|
||||
role
|
||||
[input.userId],
|
||||
input.role
|
||||
);
|
||||
this.logger.log('Transfer doc owner', {
|
||||
...pairs,
|
||||
userId: userId,
|
||||
role: role,
|
||||
userId: input.userId,
|
||||
role: input.role,
|
||||
});
|
||||
return ret.length > 0;
|
||||
} else {
|
||||
await this.permission.updatePagePermission(
|
||||
doc.workspace,
|
||||
doc.guid,
|
||||
userId,
|
||||
role
|
||||
input.userId,
|
||||
input.role
|
||||
);
|
||||
this.logger.log('Update doc user role', {
|
||||
...pairs,
|
||||
userId: userId,
|
||||
role: role,
|
||||
userId: input.userId,
|
||||
role: input.role,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
async updatePageDefaultRole(
|
||||
@CurrentUser() user: CurrentUser,
|
||||
@Args('input') input: UpdatePageDefaultRoleInput
|
||||
) {
|
||||
if (input.role === DocRole.Owner) {
|
||||
this.logger.log('Page default role can not be owner', input);
|
||||
throw new PageDefaultRoleCanNotBeOwner();
|
||||
}
|
||||
const doc = new DocID(input.docId, input.workspaceId);
|
||||
const pairs = {
|
||||
spaceId: doc.workspace,
|
||||
docId: doc.guid,
|
||||
};
|
||||
if (doc.isWorkspace) {
|
||||
this.logger.error(
|
||||
'Expect to update page default role, but it is a workspace',
|
||||
pairs
|
||||
);
|
||||
throw new ExpectToUpdateDocUserRole(
|
||||
pairs,
|
||||
'Expect doc not to be workspace'
|
||||
);
|
||||
}
|
||||
try {
|
||||
await this.permission.checkCloudPagePermission(
|
||||
doc.workspace,
|
||||
doc.guid,
|
||||
'Doc.Users.Manage',
|
||||
user.id
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof DocAccessDenied) {
|
||||
this.logger.log(
|
||||
'User does not have permission to update page default role',
|
||||
{
|
||||
...pairs,
|
||||
userId: user.id,
|
||||
}
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
await this.prisma.workspacePage.upsert({
|
||||
where: {
|
||||
workspaceId_pageId: {
|
||||
workspaceId: doc.workspace,
|
||||
pageId: doc.guid,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
defaultRole: input.role,
|
||||
},
|
||||
create: {
|
||||
workspaceId: doc.workspace,
|
||||
pageId: doc.guid,
|
||||
defaultRole: input.role,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,6 +305,7 @@ enum ErrorNames {
|
||||
NO_COPILOT_PROVIDER_AVAILABLE
|
||||
OAUTH_ACCOUNT_ALREADY_CONNECTED
|
||||
OAUTH_STATE_EXPIRED
|
||||
PAGE_DEFAULT_ROLE_CAN_NOT_BE_OWNER
|
||||
PAGE_IS_NOT_PUBLIC
|
||||
PASSWORD_REQUIRED
|
||||
QUERY_TOO_LONG
|
||||
@@ -630,7 +631,7 @@ type Mutation {
|
||||
removeWorkspaceFeature(feature: FeatureType!, workspaceId: String!): Boolean!
|
||||
resumeSubscription(idempotencyKey: String @deprecated(reason: "use header `Idempotency-Key`"), plan: SubscriptionPlan = Pro, workspaceId: String): SubscriptionType!
|
||||
revoke(userId: String!, workspaceId: String!): Boolean!
|
||||
revokeDocUserRoles(docId: String!, userIds: [String!]!): Boolean!
|
||||
revokeDocUserRoles(input: RevokeDocUserRolesInput!): Boolean!
|
||||
revokeInviteLink(workspaceId: String!): Boolean!
|
||||
revokePage(pageId: String!, workspaceId: String!): Boolean! @deprecated(reason: "use revokePublicPage")
|
||||
revokePublicPage(pageId: String!, workspaceId: String!): WorkspacePage!
|
||||
@@ -647,7 +648,8 @@ type Mutation {
|
||||
|
||||
"""Update a chat session"""
|
||||
updateCopilotSession(options: UpdateChatSessionInput!): String!
|
||||
updateDocUserRole(docId: String!, role: DocRole!, userId: String!): Boolean!
|
||||
updateDocUserRole(input: UpdateDocUserRoleInput!): Boolean!
|
||||
updatePageDefaultRole(input: UpdatePageDefaultRoleInput!): Boolean!
|
||||
updateProfile(input: UpdateUserInput!): UserType!
|
||||
|
||||
"""update server runtime configurable setting"""
|
||||
@@ -802,6 +804,12 @@ type RemoveAvatar {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
input RevokeDocUserRolesInput {
|
||||
docId: String!
|
||||
userIds: [String!]!
|
||||
workspaceId: String!
|
||||
}
|
||||
|
||||
type RuntimeConfigNotFoundDataType {
|
||||
key: String!
|
||||
}
|
||||
@@ -996,6 +1004,19 @@ input UpdateChatSessionInput {
|
||||
sessionId: String!
|
||||
}
|
||||
|
||||
input UpdateDocUserRoleInput {
|
||||
docId: String!
|
||||
role: DocRole!
|
||||
userId: String!
|
||||
workspaceId: String!
|
||||
}
|
||||
|
||||
input UpdatePageDefaultRoleInput {
|
||||
docId: String!
|
||||
role: DocRole!
|
||||
workspaceId: String!
|
||||
}
|
||||
|
||||
input UpdateUserInput {
|
||||
"""User name"""
|
||||
name: String
|
||||
|
||||
Reference in New Issue
Block a user