feat(server): allow to set default role in page (#9963)

This commit is contained in:
Brooooooklyn
2025-02-06 17:18:50 +00:00
parent f309f8f3e1
commit 41107eafae
9 changed files with 470 additions and 67 deletions

View File

@@ -115,11 +115,13 @@ model Workspace {
// NOTE: // NOTE:
// We won't make sure every page has a corresponding record in this table. // 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, // 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 { model WorkspacePage {
workspaceId String @map("workspace_id") @db.VarChar workspaceId String @map("workspace_id") @db.VarChar
pageId String @map("page_id") @db.VarChar pageId String @map("page_id") @db.VarChar
public Boolean @default(false) public Boolean @default(false)
// Workspace user's default role in this page, default is `Manager`
defaultRole Int @default(30) @db.SmallInt
// Page/Edgeless // Page/Edgeless
mode Int @default(0) @db.SmallInt mode Int @default(0) @db.SmallInt
@@ -277,7 +279,7 @@ model Snapshot {
// user snapshots are special snapshots for user storage like personal app settings, distinguished from workspace snapshots // 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 // 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 // which means all operation on user snapshot will happen in-pace
model UserSnapshot { model UserSnapshot {
userId String @map("user_id") @db.VarChar userId String @map("user_id") @db.VarChar
@@ -299,7 +301,7 @@ model Update {
createdAt DateTime @map("created_at") @db.Timestamptz(3) createdAt DateTime @map("created_at") @db.Timestamptz(3)
createdBy String? @map("created_by") @db.VarChar 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) createdByUser User? @relation(name: "createdUpdate", fields: [createdBy], references: [id], onDelete: SetNull)
// @deprecated use createdAt only // @deprecated use createdAt only
@@ -318,7 +320,7 @@ model SnapshotHistory {
expiredAt DateTime @map("expired_at") @db.Timestamptz(3) expiredAt DateTime @map("expired_at") @db.Timestamptz(3)
createdBy String? @map("created_by") @db.VarChar 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) createdByUser User? @relation(name: "createdHistory", fields: [createdBy], references: [id], onDelete: SetNull)
@@id([workspaceId, id, timestamp]) @@id([workspaceId, id, timestamp])

View File

@@ -6,7 +6,9 @@ import { getCurrentMailMessageCount } from '@affine-test/kit/utils/cloud';
import { WorkspaceMemberStatus } from '@prisma/client'; import { WorkspaceMemberStatus } from '@prisma/client';
import type { TestFn } from 'ava'; import type { TestFn } from 'ava';
import ava from 'ava'; import ava from 'ava';
import { nanoid } from 'nanoid';
import Sinon from 'sinon'; import Sinon from 'sinon';
import request from 'supertest';
import { AppModule } from '../app.module'; import { AppModule } from '../app.module';
import { EventBus } from '../base'; import { EventBus } from '../base';
@@ -199,6 +201,11 @@ const init = async (
WorkspaceRole.Collaborator WorkspaceRole.Collaborator
); );
const external = await invite(
`${prefix}external@affine.pro`,
WorkspaceRole.External
);
return { return {
invite, invite,
inviteBatch, inviteBatch,
@@ -209,12 +216,13 @@ const init = async (
admin, admin,
write, write,
read, read,
external,
}; };
}; };
test('should be able to invite multiple users', async t => { test('should be able to invite multiple users', async t => {
const { app } = t.context; 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 // 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 => { test('should be able to check seat limit', async t => {
const { app, permissions, models } = t.context; 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 // invite
@@ -275,7 +283,7 @@ test('should be able to check seat limit', async t => {
'should throw error if exceed member limit' 'should throw error if exceed member limit'
); );
models.workspaceFeature.add(ws.id, 'team_plan_v1', 'test', { models.workspaceFeature.add(ws.id, 'team_plan_v1', 'test', {
memberLimit: 5, memberLimit: 6,
}); });
await t.notThrowsAsync( await t.notThrowsAsync(
invite('member4@affine.pro', WorkspaceRole.Collaborator), invite('member4@affine.pro', WorkspaceRole.Collaborator),
@@ -303,7 +311,7 @@ test('should be able to check seat limit', async t => {
// refresh seat, fifo // refresh seat, fifo
sleep(1000); sleep(1000);
const [[members2]] = await inviteBatch(['member6@affine.pro']); const [[members2]] = await inviteBatch(['member6@affine.pro']);
await permissions.refreshSeatStatus(ws.id, 6); await permissions.refreshSeatStatus(ws.id, 7);
t.is( t.is(
await permissions.getWorkspaceMemberStatus( await permissions.getWorkspaceMemberStatus(
@@ -471,7 +479,7 @@ test('should be able to manage invite link', async t => {
admin, admin,
write, write,
read, read,
} = await init(app, 4); } = await init(app);
for (const [workspace, managers] of [ for (const [workspace, managers] of [
[ws, [owner]], [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 => { test('should be able to approve team member', async t => {
const { app } = t.context; 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( const { link } = await createInviteLink(
@@ -577,7 +585,7 @@ test('should be able to invite by link', async t => {
owner, owner,
workspace: ws, workspace: ws,
teamWorkspace: tws, teamWorkspace: tws,
} = await init(app, 4); } = await init(app, 5);
const [inviteId, invite] = await createInviteLink(ws); const [inviteId, invite] = await createInviteLink(ws);
const [teamInviteId, teamInvite, acceptTeamInvite] = const [teamInviteId, teamInvite, acceptTeamInvite] =
await createInviteLink(tws); await createInviteLink(tws);
@@ -594,7 +602,7 @@ test('should be able to invite by link', async t => {
{ {
// invite link // 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 user = await invite(`test${i}@affine.pro`);
const status = await permissions.getWorkspaceMemberStatus(ws.id, user.id); const status = await permissions.getWorkspaceMemberStatus(ws.id, user.id);
t.is( 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', { 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( t.is(
await permissions.getWorkspaceMemberStatus(tws.id, m3.id), await permissions.getWorkspaceMemberStatus(tws.id, m3.id),
WorkspaceMemberStatus.UnderReview, 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', { 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( t.is(
await permissions.getWorkspaceMemberStatus(tws.id, m4.id), await permissions.getWorkspaceMemberStatus(tws.id, m4.id),
WorkspaceMemberStatus.UnderReview, 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 => { test('should be able to send mails', async t => {
const { app } = t.context; const { app } = t.context;
const { inviteBatch } = await init(app, 4); const { inviteBatch } = await init(app, 5);
const primitiveMailCount = await getCurrentMailMessageCount(); const primitiveMailCount = await getCurrentMailMessageCount();
{ {
@@ -682,7 +690,7 @@ test('should be able to emit events', async t => {
const { app, event } = t.context; 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']); await inviteBatch(['m1@affine.pro', 'm2@affine.pro']);
const [membersUpdated] = event.emit const [membersUpdated] = event.emit
@@ -693,7 +701,7 @@ test('should be able to emit events', async t => {
'workspace.members.updated', 'workspace.members.updated',
{ {
workspaceId: tws.id, workspaceId: tws.id,
count: 6, count: 7,
}, },
]); ]);
} }
@@ -787,7 +795,7 @@ test('should be able to emit events', async t => {
[ [
'workspace.members.updated', 'workspace.members.updated',
{ {
count: 3, count: 4,
workspaceId: tws.id, 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}.`,
},
],
});
}
});

View File

@@ -54,7 +54,7 @@ test('should create a workspace', async t => {
t.is(typeof workspace.id, 'string', 'workspace.id is not a string'); 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 { app } = t.context;
const user = await signUp(app, 'u1', 'u1@affine.pro', '1'); const user = await signUp(app, 'u1', 'u1@affine.pro', '1');
const workspace = await createWorkspace(app, user.token.token); const workspace = await createWorkspace(app, user.token.token);

View File

@@ -470,6 +470,10 @@ export const USER_FRIENDLY_ERRORS = {
type: 'action_forbidden', type: 'action_forbidden',
message: 'A Team workspace is required to perform this action.', 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 // Subscription Errors
unsupported_subscription_plan: { unsupported_subscription_plan: {

View File

@@ -405,6 +405,12 @@ export class ActionForbiddenOnNonTeamWorkspace extends UserFriendlyError {
super('action_forbidden', 'action_forbidden_on_non_team_workspace', message); 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() @ObjectType()
class UnsupportedSubscriptionPlanDataType { class UnsupportedSubscriptionPlanDataType {
@Field() plan!: string @Field() plan!: string
@@ -761,6 +767,7 @@ export enum ErrorNames {
FAILED_TO_SAVE_UPDATES, FAILED_TO_SAVE_UPDATES,
FAILED_TO_UPSERT_SNAPSHOT, FAILED_TO_UPSERT_SNAPSHOT,
ACTION_FORBIDDEN_ON_NON_TEAM_WORKSPACE, ACTION_FORBIDDEN_ON_NON_TEAM_WORKSPACE,
PAGE_DEFAULT_ROLE_CAN_NOT_BE_OWNER,
UNSUPPORTED_SUBSCRIPTION_PLAN, UNSUPPORTED_SUBSCRIPTION_PLAN,
FAILED_TO_CHECKOUT, FAILED_TO_CHECKOUT,
INVALID_CHECKOUT_PARAMETERS, INVALID_CHECKOUT_PARAMETERS,

View File

@@ -571,31 +571,70 @@ export class PermissionService {
} }
if (user) { if (user) {
const count = await this.prisma.workspacePageUserPermission.count({ const [roleEntity, pageEntity, workspaceRoleEntity] = await Promise.all([
this.prisma.workspacePageUserPermission.findFirst({
where: { where: {
workspaceId: ws, workspaceId: ws,
pageId: page, pageId: page,
userId: user, userId: user,
type: {
gte: role,
}, },
select: {
type: true,
}, },
}); }),
this.prisma.workspacePage.findFirst({
// page shared to user where: {
// accessible
if (count > 0) {
return true;
} else {
this.logger.log("User's PageRole is lower than required", {
workspaceId: ws, workspaceId: ws,
pageId: page, pageId: page,
},
select: {
defaultRole: true,
},
}),
this.prisma.workspaceUserPermission.findFirst({
where: {
workspaceId: ws,
userId: user, userId: user,
OR: this.acceptedCondition,
},
select: {
type: true,
},
}),
]);
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;
}
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], requiredRole: DocRole[role],
action, action,
}); });
} }
}
// check whether user has workspace related permission // check whether user has workspace related permission
return this.tryCheckWorkspace( return this.tryCheckWorkspace(

View File

@@ -240,7 +240,7 @@ export function mapDocRoleToPermissions(docRole: DocRole) {
export function fixupDocRole( export function fixupDocRole(
workspaceRole: WorkspaceRole = WorkspaceRole.External, workspaceRole: WorkspaceRole = WorkspaceRole.External,
docRole: DocRole = DocRole.External docRole: DocRole = DocRole.External
) { ): DocRole {
switch (workspaceRole) { switch (workspaceRole) {
case WorkspaceRole.External: case WorkspaceRole.External:
// Workspace External user won't be able to have any high permission doc role // Workspace External user won't be able to have any high permission doc role

View File

@@ -14,11 +14,13 @@ import type { WorkspacePage as PrismaWorkspacePage } from '@prisma/client';
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import { import {
DocAccessDenied,
ExpectToGrantDocUserRoles, ExpectToGrantDocUserRoles,
ExpectToPublishPage, ExpectToPublishPage,
ExpectToRevokeDocUserRoles, ExpectToRevokeDocUserRoles,
ExpectToRevokePublicPage, ExpectToRevokePublicPage,
ExpectToUpdateDocUserRole, ExpectToUpdateDocUserRole,
PageDefaultRoleCanNotBeOwner,
PageIsNotPublic, PageIsNotPublic,
paginate, paginate,
Paginated, Paginated,
@@ -74,6 +76,45 @@ class GrantDocUserRolesInput {
userIds!: string[]; 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() @ObjectType()
class GrantedDocUserType { class GrantedDocUserType {
@Field(() => String) @Field(() => String)
@@ -413,12 +454,11 @@ export class PagePermissionResolver {
@Mutation(() => Boolean) @Mutation(() => Boolean)
async revokeDocUserRoles( async revokeDocUserRoles(
@CurrentUser() user: CurrentUser, @CurrentUser() user: CurrentUser,
@Args('docId') docId: string, @Args('input') input: RevokeDocUserRolesInput
@Args('userIds', { type: () => [String] }) userIds: string[]
): Promise<boolean> { ): Promise<boolean> {
const doc = new DocID(docId); const doc = new DocID(input.docId, input.workspaceId);
const pairs = { const pairs = {
spaceId: doc.workspace, spaceId: input.workspaceId,
docId: doc.guid, docId: doc.guid,
}; };
if (doc.isWorkspace) { if (doc.isWorkspace) {
@@ -436,10 +476,10 @@ export class PagePermissionResolver {
user.id, user.id,
WorkspaceRole.Collaborator 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', { this.logger.log('Revoke doc user roles', {
...pairs, ...pairs,
userIds: userIds, userIds: input.userIds,
}); });
return true; return true;
} }
@@ -447,11 +487,9 @@ export class PagePermissionResolver {
@Mutation(() => Boolean) @Mutation(() => Boolean)
async updateDocUserRole( async updateDocUserRole(
@CurrentUser() user: CurrentUser, @CurrentUser() user: CurrentUser,
@Args('docId') docId: string, @Args('input') input: UpdateDocUserRoleInput
@Args('userId') userId: string,
@Args('role', { type: () => DocRole }) role: DocRole
): Promise<boolean> { ): Promise<boolean> {
const doc = new DocID(docId); const doc = new DocID(input.docId, input.workspaceId);
const pairs = { const pairs = {
spaceId: doc.workspace, spaceId: doc.workspace,
docId: doc.guid, docId: doc.guid,
@@ -471,32 +509,94 @@ export class PagePermissionResolver {
user.id, user.id,
WorkspaceRole.Collaborator WorkspaceRole.Collaborator
); );
if (role === DocRole.Owner) { if (input.role === DocRole.Owner) {
const ret = await this.permission.grantPagePermission( const ret = await this.permission.grantPagePermission(
doc.workspace, doc.workspace,
doc.guid, doc.guid,
[userId], [input.userId],
role input.role
); );
this.logger.log('Transfer doc owner', { this.logger.log('Transfer doc owner', {
...pairs, ...pairs,
userId: userId, userId: input.userId,
role: role, role: input.role,
}); });
return ret.length > 0; return ret.length > 0;
} else { } else {
await this.permission.updatePagePermission( await this.permission.updatePagePermission(
doc.workspace, doc.workspace,
doc.guid, doc.guid,
userId, input.userId,
role input.role
); );
this.logger.log('Update doc user role', { this.logger.log('Update doc user role', {
...pairs, ...pairs,
userId: userId, userId: input.userId,
role: role, role: input.role,
}); });
return true; 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;
}
} }

View File

@@ -305,6 +305,7 @@ enum ErrorNames {
NO_COPILOT_PROVIDER_AVAILABLE NO_COPILOT_PROVIDER_AVAILABLE
OAUTH_ACCOUNT_ALREADY_CONNECTED OAUTH_ACCOUNT_ALREADY_CONNECTED
OAUTH_STATE_EXPIRED OAUTH_STATE_EXPIRED
PAGE_DEFAULT_ROLE_CAN_NOT_BE_OWNER
PAGE_IS_NOT_PUBLIC PAGE_IS_NOT_PUBLIC
PASSWORD_REQUIRED PASSWORD_REQUIRED
QUERY_TOO_LONG QUERY_TOO_LONG
@@ -630,7 +631,7 @@ type Mutation {
removeWorkspaceFeature(feature: FeatureType!, workspaceId: String!): Boolean! removeWorkspaceFeature(feature: FeatureType!, workspaceId: String!): Boolean!
resumeSubscription(idempotencyKey: String @deprecated(reason: "use header `Idempotency-Key`"), plan: SubscriptionPlan = Pro, workspaceId: String): SubscriptionType! resumeSubscription(idempotencyKey: String @deprecated(reason: "use header `Idempotency-Key`"), plan: SubscriptionPlan = Pro, workspaceId: String): SubscriptionType!
revoke(userId: String!, workspaceId: String!): Boolean! revoke(userId: String!, workspaceId: String!): Boolean!
revokeDocUserRoles(docId: String!, userIds: [String!]!): Boolean! revokeDocUserRoles(input: RevokeDocUserRolesInput!): Boolean!
revokeInviteLink(workspaceId: String!): Boolean! revokeInviteLink(workspaceId: String!): Boolean!
revokePage(pageId: String!, workspaceId: String!): Boolean! @deprecated(reason: "use revokePublicPage") revokePage(pageId: String!, workspaceId: String!): Boolean! @deprecated(reason: "use revokePublicPage")
revokePublicPage(pageId: String!, workspaceId: String!): WorkspacePage! revokePublicPage(pageId: String!, workspaceId: String!): WorkspacePage!
@@ -647,7 +648,8 @@ type Mutation {
"""Update a chat session""" """Update a chat session"""
updateCopilotSession(options: UpdateChatSessionInput!): String! updateCopilotSession(options: UpdateChatSessionInput!): String!
updateDocUserRole(docId: String!, role: DocRole!, userId: String!): Boolean! updateDocUserRole(input: UpdateDocUserRoleInput!): Boolean!
updatePageDefaultRole(input: UpdatePageDefaultRoleInput!): Boolean!
updateProfile(input: UpdateUserInput!): UserType! updateProfile(input: UpdateUserInput!): UserType!
"""update server runtime configurable setting""" """update server runtime configurable setting"""
@@ -802,6 +804,12 @@ type RemoveAvatar {
success: Boolean! success: Boolean!
} }
input RevokeDocUserRolesInput {
docId: String!
userIds: [String!]!
workspaceId: String!
}
type RuntimeConfigNotFoundDataType { type RuntimeConfigNotFoundDataType {
key: String! key: String!
} }
@@ -996,6 +1004,19 @@ input UpdateChatSessionInput {
sessionId: String! sessionId: String!
} }
input UpdateDocUserRoleInput {
docId: String!
role: DocRole!
userId: String!
workspaceId: String!
}
input UpdatePageDefaultRoleInput {
docId: String!
role: DocRole!
workspaceId: String!
}
input UpdateUserInput { input UpdateUserInput {
"""User name""" """User name"""
name: String name: String