mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 20:08:37 +00:00
refactor(server): separate page visibility from workspace permission (#4836)
This commit is contained in:
@@ -0,0 +1,61 @@
|
|||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "user_workspace_permissions" DROP CONSTRAINT "user_workspace_permissions_entity_id_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "user_workspace_permissions" DROP CONSTRAINT "user_workspace_permissions_workspace_id_fkey";
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "workspace_pages" (
|
||||||
|
"workspace_id" VARCHAR(36) NOT NULL,
|
||||||
|
"page_id" VARCHAR(36) NOT NULL,
|
||||||
|
"public" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"mode" SMALLINT NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
CONSTRAINT "workspace_pages_pkey" PRIMARY KEY ("workspace_id","page_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "workspace_user_permissions" (
|
||||||
|
"id" VARCHAR(36) NOT NULL,
|
||||||
|
"workspace_id" VARCHAR(36) NOT NULL,
|
||||||
|
"user_id" VARCHAR(36) NOT NULL,
|
||||||
|
"type" SMALLINT NOT NULL,
|
||||||
|
"accepted" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "workspace_user_permissions_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "workspace_page_user_permissions" (
|
||||||
|
"id" VARCHAR(36) NOT NULL,
|
||||||
|
"workspace_id" VARCHAR(36) NOT NULL,
|
||||||
|
"page_id" VARCHAR(36) NOT NULL,
|
||||||
|
"user_id" VARCHAR(36) NOT NULL,
|
||||||
|
"type" SMALLINT NOT NULL,
|
||||||
|
"accepted" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "workspace_page_user_permissions_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "workspace_user_permissions_workspace_id_user_id_key" ON "workspace_user_permissions"("workspace_id", "user_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "workspace_page_user_permissions_workspace_id_page_id_user_i_key" ON "workspace_page_user_permissions"("workspace_id", "page_id", "user_id");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "workspace_pages" ADD CONSTRAINT "workspace_pages_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "workspace_user_permissions" ADD CONSTRAINT "workspace_user_permissions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "workspace_user_permissions" ADD CONSTRAINT "workspace_user_permissions_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "workspace_page_user_permissions" ADD CONSTRAINT "workspace_page_user_permissions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "workspace_page_user_permissions" ADD CONSTRAINT "workspace_page_user_permissions_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -9,51 +9,108 @@ datasource db {
|
|||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id String @id @default(uuid()) @db.VarChar
|
||||||
|
name String
|
||||||
|
email String @unique
|
||||||
|
emailVerified DateTime? @map("email_verified")
|
||||||
|
// image field is for the next-auth
|
||||||
|
avatarUrl String? @map("avatar_url") @db.VarChar
|
||||||
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
|
/// Not available if user signed up through OAuth providers
|
||||||
|
password String? @db.VarChar
|
||||||
|
|
||||||
|
accounts Account[]
|
||||||
|
sessions Session[]
|
||||||
|
features UserFeatureGates[]
|
||||||
|
customer UserStripeCustomer?
|
||||||
|
subscription UserSubscription?
|
||||||
|
invoices UserInvoice[]
|
||||||
|
workspacePermissions WorkspaceUserPermission[]
|
||||||
|
pagePermissions WorkspacePageUserPermission[]
|
||||||
|
|
||||||
|
@@map("users")
|
||||||
|
}
|
||||||
|
|
||||||
model Workspace {
|
model Workspace {
|
||||||
id String @id @default(uuid()) @db.VarChar
|
id String @id @default(uuid()) @db.VarChar
|
||||||
public Boolean
|
public Boolean
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
users UserWorkspacePermission[]
|
|
||||||
|
pages WorkspacePage[]
|
||||||
|
permissions WorkspaceUserPermission[]
|
||||||
|
pagePermissions WorkspacePageUserPermission[]
|
||||||
|
|
||||||
@@map("workspaces")
|
@@map("workspaces")
|
||||||
}
|
}
|
||||||
|
|
||||||
model UserWorkspacePermission {
|
// Table for workspace page meta data
|
||||||
id String @id @default(uuid()) @db.VarChar
|
// NOTE:
|
||||||
workspaceId String @map("workspace_id") @db.VarChar
|
// We won't make sure every page has a corresponding record in this table.
|
||||||
subPageId String? @map("sub_page_id") @db.VarChar
|
// Only the ones that have ever changed will have records here,
|
||||||
userId String? @map("entity_id") @db.VarChar
|
// and for others we will make sure it's has a default value return in our bussiness logic.
|
||||||
|
model WorkspacePage {
|
||||||
|
workspaceId String @map("workspace_id") @db.VarChar(36)
|
||||||
|
pageId String @map("page_id") @db.VarChar(36)
|
||||||
|
public Boolean @default(false)
|
||||||
|
// Page/Edgeless
|
||||||
|
mode Int @default(0) @db.SmallInt
|
||||||
|
|
||||||
|
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@id([workspaceId, pageId])
|
||||||
|
@@map("workspace_pages")
|
||||||
|
}
|
||||||
|
|
||||||
|
// @deprecated, use WorkspaceUserPermission
|
||||||
|
model DeprecatedUserWorkspacePermission {
|
||||||
|
id String @id @default(uuid()) @db.VarChar
|
||||||
|
workspaceId String @map("workspace_id") @db.VarChar
|
||||||
|
subPageId String? @map("sub_page_id") @db.VarChar
|
||||||
|
userId String? @map("entity_id") @db.VarChar
|
||||||
/// Read/Write/Admin/Owner
|
/// Read/Write/Admin/Owner
|
||||||
type Int @db.SmallInt
|
type Int @db.SmallInt
|
||||||
/// Whether the permission invitation is accepted by the user
|
/// Whether the permission invitation is accepted by the user
|
||||||
accepted Boolean @default(false)
|
accepted Boolean @default(false)
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
||||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@unique([workspaceId, subPageId, userId])
|
@@unique([workspaceId, subPageId, userId])
|
||||||
@@map("user_workspace_permissions")
|
@@map("user_workspace_permissions")
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model WorkspaceUserPermission {
|
||||||
id String @id @default(uuid()) @db.VarChar
|
id String @id @default(uuid()) @db.VarChar(36)
|
||||||
name String
|
workspaceId String @map("workspace_id") @db.VarChar(36)
|
||||||
email String @unique
|
userId String @map("user_id") @db.VarChar(36)
|
||||||
emailVerified DateTime? @map("email_verified")
|
// Read/Write
|
||||||
// image field is for the next-auth
|
type Int @db.SmallInt
|
||||||
avatarUrl String? @map("avatar_url") @db.VarChar
|
/// Whether the permission invitation is accepted by the user
|
||||||
accounts Account[]
|
accepted Boolean @default(false)
|
||||||
sessions Session[]
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
workspaces UserWorkspacePermission[]
|
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
||||||
/// Not available if user signed up through OAuth providers
|
|
||||||
password String? @db.VarChar
|
|
||||||
features UserFeatureGates[]
|
|
||||||
customer UserStripeCustomer?
|
|
||||||
subscription UserSubscription?
|
|
||||||
invoices UserInvoice[]
|
|
||||||
|
|
||||||
@@map("users")
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([workspaceId, userId])
|
||||||
|
@@map("workspace_user_permissions")
|
||||||
|
}
|
||||||
|
|
||||||
|
model WorkspacePageUserPermission {
|
||||||
|
id String @id @default(uuid()) @db.VarChar(36)
|
||||||
|
workspaceId String @map("workspace_id") @db.VarChar(36)
|
||||||
|
pageId String @map("page_id") @db.VarChar(36)
|
||||||
|
userId String @map("user_id") @db.VarChar(36)
|
||||||
|
// Read/Write
|
||||||
|
type Int @db.SmallInt
|
||||||
|
/// Whether the permission invitation is accepted by the user
|
||||||
|
accepted Boolean @default(false)
|
||||||
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([workspaceId, pageId, userId])
|
||||||
|
@@map("workspace_page_user_permissions")
|
||||||
}
|
}
|
||||||
|
|
||||||
model UserFeatureGates {
|
model UserFeatureGates {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { join } from 'node:path';
|
|||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import { camelCase, snakeCase, upperFirst } from 'lodash-es';
|
import { camelCase, kebabCase, upperFirst } from 'lodash-es';
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandRunner,
|
CommandRunner,
|
||||||
@@ -45,7 +45,7 @@ export class CreateCommand extends CommandRunner {
|
|||||||
|
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
const content = this.createScript(upperFirst(camelCase(name)) + timestamp);
|
const content = this.createScript(upperFirst(camelCase(name)) + timestamp);
|
||||||
const fileName = `${timestamp}-${snakeCase(name)}.ts`;
|
const fileName = `${timestamp}-${kebabCase(name)}.ts`;
|
||||||
const filePath = join(
|
const filePath = join(
|
||||||
fileURLToPath(import.meta.url),
|
fileURLToPath(import.meta.url),
|
||||||
'../../migrations',
|
'../../migrations',
|
||||||
@@ -54,6 +54,7 @@ export class CreateCommand extends CommandRunner {
|
|||||||
|
|
||||||
this.logger.log(`Creating ${fileName}...`);
|
this.logger.log(`Creating ${fileName}...`);
|
||||||
writeFileSync(filePath, content);
|
writeFileSync(filePath, content);
|
||||||
|
this.logger.log('Migration file created at', filePath);
|
||||||
this.logger.log('Done');
|
this.logger.log('Done');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ export class RunCommand extends CommandRunner {
|
|||||||
done.push(migration);
|
done.push(migration);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.error('Failed to run data migration', e);
|
this.logger.error('Failed to run data migration', e);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { PrismaService } from '../../prisma';
|
||||||
|
|
||||||
|
export class PagePermission1699005339766 {
|
||||||
|
// do the migration
|
||||||
|
static async up(db: PrismaService) {
|
||||||
|
const turn = 0;
|
||||||
|
const lastTurnCount = 50;
|
||||||
|
const done = new Set<string>();
|
||||||
|
|
||||||
|
while (lastTurnCount === 50) {
|
||||||
|
const workspaces = await db.workspace.findMany({
|
||||||
|
skip: turn * 50,
|
||||||
|
take: 50,
|
||||||
|
orderBy: {
|
||||||
|
createdAt: 'asc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const workspace of workspaces) {
|
||||||
|
if (done.has(workspace.id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldPermissions =
|
||||||
|
await db.deprecatedUserWorkspacePermission.findMany({
|
||||||
|
where: {
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const oldPermission of oldPermissions) {
|
||||||
|
// mark subpage public
|
||||||
|
if (oldPermission.subPageId) {
|
||||||
|
const existed = await db.workspacePage.findUnique({
|
||||||
|
where: {
|
||||||
|
workspaceId_pageId: {
|
||||||
|
workspaceId: oldPermission.workspaceId,
|
||||||
|
pageId: oldPermission.subPageId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!existed) {
|
||||||
|
await db.workspacePage.create({
|
||||||
|
select: null,
|
||||||
|
data: {
|
||||||
|
workspaceId: oldPermission.workspaceId,
|
||||||
|
pageId: oldPermission.subPageId,
|
||||||
|
public: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (oldPermission.userId) {
|
||||||
|
// workspace user permission
|
||||||
|
const existed = await db.workspaceUserPermission.findUnique({
|
||||||
|
where: {
|
||||||
|
id: oldPermission.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existed) {
|
||||||
|
await db.workspaceUserPermission.create({
|
||||||
|
select: null,
|
||||||
|
data: {
|
||||||
|
// this id is used at invite email, should keep
|
||||||
|
id: oldPermission.id,
|
||||||
|
workspaceId: oldPermission.workspaceId,
|
||||||
|
userId: oldPermission.userId,
|
||||||
|
type: oldPermission.type,
|
||||||
|
accepted: oldPermission.accepted,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// ignore wrong data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
done.add(workspace.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// revert the migration
|
||||||
|
static async down() {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -81,7 +81,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
|||||||
@MessageBody() workspaceId: string,
|
@MessageBody() workspaceId: string,
|
||||||
@ConnectedSocket() client: Socket
|
@ConnectedSocket() client: Socket
|
||||||
): Promise<EventResponse<{ clientId: string }>> {
|
): Promise<EventResponse<{ clientId: string }>> {
|
||||||
const canWrite = await this.permissions.tryCheck(
|
const canWrite = await this.permissions.tryCheckWorkspace(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
user.id,
|
user.id,
|
||||||
Permission.Write
|
Permission.Write
|
||||||
@@ -181,7 +181,10 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
|||||||
}
|
}
|
||||||
): Promise<{ missing: string; state?: string } | false> {
|
): Promise<{ missing: string; state?: string } | false> {
|
||||||
if (!client.rooms.has(workspaceId)) {
|
if (!client.rooms.has(workspaceId)) {
|
||||||
const canRead = await this.permissions.tryCheck(workspaceId, user.id);
|
const canRead = await this.permissions.tryCheckWorkspace(
|
||||||
|
workspaceId,
|
||||||
|
user.id
|
||||||
|
);
|
||||||
if (!canRead) {
|
if (!canRead) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -266,7 +269,10 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
|||||||
}
|
}
|
||||||
): Promise<EventResponse<{ missing: string; state?: string }>> {
|
): Promise<EventResponse<{ missing: string; state?: string }>> {
|
||||||
if (!client.rooms.has(workspaceId)) {
|
if (!client.rooms.has(workspaceId)) {
|
||||||
const canRead = await this.permissions.tryCheck(workspaceId, user.id);
|
const canRead = await this.permissions.tryCheckWorkspace(
|
||||||
|
workspaceId,
|
||||||
|
user.id
|
||||||
|
);
|
||||||
if (!canRead) {
|
if (!canRead) {
|
||||||
return {
|
return {
|
||||||
error: new AccessDeniedError(workspaceId),
|
error: new AccessDeniedError(workspaceId),
|
||||||
|
|||||||
@@ -4,12 +4,17 @@ import { DocModule } from '../doc';
|
|||||||
import { UsersService } from '../users';
|
import { UsersService } from '../users';
|
||||||
import { WorkspacesController } from './controller';
|
import { WorkspacesController } from './controller';
|
||||||
import { PermissionService } from './permission';
|
import { PermissionService } from './permission';
|
||||||
import { WorkspaceResolver } from './resolver';
|
import { PagePermissionResolver, WorkspaceResolver } from './resolver';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [DocModule.forFeature()],
|
imports: [DocModule.forFeature()],
|
||||||
controllers: [WorkspacesController],
|
controllers: [WorkspacesController],
|
||||||
providers: [WorkspaceResolver, PermissionService, UsersService],
|
providers: [
|
||||||
|
WorkspaceResolver,
|
||||||
|
PermissionService,
|
||||||
|
UsersService,
|
||||||
|
PagePermissionResolver,
|
||||||
|
],
|
||||||
exports: [PermissionService],
|
exports: [PermissionService],
|
||||||
})
|
})
|
||||||
export class WorkspaceModule {}
|
export class WorkspaceModule {}
|
||||||
|
|||||||
@@ -4,15 +4,20 @@ import { Prisma } from '@prisma/client';
|
|||||||
import { PrismaService } from '../../prisma';
|
import { PrismaService } from '../../prisma';
|
||||||
import { Permission } from './types';
|
import { Permission } from './types';
|
||||||
|
|
||||||
|
export enum PublicPageMode {
|
||||||
|
Page,
|
||||||
|
Edgeless,
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PermissionService {
|
export class PermissionService {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
/// Start regin: workspace permission
|
||||||
async get(ws: string, user: string) {
|
async get(ws: string, user: string) {
|
||||||
const data = await this.prisma.userWorkspacePermission.findFirst({
|
const data = await this.prisma.workspaceUserPermission.findFirst({
|
||||||
where: {
|
where: {
|
||||||
workspaceId: ws,
|
workspaceId: ws,
|
||||||
subPageId: null,
|
|
||||||
userId: user,
|
userId: user,
|
||||||
accepted: true,
|
accepted: true,
|
||||||
},
|
},
|
||||||
@@ -22,7 +27,7 @@ export class PermissionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getWorkspaceOwner(workspaceId: string) {
|
async getWorkspaceOwner(workspaceId: string) {
|
||||||
return this.prisma.userWorkspacePermission.findFirstOrThrow({
|
return this.prisma.workspaceUserPermission.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
type: Permission.Owner,
|
type: Permission.Owner,
|
||||||
@@ -34,7 +39,7 @@ export class PermissionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async tryGetWorkspaceOwner(workspaceId: string) {
|
async tryGetWorkspaceOwner(workspaceId: string) {
|
||||||
return this.prisma.userWorkspacePermission.findFirst({
|
return this.prisma.workspaceUserPermission.findFirst({
|
||||||
where: {
|
where: {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
type: Permission.Owner,
|
type: Permission.Owner,
|
||||||
@@ -46,77 +51,76 @@ export class PermissionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async isAccessible(ws: string, id: string, user?: string): Promise<boolean> {
|
async isAccessible(ws: string, id: string, user?: string): Promise<boolean> {
|
||||||
if (user) {
|
// workspace
|
||||||
const hasPermission = await this.tryCheck(ws, user);
|
if (ws === id) {
|
||||||
if (hasPermission) return true;
|
return this.tryCheckWorkspace(ws, user, Permission.Read);
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if this is a public workspace
|
return this.tryCheckPage(ws, id, user);
|
||||||
const count = await this.prisma.workspace.count({
|
|
||||||
where: { id: ws, public: true },
|
|
||||||
});
|
|
||||||
if (count > 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check whether this is a public subpage
|
|
||||||
const workspace = await this.prisma.userWorkspacePermission.findMany({
|
|
||||||
where: {
|
|
||||||
workspaceId: ws,
|
|
||||||
userId: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const subpages = workspace
|
|
||||||
.map(ws => ws.subPageId)
|
|
||||||
.filter((v): v is string => !!v);
|
|
||||||
if (subpages.length > 0 && ws === id) {
|
|
||||||
// rootDoc is always accessible when there is a public subpage
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
// check if this is a public subpage
|
|
||||||
return subpages.some(subpage => id === subpage);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async check(
|
async checkWorkspace(
|
||||||
ws: string,
|
ws: string,
|
||||||
user: string,
|
user?: string,
|
||||||
permission: Permission = Permission.Read
|
permission: Permission = Permission.Read
|
||||||
) {
|
) {
|
||||||
if (!(await this.tryCheck(ws, user, permission))) {
|
if (!(await this.tryCheckWorkspace(ws, user, permission))) {
|
||||||
throw new ForbiddenException('Permission denied');
|
throw new ForbiddenException('Permission denied');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async tryCheck(
|
async tryCheckWorkspace(
|
||||||
ws: string,
|
ws: string,
|
||||||
user: string,
|
user?: string,
|
||||||
permission: Permission = Permission.Read
|
permission: Permission = Permission.Read
|
||||||
) {
|
) {
|
||||||
// If the permission is read, we should check if the workspace is public
|
// If the permission is read, we should check if the workspace is public
|
||||||
if (permission === Permission.Read) {
|
if (permission === Permission.Read) {
|
||||||
const data = await this.prisma.workspace.count({
|
const count = await this.prisma.workspace.count({
|
||||||
where: { id: ws, public: true },
|
where: { id: ws, public: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data > 0) {
|
// workspace is public
|
||||||
|
// accessible
|
||||||
|
if (count > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicPage = await this.prisma.workspacePage.findFirst({
|
||||||
|
select: {
|
||||||
|
pageId: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
workspaceId: ws,
|
||||||
|
public: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// has any public pages
|
||||||
|
if (publicPage) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await this.prisma.userWorkspacePermission.count({
|
if (user) {
|
||||||
where: {
|
// normally check if the user has the permission
|
||||||
workspaceId: ws,
|
const count = await this.prisma.workspaceUserPermission.count({
|
||||||
subPageId: null,
|
where: {
|
||||||
userId: user,
|
workspaceId: ws,
|
||||||
accepted: true,
|
userId: user,
|
||||||
type: {
|
accepted: true,
|
||||||
gte: permission,
|
type: {
|
||||||
|
gte: permission,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return data > 0;
|
return count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// unsigned in, workspace is not public
|
||||||
|
// unaccessible
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async grant(
|
async grant(
|
||||||
@@ -124,10 +128,9 @@ export class PermissionService {
|
|||||||
user: string,
|
user: string,
|
||||||
permission: Permission = Permission.Read
|
permission: Permission = Permission.Read
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const data = await this.prisma.userWorkspacePermission.findFirst({
|
const data = await this.prisma.workspaceUserPermission.findFirst({
|
||||||
where: {
|
where: {
|
||||||
workspaceId: ws,
|
workspaceId: ws,
|
||||||
subPageId: null,
|
|
||||||
userId: user,
|
userId: user,
|
||||||
accepted: true,
|
accepted: true,
|
||||||
},
|
},
|
||||||
@@ -136,9 +139,12 @@ export class PermissionService {
|
|||||||
if (data) {
|
if (data) {
|
||||||
const [p] = await this.prisma.$transaction(
|
const [p] = await this.prisma.$transaction(
|
||||||
[
|
[
|
||||||
this.prisma.userWorkspacePermission.update({
|
this.prisma.workspaceUserPermission.update({
|
||||||
where: {
|
where: {
|
||||||
id: data.id,
|
workspaceId_userId: {
|
||||||
|
workspaceId: ws,
|
||||||
|
userId: user,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
type: permission,
|
type: permission,
|
||||||
@@ -147,7 +153,7 @@ export class PermissionService {
|
|||||||
|
|
||||||
// If the new permission is owner, we need to revoke old owner
|
// If the new permission is owner, we need to revoke old owner
|
||||||
permission === Permission.Owner
|
permission === Permission.Owner
|
||||||
? this.prisma.userWorkspacePermission.updateMany({
|
? this.prisma.workspaceUserPermission.updateMany({
|
||||||
where: {
|
where: {
|
||||||
workspaceId: ws,
|
workspaceId: ws,
|
||||||
type: Permission.Owner,
|
type: Permission.Owner,
|
||||||
@@ -166,11 +172,10 @@ export class PermissionService {
|
|||||||
return p.id;
|
return p.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.prisma.userWorkspacePermission
|
return this.prisma.workspaceUserPermission
|
||||||
.create({
|
.create({
|
||||||
data: {
|
data: {
|
||||||
workspaceId: ws,
|
workspaceId: ws,
|
||||||
subPageId: null,
|
|
||||||
userId: user,
|
userId: user,
|
||||||
type: permission,
|
type: permission,
|
||||||
},
|
},
|
||||||
@@ -178,10 +183,10 @@ export class PermissionService {
|
|||||||
.then(p => p.id);
|
.then(p => p.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getInvitationById(inviteId: string, workspaceId: string) {
|
async getWorkspaceInvitation(invitationId: string, workspaceId: string) {
|
||||||
return this.prisma.userWorkspacePermission.findUniqueOrThrow({
|
return this.prisma.workspaceUserPermission.findUniqueOrThrow({
|
||||||
where: {
|
where: {
|
||||||
id: inviteId,
|
id: invitationId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
@@ -190,11 +195,11 @@ export class PermissionService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async acceptById(ws: string, id: string) {
|
async acceptWorkspaceInvitation(invitationId: string, workspaceId: string) {
|
||||||
const result = await this.prisma.userWorkspacePermission.updateMany({
|
const result = await this.prisma.workspaceUserPermission.updateMany({
|
||||||
where: {
|
where: {
|
||||||
id,
|
id: invitationId,
|
||||||
workspaceId: ws,
|
workspaceId: workspaceId,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
accepted: true,
|
accepted: true,
|
||||||
@@ -204,27 +209,10 @@ export class PermissionService {
|
|||||||
return result.count > 0;
|
return result.count > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async accept(ws: string, user: string) {
|
async revokeWorkspace(ws: string, user: string) {
|
||||||
const result = await this.prisma.userWorkspacePermission.updateMany({
|
const result = await this.prisma.workspaceUserPermission.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
workspaceId: ws,
|
workspaceId: ws,
|
||||||
subPageId: null,
|
|
||||||
userId: user,
|
|
||||||
accepted: false,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
accepted: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return result.count > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async revoke(ws: string, user: string) {
|
|
||||||
const result = await this.prisma.userWorkspacePermission.deleteMany({
|
|
||||||
where: {
|
|
||||||
workspaceId: ws,
|
|
||||||
subPageId: null,
|
|
||||||
userId: user,
|
userId: user,
|
||||||
type: {
|
type: {
|
||||||
// We shouldn't revoke owner permission, should auto deleted by workspace/user delete cascading
|
// We shouldn't revoke owner permission, should auto deleted by workspace/user delete cascading
|
||||||
@@ -235,56 +223,177 @@ export class PermissionService {
|
|||||||
|
|
||||||
return result.count > 0;
|
return result.count > 0;
|
||||||
}
|
}
|
||||||
|
/// End regin: workspace permission
|
||||||
|
|
||||||
async isPageAccessible(ws: string, page: string, user?: string) {
|
/// Start regin: page permission
|
||||||
const data = await this.prisma.userWorkspacePermission.findFirst({
|
async checkPagePermission(
|
||||||
|
ws: string,
|
||||||
|
page: string,
|
||||||
|
user?: string,
|
||||||
|
permission = Permission.Read
|
||||||
|
) {
|
||||||
|
if (!(await this.tryCheckPage(ws, page, user, permission))) {
|
||||||
|
throw new ForbiddenException('Permission denied');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async tryCheckPage(
|
||||||
|
ws: string,
|
||||||
|
page: string,
|
||||||
|
user?: string,
|
||||||
|
permission = Permission.Read
|
||||||
|
) {
|
||||||
|
// check whether page is public
|
||||||
|
const count = await this.prisma.workspacePage.count({
|
||||||
where: {
|
where: {
|
||||||
workspaceId: ws,
|
workspaceId: ws,
|
||||||
subPageId: page,
|
pageId: page,
|
||||||
userId: user,
|
public: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return data?.accepted || false;
|
// page is public
|
||||||
|
// accessible
|
||||||
|
if (count > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
const count = await this.prisma.workspacePageUserPermission.count({
|
||||||
|
where: {
|
||||||
|
workspaceId: ws,
|
||||||
|
pageId: page,
|
||||||
|
userId: user,
|
||||||
|
accepted: true,
|
||||||
|
type: {
|
||||||
|
gte: permission,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// page shared to user
|
||||||
|
// accessible
|
||||||
|
if (count > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check whether user has workspace related permission
|
||||||
|
return this.tryCheckWorkspace(ws, user, permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
async publishPage(ws: string, page: string, mode = PublicPageMode.Page) {
|
||||||
|
return this.prisma.workspacePage.upsert({
|
||||||
|
where: {
|
||||||
|
workspaceId_pageId: {
|
||||||
|
workspaceId: ws,
|
||||||
|
pageId: page,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
mode,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
workspaceId: ws,
|
||||||
|
pageId: page,
|
||||||
|
mode,
|
||||||
|
public: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async revokePublicPage(ws: string, page: string) {
|
||||||
|
const workspacePage = await this.prisma.workspacePage.findUnique({
|
||||||
|
where: {
|
||||||
|
workspaceId_pageId: {
|
||||||
|
workspaceId: ws,
|
||||||
|
pageId: page,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!workspacePage) {
|
||||||
|
throw new Error('Page is not public');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.prisma.workspacePage.update({
|
||||||
|
where: {
|
||||||
|
workspaceId_pageId: {
|
||||||
|
workspaceId: ws,
|
||||||
|
pageId: page,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
public: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async grantPage(
|
async grantPage(
|
||||||
ws: string,
|
ws: string,
|
||||||
page: string,
|
page: string,
|
||||||
user?: string,
|
user: string,
|
||||||
permission: Permission = Permission.Read
|
permission: Permission = Permission.Read
|
||||||
) {
|
) {
|
||||||
const data = await this.prisma.userWorkspacePermission.findFirst({
|
const data = await this.prisma.workspacePageUserPermission.findFirst({
|
||||||
where: {
|
where: {
|
||||||
workspaceId: ws,
|
workspaceId: ws,
|
||||||
subPageId: page,
|
pageId: page,
|
||||||
userId: user,
|
userId: user,
|
||||||
|
accepted: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
return data.accepted;
|
const [p] = await this.prisma.$transaction(
|
||||||
|
[
|
||||||
|
this.prisma.workspacePageUserPermission.update({
|
||||||
|
where: {
|
||||||
|
id: data.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: permission,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
// If the new permission is owner, we need to revoke old owner
|
||||||
|
permission === Permission.Owner
|
||||||
|
? this.prisma.workspacePageUserPermission.updateMany({
|
||||||
|
where: {
|
||||||
|
workspaceId: ws,
|
||||||
|
pageId: page,
|
||||||
|
type: Permission.Owner,
|
||||||
|
userId: {
|
||||||
|
not: user,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Permission.Admin,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: null,
|
||||||
|
].filter(Boolean) as Prisma.PrismaPromise<any>[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return p.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.prisma.userWorkspacePermission
|
return this.prisma.workspacePageUserPermission
|
||||||
.create({
|
.create({
|
||||||
data: {
|
data: {
|
||||||
workspaceId: ws,
|
workspaceId: ws,
|
||||||
subPageId: page,
|
pageId: page,
|
||||||
userId: user,
|
userId: user,
|
||||||
// if provide user id, user need to accept the invitation
|
|
||||||
accepted: user ? false : true,
|
|
||||||
type: permission,
|
type: permission,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(ret => ret.accepted);
|
.then(p => p.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async revokePage(ws: string, page: string, user?: string) {
|
async revokePage(ws: string, page: string, user: string) {
|
||||||
const result = await this.prisma.userWorkspacePermission.deleteMany({
|
const result = await this.prisma.workspacePageUserPermission.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
workspaceId: ws,
|
workspaceId: ws,
|
||||||
subPageId: page,
|
pageId: page,
|
||||||
userId: user,
|
userId: user,
|
||||||
type: {
|
type: {
|
||||||
// We shouldn't revoke owner permission, should auto deleted by workspace/user delete cascading
|
// We shouldn't revoke owner permission, should auto deleted by workspace/user delete cascading
|
||||||
@@ -295,4 +404,5 @@ export class PermissionService {
|
|||||||
|
|
||||||
return result.count > 0;
|
return result.count > 0;
|
||||||
}
|
}
|
||||||
|
/// End regin: page permission
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ import {
|
|||||||
ResolveField,
|
ResolveField,
|
||||||
Resolver,
|
Resolver,
|
||||||
} from '@nestjs/graphql';
|
} from '@nestjs/graphql';
|
||||||
import type { User, Workspace } from '@prisma/client';
|
import type {
|
||||||
|
User,
|
||||||
|
Workspace,
|
||||||
|
WorkspacePage as PrismaWorkspacePage,
|
||||||
|
} from '@prisma/client';
|
||||||
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
|
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
|
||||||
import { applyUpdate, Doc } from 'yjs';
|
import { applyUpdate, Doc } from 'yjs';
|
||||||
|
|
||||||
@@ -39,7 +43,7 @@ import { MailService } from '../auth/mailer';
|
|||||||
import { AuthService } from '../auth/service';
|
import { AuthService } from '../auth/service';
|
||||||
import { UsersService } from '../users';
|
import { UsersService } from '../users';
|
||||||
import { UserType } from '../users/resolver';
|
import { UserType } from '../users/resolver';
|
||||||
import { PermissionService } from './permission';
|
import { PermissionService, PublicPageMode } from './permission';
|
||||||
import { Permission } from './types';
|
import { Permission } from './types';
|
||||||
import { defaultWorkspaceAvatar } from './utils';
|
import { defaultWorkspaceAvatar } from './utils';
|
||||||
|
|
||||||
@@ -172,28 +176,11 @@ export class WorkspaceResolver {
|
|||||||
complexity: 2,
|
complexity: 2,
|
||||||
})
|
})
|
||||||
memberCount(@Parent() workspace: WorkspaceType) {
|
memberCount(@Parent() workspace: WorkspaceType) {
|
||||||
return this.prisma.userWorkspacePermission.count({
|
return this.prisma.workspaceUserPermission.count({
|
||||||
where: {
|
|
||||||
workspaceId: workspace.id,
|
|
||||||
userId: {
|
|
||||||
not: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@ResolveField(() => [String], {
|
|
||||||
description: 'Shared pages of workspace',
|
|
||||||
complexity: 2,
|
|
||||||
})
|
|
||||||
async sharedPages(@Parent() workspace: WorkspaceType) {
|
|
||||||
const data = await this.prisma.userWorkspacePermission.findMany({
|
|
||||||
where: {
|
where: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return data.map(item => item.subPageId).filter(Boolean);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ResolveField(() => UserType, {
|
@ResolveField(() => UserType, {
|
||||||
@@ -215,12 +202,9 @@ export class WorkspaceResolver {
|
|||||||
@Args('skip', { type: () => Int, nullable: true }) skip?: number,
|
@Args('skip', { type: () => Int, nullable: true }) skip?: number,
|
||||||
@Args('take', { type: () => Int, nullable: true }) take?: number
|
@Args('take', { type: () => Int, nullable: true }) take?: number
|
||||||
) {
|
) {
|
||||||
const data = await this.prisma.userWorkspacePermission.findMany({
|
const data = await this.prisma.workspaceUserPermission.findMany({
|
||||||
where: {
|
where: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
userId: {
|
|
||||||
not: null,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
skip,
|
skip,
|
||||||
take: take || 8,
|
take: take || 8,
|
||||||
@@ -265,7 +249,7 @@ export class WorkspaceResolver {
|
|||||||
complexity: 2,
|
complexity: 2,
|
||||||
})
|
})
|
||||||
async workspaces(@CurrentUser() user: User) {
|
async workspaces(@CurrentUser() user: User) {
|
||||||
const data = await this.prisma.userWorkspacePermission.findMany({
|
const data = await this.prisma.workspaceUserPermission.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
accepted: true,
|
accepted: true,
|
||||||
@@ -309,7 +293,7 @@ export class WorkspaceResolver {
|
|||||||
description: 'Get workspace by id',
|
description: 'Get workspace by id',
|
||||||
})
|
})
|
||||||
async workspace(@CurrentUser() user: UserType, @Args('id') id: string) {
|
async workspace(@CurrentUser() user: UserType, @Args('id') id: string) {
|
||||||
await this.permissions.check(id, user.id);
|
await this.permissions.checkWorkspace(id, user.id);
|
||||||
const workspace = await this.prisma.workspace.findUnique({ where: { id } });
|
const workspace = await this.prisma.workspace.findUnique({ where: { id } });
|
||||||
|
|
||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
@@ -343,7 +327,7 @@ export class WorkspaceResolver {
|
|||||||
const workspace = await this.prisma.workspace.create({
|
const workspace = await this.prisma.workspace.create({
|
||||||
data: {
|
data: {
|
||||||
public: false,
|
public: false,
|
||||||
users: {
|
permissions: {
|
||||||
create: {
|
create: {
|
||||||
type: Permission.Owner,
|
type: Permission.Owner,
|
||||||
user: {
|
user: {
|
||||||
@@ -378,7 +362,7 @@ export class WorkspaceResolver {
|
|||||||
@Args({ name: 'input', type: () => UpdateWorkspaceInput })
|
@Args({ name: 'input', type: () => UpdateWorkspaceInput })
|
||||||
{ id, ...updates }: UpdateWorkspaceInput
|
{ id, ...updates }: UpdateWorkspaceInput
|
||||||
) {
|
) {
|
||||||
await this.permissions.check(id, user.id, Permission.Admin);
|
await this.permissions.checkWorkspace(id, user.id, Permission.Admin);
|
||||||
|
|
||||||
return this.prisma.workspace.update({
|
return this.prisma.workspace.update({
|
||||||
where: {
|
where: {
|
||||||
@@ -390,7 +374,7 @@ export class WorkspaceResolver {
|
|||||||
|
|
||||||
@Mutation(() => Boolean)
|
@Mutation(() => Boolean)
|
||||||
async deleteWorkspace(@CurrentUser() user: UserType, @Args('id') id: string) {
|
async deleteWorkspace(@CurrentUser() user: UserType, @Args('id') id: string) {
|
||||||
await this.permissions.check(id, user.id, Permission.Owner);
|
await this.permissions.checkWorkspace(id, user.id, Permission.Owner);
|
||||||
|
|
||||||
await this.prisma.workspace.delete({
|
await this.prisma.workspace.delete({
|
||||||
where: {
|
where: {
|
||||||
@@ -422,7 +406,11 @@ export class WorkspaceResolver {
|
|||||||
@Args('permission', { type: () => Permission }) permission: Permission,
|
@Args('permission', { type: () => Permission }) permission: Permission,
|
||||||
@Args('sendInviteMail', { nullable: true }) sendInviteMail: boolean
|
@Args('sendInviteMail', { nullable: true }) sendInviteMail: boolean
|
||||||
) {
|
) {
|
||||||
await this.permissions.check(workspaceId, user.id, Permission.Admin);
|
await this.permissions.checkWorkspace(
|
||||||
|
workspaceId,
|
||||||
|
user.id,
|
||||||
|
Permission.Admin
|
||||||
|
);
|
||||||
|
|
||||||
if (permission === Permission.Owner) {
|
if (permission === Permission.Owner) {
|
||||||
throw new ForbiddenException('Cannot change owner');
|
throw new ForbiddenException('Cannot change owner');
|
||||||
@@ -431,7 +419,7 @@ export class WorkspaceResolver {
|
|||||||
const target = await this.users.findUserByEmail(email);
|
const target = await this.users.findUserByEmail(email);
|
||||||
|
|
||||||
if (target) {
|
if (target) {
|
||||||
const originRecord = await this.prisma.userWorkspacePermission.findFirst({
|
const originRecord = await this.prisma.workspaceUserPermission.findFirst({
|
||||||
where: {
|
where: {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
userId: target.id,
|
userId: target.id,
|
||||||
@@ -463,7 +451,10 @@ export class WorkspaceResolver {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const ret = await this.permissions.revoke(workspaceId, target.id);
|
const ret = await this.permissions.revokeWorkspace(
|
||||||
|
workspaceId,
|
||||||
|
target.id
|
||||||
|
);
|
||||||
|
|
||||||
if (!ret) {
|
if (!ret) {
|
||||||
this.logger.fatal(
|
this.logger.fatal(
|
||||||
@@ -502,7 +493,10 @@ export class WorkspaceResolver {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const ret = await this.permissions.revoke(workspaceId, user.id);
|
const ret = await this.permissions.revokeWorkspace(
|
||||||
|
workspaceId,
|
||||||
|
user.id
|
||||||
|
);
|
||||||
|
|
||||||
if (!ret) {
|
if (!ret) {
|
||||||
this.logger.fatal(
|
this.logger.fatal(
|
||||||
@@ -532,7 +526,7 @@ export class WorkspaceResolver {
|
|||||||
description: 'Update workspace',
|
description: 'Update workspace',
|
||||||
})
|
})
|
||||||
async getInviteInfo(@Args('inviteId') inviteId: string) {
|
async getInviteInfo(@Args('inviteId') inviteId: string) {
|
||||||
const workspaceId = await this.prisma.userWorkspacePermission
|
const workspaceId = await this.prisma.workspaceUserPermission
|
||||||
.findUniqueOrThrow({
|
.findUniqueOrThrow({
|
||||||
where: {
|
where: {
|
||||||
id: inviteId,
|
id: inviteId,
|
||||||
@@ -556,7 +550,7 @@ export class WorkspaceResolver {
|
|||||||
const metaJSON = doc.getMap('meta').toJSON();
|
const metaJSON = doc.getMap('meta').toJSON();
|
||||||
|
|
||||||
const owner = await this.permissions.getWorkspaceOwner(workspaceId);
|
const owner = await this.permissions.getWorkspaceOwner(workspaceId);
|
||||||
const invitee = await this.permissions.getInvitationById(
|
const invitee = await this.permissions.getWorkspaceInvitation(
|
||||||
inviteId,
|
inviteId,
|
||||||
workspaceId
|
workspaceId
|
||||||
);
|
);
|
||||||
@@ -588,9 +582,13 @@ export class WorkspaceResolver {
|
|||||||
@Args('workspaceId') workspaceId: string,
|
@Args('workspaceId') workspaceId: string,
|
||||||
@Args('userId') userId: string
|
@Args('userId') userId: string
|
||||||
) {
|
) {
|
||||||
await this.permissions.check(workspaceId, user.id, Permission.Admin);
|
await this.permissions.checkWorkspace(
|
||||||
|
workspaceId,
|
||||||
|
user.id,
|
||||||
|
Permission.Admin
|
||||||
|
);
|
||||||
|
|
||||||
return this.permissions.revoke(workspaceId, userId);
|
return this.permissions.revokeWorkspace(workspaceId, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => Boolean)
|
@Mutation(() => Boolean)
|
||||||
@@ -619,15 +617,7 @@ export class WorkspaceResolver {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.permissions.acceptById(workspaceId, inviteId);
|
return this.permissions.acceptWorkspaceInvitation(inviteId, workspaceId);
|
||||||
}
|
|
||||||
|
|
||||||
@Mutation(() => Boolean)
|
|
||||||
async acceptInvite(
|
|
||||||
@CurrentUser() user: UserType,
|
|
||||||
@Args('workspaceId') workspaceId: string
|
|
||||||
) {
|
|
||||||
return this.permissions.accept(workspaceId, user.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => Boolean)
|
@Mutation(() => Boolean)
|
||||||
@@ -637,7 +627,7 @@ export class WorkspaceResolver {
|
|||||||
@Args('workspaceName') workspaceName: string,
|
@Args('workspaceName') workspaceName: string,
|
||||||
@Args('sendLeaveMail', { nullable: true }) sendLeaveMail: boolean
|
@Args('sendLeaveMail', { nullable: true }) sendLeaveMail: boolean
|
||||||
) {
|
) {
|
||||||
await this.permissions.check(workspaceId, user.id);
|
await this.permissions.checkWorkspace(workspaceId, user.id);
|
||||||
|
|
||||||
const owner = await this.permissions.getWorkspaceOwner(workspaceId);
|
const owner = await this.permissions.getWorkspaceOwner(workspaceId);
|
||||||
|
|
||||||
@@ -654,50 +644,7 @@ export class WorkspaceResolver {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.permissions.revoke(workspaceId, user.id);
|
return this.permissions.revokeWorkspace(workspaceId, user.id);
|
||||||
}
|
|
||||||
|
|
||||||
@Mutation(() => Boolean)
|
|
||||||
async sharePage(
|
|
||||||
@CurrentUser() user: UserType,
|
|
||||||
@Args('workspaceId') workspaceId: string,
|
|
||||||
@Args('pageId') pageId: string
|
|
||||||
) {
|
|
||||||
const docId = new DocID(pageId, workspaceId);
|
|
||||||
|
|
||||||
if (docId.isWorkspace) {
|
|
||||||
throw new ForbiddenException('Expect page not to be workspace');
|
|
||||||
}
|
|
||||||
|
|
||||||
const userWorkspace = await this.prisma.userWorkspacePermission.findFirst({
|
|
||||||
where: {
|
|
||||||
userId: user.id,
|
|
||||||
workspaceId: docId.workspace,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userWorkspace?.accepted) {
|
|
||||||
throw new ForbiddenException('Permission denied');
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.permissions.grantPage(docId.workspace, docId.guid);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Mutation(() => Boolean)
|
|
||||||
async revokePage(
|
|
||||||
@CurrentUser() user: UserType,
|
|
||||||
@Args('workspaceId') workspaceId: string,
|
|
||||||
@Args('pageId') pageId: string
|
|
||||||
) {
|
|
||||||
const docId = new DocID(pageId, workspaceId);
|
|
||||||
|
|
||||||
if (docId.isWorkspace) {
|
|
||||||
throw new ForbiddenException('Expect page not to be workspace');
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.permissions.check(docId.workspace, user.id, Permission.Admin);
|
|
||||||
|
|
||||||
return this.permissions.revokePage(docId.workspace, docId.guid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query(() => [String], {
|
@Query(() => [String], {
|
||||||
@@ -707,7 +654,7 @@ export class WorkspaceResolver {
|
|||||||
@CurrentUser() user: UserType,
|
@CurrentUser() user: UserType,
|
||||||
@Args('workspaceId') workspaceId: string
|
@Args('workspaceId') workspaceId: string
|
||||||
) {
|
) {
|
||||||
await this.permissions.check(workspaceId, user.id);
|
await this.permissions.checkWorkspace(workspaceId, user.id);
|
||||||
|
|
||||||
return this.storage.listBlobs(workspaceId);
|
return this.storage.listBlobs(workspaceId);
|
||||||
}
|
}
|
||||||
@@ -717,14 +664,14 @@ export class WorkspaceResolver {
|
|||||||
@CurrentUser() user: UserType,
|
@CurrentUser() user: UserType,
|
||||||
@Args('workspaceId') workspaceId: string
|
@Args('workspaceId') workspaceId: string
|
||||||
) {
|
) {
|
||||||
await this.permissions.check(workspaceId, user.id);
|
await this.permissions.checkWorkspace(workspaceId, user.id);
|
||||||
|
|
||||||
return this.storage.blobsSize([workspaceId]).then(size => ({ size }));
|
return this.storage.blobsSize([workspaceId]).then(size => ({ size }));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query(() => WorkspaceBlobSizes)
|
@Query(() => WorkspaceBlobSizes)
|
||||||
async collectAllBlobSizes(@CurrentUser() user: UserType) {
|
async collectAllBlobSizes(@CurrentUser() user: UserType) {
|
||||||
const workspaces = await this.prisma.userWorkspacePermission
|
const workspaces = await this.prisma.workspaceUserPermission
|
||||||
.findMany({
|
.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -751,7 +698,7 @@ export class WorkspaceResolver {
|
|||||||
@Args('workspaceId') workspaceId: string,
|
@Args('workspaceId') workspaceId: string,
|
||||||
@Args('size', { type: () => Float }) size: number
|
@Args('size', { type: () => Float }) size: number
|
||||||
) {
|
) {
|
||||||
const canWrite = await this.permissions.tryCheck(
|
const canWrite = await this.permissions.tryCheckWorkspace(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
user.id,
|
user.id,
|
||||||
Permission.Write
|
Permission.Write
|
||||||
@@ -775,7 +722,11 @@ export class WorkspaceResolver {
|
|||||||
@Args({ name: 'blob', type: () => GraphQLUpload })
|
@Args({ name: 'blob', type: () => GraphQLUpload })
|
||||||
blob: FileUpload
|
blob: FileUpload
|
||||||
) {
|
) {
|
||||||
await this.permissions.check(workspaceId, user.id, Permission.Write);
|
await this.permissions.checkWorkspace(
|
||||||
|
workspaceId,
|
||||||
|
user.id,
|
||||||
|
Permission.Write
|
||||||
|
);
|
||||||
|
|
||||||
// quota was apply to owner's account
|
// quota was apply to owner's account
|
||||||
const { user: owner } =
|
const { user: owner } =
|
||||||
@@ -831,8 +782,151 @@ export class WorkspaceResolver {
|
|||||||
@Args('workspaceId') workspaceId: string,
|
@Args('workspaceId') workspaceId: string,
|
||||||
@Args('hash') hash: string
|
@Args('hash') hash: string
|
||||||
) {
|
) {
|
||||||
await this.permissions.check(workspaceId, user.id);
|
await this.permissions.checkWorkspace(workspaceId, user.id);
|
||||||
|
|
||||||
return this.storage.deleteBlob(workspaceId, hash);
|
return this.storage.deleteBlob(workspaceId, hash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerEnumType(PublicPageMode, {
|
||||||
|
name: 'PublicPageMode',
|
||||||
|
description: 'The mode which the public page default in',
|
||||||
|
});
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
class WorkspacePage implements Partial<PrismaWorkspacePage> {
|
||||||
|
@Field(() => String, { name: 'id' })
|
||||||
|
pageId!: string;
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
workspaceId!: string;
|
||||||
|
|
||||||
|
@Field(() => PublicPageMode)
|
||||||
|
mode!: PublicPageMode;
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
public!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(CloudThrottlerGuard)
|
||||||
|
@Auth()
|
||||||
|
@Resolver(() => WorkspaceType)
|
||||||
|
export class PagePermissionResolver {
|
||||||
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly permission: PermissionService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
@ResolveField(() => [String], {
|
||||||
|
description: 'Shared pages of workspace',
|
||||||
|
complexity: 2,
|
||||||
|
deprecationReason: 'use WorkspaceType.publicPages',
|
||||||
|
})
|
||||||
|
async sharedPages(@Parent() workspace: WorkspaceType) {
|
||||||
|
const data = await this.prisma.workspacePage.findMany({
|
||||||
|
where: {
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
public: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return data.map(row => row.pageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => [WorkspacePage], {
|
||||||
|
description: 'Public pages of a workspace',
|
||||||
|
complexity: 2,
|
||||||
|
})
|
||||||
|
async publicPages(@Parent() workspace: WorkspaceType) {
|
||||||
|
return this.prisma.workspacePage.findMany({
|
||||||
|
where: {
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
public: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
@Mutation(() => Boolean, {
|
||||||
|
name: 'sharePage',
|
||||||
|
deprecationReason: 'renamed to publicPage',
|
||||||
|
})
|
||||||
|
async deprecatedSharePage(
|
||||||
|
@CurrentUser() user: UserType,
|
||||||
|
@Args('workspaceId') workspaceId: string,
|
||||||
|
@Args('pageId') pageId: string
|
||||||
|
) {
|
||||||
|
await this.publishPage(user, workspaceId, pageId, PublicPageMode.Page);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mutation(() => WorkspacePage)
|
||||||
|
async publishPage(
|
||||||
|
@CurrentUser() user: UserType,
|
||||||
|
@Args('workspaceId') workspaceId: string,
|
||||||
|
@Args('pageId') pageId: string,
|
||||||
|
@Args({
|
||||||
|
name: 'mode',
|
||||||
|
type: () => PublicPageMode,
|
||||||
|
nullable: true,
|
||||||
|
defaultValue: PublicPageMode.Page,
|
||||||
|
})
|
||||||
|
mode: PublicPageMode
|
||||||
|
) {
|
||||||
|
const docId = new DocID(pageId, workspaceId);
|
||||||
|
|
||||||
|
if (docId.isWorkspace) {
|
||||||
|
throw new ForbiddenException('Expect page not to be workspace');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.permission.checkWorkspace(
|
||||||
|
workspaceId,
|
||||||
|
user.id,
|
||||||
|
Permission.Admin
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.permission.publishPage(docId.workspace, docId.guid, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
@Mutation(() => Boolean, {
|
||||||
|
name: 'revokePage',
|
||||||
|
deprecationReason: 'use revokePublicPage',
|
||||||
|
})
|
||||||
|
async deprecatedRevokePage(
|
||||||
|
@CurrentUser() user: UserType,
|
||||||
|
@Args('workspaceId') workspaceId: string,
|
||||||
|
@Args('pageId') pageId: string
|
||||||
|
) {
|
||||||
|
await this.revokePublicPage(user, workspaceId, pageId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mutation(() => WorkspacePage)
|
||||||
|
async revokePublicPage(
|
||||||
|
@CurrentUser() user: UserType,
|
||||||
|
@Args('workspaceId') workspaceId: string,
|
||||||
|
@Args('pageId') pageId: string
|
||||||
|
) {
|
||||||
|
const docId = new DocID(pageId, workspaceId);
|
||||||
|
|
||||||
|
if (docId.isWorkspace) {
|
||||||
|
throw new ForbiddenException('Expect page not to be workspace');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.permission.checkWorkspace(
|
||||||
|
docId.workspace,
|
||||||
|
user.id,
|
||||||
|
Permission.Admin
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.permission.revokePublicPage(docId.workspace, docId.guid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -184,11 +184,14 @@ type WorkspaceType {
|
|||||||
"""member count of workspace"""
|
"""member count of workspace"""
|
||||||
memberCount: Int!
|
memberCount: Int!
|
||||||
|
|
||||||
"""Shared pages of workspace"""
|
|
||||||
sharedPages: [String!]!
|
|
||||||
|
|
||||||
"""Owner of workspace"""
|
"""Owner of workspace"""
|
||||||
owner: UserType!
|
owner: UserType!
|
||||||
|
|
||||||
|
"""Shared pages of workspace"""
|
||||||
|
sharedPages: [String!]! @deprecated(reason: "use WorkspaceType.publicPages")
|
||||||
|
|
||||||
|
"""Public pages of a workspace"""
|
||||||
|
publicPages: [WorkspacePage!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
type InvitationWorkspaceType {
|
type InvitationWorkspaceType {
|
||||||
@@ -216,6 +219,19 @@ type InvitationType {
|
|||||||
invitee: UserType!
|
invitee: UserType!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WorkspacePage {
|
||||||
|
id: String!
|
||||||
|
workspaceId: String!
|
||||||
|
mode: PublicPageMode!
|
||||||
|
public: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""The mode which the public page default in"""
|
||||||
|
enum PublicPageMode {
|
||||||
|
Page
|
||||||
|
Edgeless
|
||||||
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
"""Get is owner of workspace"""
|
"""Get is owner of workspace"""
|
||||||
isOwner(workspaceId: String!): Boolean!
|
isOwner(workspaceId: String!): Boolean!
|
||||||
@@ -265,12 +281,13 @@ type Mutation {
|
|||||||
invite(workspaceId: String!, email: String!, permission: Permission!, sendInviteMail: Boolean): String!
|
invite(workspaceId: String!, email: String!, permission: Permission!, sendInviteMail: Boolean): String!
|
||||||
revoke(workspaceId: String!, userId: String!): Boolean!
|
revoke(workspaceId: String!, userId: String!): Boolean!
|
||||||
acceptInviteById(workspaceId: String!, inviteId: String!, sendAcceptMail: Boolean): Boolean!
|
acceptInviteById(workspaceId: String!, inviteId: String!, sendAcceptMail: Boolean): Boolean!
|
||||||
acceptInvite(workspaceId: String!): Boolean!
|
|
||||||
leaveWorkspace(workspaceId: String!, workspaceName: String!, sendLeaveMail: Boolean): Boolean!
|
leaveWorkspace(workspaceId: String!, workspaceName: String!, sendLeaveMail: Boolean): Boolean!
|
||||||
sharePage(workspaceId: String!, pageId: String!): Boolean!
|
|
||||||
revokePage(workspaceId: String!, pageId: String!): Boolean!
|
|
||||||
setBlob(workspaceId: String!, blob: Upload!): String!
|
setBlob(workspaceId: String!, blob: Upload!): String!
|
||||||
deleteBlob(workspaceId: String!, hash: String!): Boolean!
|
deleteBlob(workspaceId: String!, hash: String!): Boolean!
|
||||||
|
sharePage(workspaceId: String!, pageId: String!): Boolean! @deprecated(reason: "renamed to publicPage")
|
||||||
|
publishPage(workspaceId: String!, pageId: String!, mode: PublicPageMode = Page): WorkspacePage!
|
||||||
|
revokePage(workspaceId: String!, pageId: String!): Boolean! @deprecated(reason: "use revokePublicPage")
|
||||||
|
revokePublicPage(workspaceId: String!, pageId: String!): WorkspacePage!
|
||||||
|
|
||||||
"""Upload user avatar"""
|
"""Upload user avatar"""
|
||||||
uploadAvatar(avatar: Upload!): UserType!
|
uploadAvatar(avatar: Upload!): UserType!
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ const FakePrisma = {
|
|||||||
return { id: this.id, blob: Buffer.from([0, 0]) };
|
return { id: this.id, blob: Buffer.from([0, 0]) };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
get userWorkspacePermission() {
|
get workspaceUserPermission() {
|
||||||
return {
|
return {
|
||||||
id: randomUUID(),
|
id: randomUUID(),
|
||||||
prisma: this,
|
prisma: this,
|
||||||
@@ -79,7 +79,7 @@ const FakePrisma = {
|
|||||||
async findFirstOrThrow() {
|
async findFirstOrThrow() {
|
||||||
return { id: this.id, user: this.prisma.fakeUser };
|
return { id: this.id, user: this.prisma.fakeUser };
|
||||||
},
|
},
|
||||||
async userWorkspacePermission() {
|
async workspaceUserPermission() {
|
||||||
return {
|
return {
|
||||||
id: randomUUID(),
|
id: randomUUID(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
|
|||||||
@@ -78,11 +78,11 @@ async function createWorkspace(
|
|||||||
return res.body.data.createWorkspace;
|
return res.body.data.createWorkspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getWorkspaceSharedPages(
|
export async function getWorkspacePublicPages(
|
||||||
app: INestApplication,
|
app: INestApplication,
|
||||||
token: string,
|
token: string,
|
||||||
workspaceId: string
|
workspaceId: string
|
||||||
): Promise<string[]> {
|
) {
|
||||||
const res = await request(app.getHttpServer())
|
const res = await request(app.getHttpServer())
|
||||||
.post(gql)
|
.post(gql)
|
||||||
.auth(token, { type: 'bearer' })
|
.auth(token, { type: 'bearer' })
|
||||||
@@ -91,13 +91,16 @@ export async function getWorkspaceSharedPages(
|
|||||||
query: `
|
query: `
|
||||||
query {
|
query {
|
||||||
workspace(id: "${workspaceId}") {
|
workspace(id: "${workspaceId}") {
|
||||||
sharedPages
|
publicPages {
|
||||||
|
id
|
||||||
|
mode
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
.expect(200);
|
.expect(200);
|
||||||
return res.body.data.workspace.sharedPages;
|
return res.body.data.workspace.publicPages;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getWorkspace(
|
async function getWorkspace(
|
||||||
@@ -210,26 +213,6 @@ async function acceptInviteById(
|
|||||||
return res.body.data.acceptInviteById;
|
return res.body.data.acceptInviteById;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function acceptInvite(
|
|
||||||
app: INestApplication,
|
|
||||||
token: string,
|
|
||||||
workspaceId: string
|
|
||||||
): Promise<boolean> {
|
|
||||||
const res = await request(app.getHttpServer())
|
|
||||||
.post(gql)
|
|
||||||
.auth(token, { type: 'bearer' })
|
|
||||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
|
||||||
.send({
|
|
||||||
query: `
|
|
||||||
mutation {
|
|
||||||
acceptInvite(workspaceId: "${workspaceId}")
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
})
|
|
||||||
.expect(200);
|
|
||||||
return res.body.data.acceptInvite;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function leaveWorkspace(
|
async function leaveWorkspace(
|
||||||
app: INestApplication,
|
app: INestApplication,
|
||||||
token: string,
|
token: string,
|
||||||
@@ -272,12 +255,12 @@ async function revokeUser(
|
|||||||
return res.body.data.revoke;
|
return res.body.data.revoke;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sharePage(
|
async function publishPage(
|
||||||
app: INestApplication,
|
app: INestApplication,
|
||||||
token: string,
|
token: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
pageId: string
|
pageId: string
|
||||||
): Promise<boolean | string> {
|
) {
|
||||||
const res = await request(app.getHttpServer())
|
const res = await request(app.getHttpServer())
|
||||||
.post(gql)
|
.post(gql)
|
||||||
.auth(token, { type: 'bearer' })
|
.auth(token, { type: 'bearer' })
|
||||||
@@ -285,20 +268,23 @@ async function sharePage(
|
|||||||
.send({
|
.send({
|
||||||
query: `
|
query: `
|
||||||
mutation {
|
mutation {
|
||||||
sharePage(workspaceId: "${workspaceId}", pageId: "${pageId}")
|
publishPage(workspaceId: "${workspaceId}", pageId: "${pageId}") {
|
||||||
|
id
|
||||||
|
mode
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
.expect(200);
|
.expect(200);
|
||||||
return res.body.errors?.[0]?.message || res.body.data?.sharePage;
|
return res.body.errors?.[0]?.message || res.body.data?.publishPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function revokePage(
|
async function revokePublicPage(
|
||||||
app: INestApplication,
|
app: INestApplication,
|
||||||
token: string,
|
token: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
pageId: string
|
pageId: string
|
||||||
): Promise<boolean | string> {
|
) {
|
||||||
const res = await request(app.getHttpServer())
|
const res = await request(app.getHttpServer())
|
||||||
.post(gql)
|
.post(gql)
|
||||||
.auth(token, { type: 'bearer' })
|
.auth(token, { type: 'bearer' })
|
||||||
@@ -306,12 +292,16 @@ async function revokePage(
|
|||||||
.send({
|
.send({
|
||||||
query: `
|
query: `
|
||||||
mutation {
|
mutation {
|
||||||
revokePage(workspaceId: "${workspaceId}", pageId: "${pageId}")
|
revokePublicPage(workspaceId: "${workspaceId}", pageId: "${pageId}") {
|
||||||
|
id
|
||||||
|
mode
|
||||||
|
public
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
.expect(200);
|
.expect(200);
|
||||||
return res.body.errors?.[0]?.message || res.body.data?.revokePage;
|
return res.body.errors?.[0]?.message || res.body.data?.revokePublicPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listBlobs(
|
async function listBlobs(
|
||||||
@@ -572,7 +562,6 @@ export class FakePrisma {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
acceptInvite,
|
|
||||||
acceptInviteById,
|
acceptInviteById,
|
||||||
changeEmail,
|
changeEmail,
|
||||||
checkBlobSize,
|
checkBlobSize,
|
||||||
@@ -587,12 +576,12 @@ export {
|
|||||||
inviteUser,
|
inviteUser,
|
||||||
leaveWorkspace,
|
leaveWorkspace,
|
||||||
listBlobs,
|
listBlobs,
|
||||||
revokePage,
|
publishPage,
|
||||||
|
revokePublicPage,
|
||||||
revokeUser,
|
revokeUser,
|
||||||
sendChangeEmail,
|
sendChangeEmail,
|
||||||
sendVerifyChangeEmail,
|
sendVerifyChangeEmail,
|
||||||
setBlob,
|
setBlob,
|
||||||
sharePage,
|
|
||||||
signUp,
|
signUp,
|
||||||
updateWorkspace,
|
updateWorkspace,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { AppModule } from '../src/app';
|
|||||||
import { MailService } from '../src/modules/auth/mailer';
|
import { MailService } from '../src/modules/auth/mailer';
|
||||||
import { AuthService } from '../src/modules/auth/service';
|
import { AuthService } from '../src/modules/auth/service';
|
||||||
import {
|
import {
|
||||||
acceptInvite,
|
|
||||||
acceptInviteById,
|
acceptInviteById,
|
||||||
createWorkspace,
|
createWorkspace,
|
||||||
getWorkspace,
|
getWorkspace,
|
||||||
@@ -78,33 +77,20 @@ test('should invite a user', async t => {
|
|||||||
t.truthy(invite, 'failed to invite user');
|
t.truthy(invite, 'failed to invite user');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should accept an invite', async t => {
|
|
||||||
const { app } = t.context;
|
|
||||||
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1');
|
|
||||||
const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1');
|
|
||||||
|
|
||||||
const workspace = await createWorkspace(app, u1.token.token);
|
|
||||||
await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin');
|
|
||||||
|
|
||||||
const accept = await acceptInvite(app, u2.token.token, workspace.id);
|
|
||||||
t.is(accept, true, 'failed to accept invite');
|
|
||||||
|
|
||||||
const currWorkspace = await getWorkspace(app, u1.token.token, workspace.id);
|
|
||||||
const currMember = currWorkspace.members.find(u => u.email === u2.email);
|
|
||||||
t.not(currMember, undefined, 'failed to invite user');
|
|
||||||
t.is(currMember!.id, u2.id, 'failed to invite user');
|
|
||||||
t.true(!currMember!.accepted, 'failed to invite user');
|
|
||||||
t.pass();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should leave a workspace', async t => {
|
test('should leave a workspace', async t => {
|
||||||
const { app } = t.context;
|
const { app } = t.context;
|
||||||
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1');
|
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1');
|
||||||
const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1');
|
const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1');
|
||||||
|
|
||||||
const workspace = await createWorkspace(app, u1.token.token);
|
const workspace = await createWorkspace(app, u1.token.token);
|
||||||
await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin');
|
const id = await inviteUser(
|
||||||
await acceptInvite(app, u2.token.token, workspace.id);
|
app,
|
||||||
|
u1.token.token,
|
||||||
|
workspace.id,
|
||||||
|
u2.email,
|
||||||
|
'Admin'
|
||||||
|
);
|
||||||
|
await acceptInviteById(app, workspace.id, id, false);
|
||||||
|
|
||||||
const leave = await leaveWorkspace(app, u2.token.token, workspace.id);
|
const leave = await leaveWorkspace(app, u2.token.token, workspace.id);
|
||||||
|
|
||||||
@@ -253,11 +239,23 @@ test('should support pagination for member', async t => {
|
|||||||
const u3 = await signUp(app, 'u3', 'u3@affine.pro', '1');
|
const u3 = await signUp(app, 'u3', 'u3@affine.pro', '1');
|
||||||
|
|
||||||
const workspace = await createWorkspace(app, u1.token.token);
|
const workspace = await createWorkspace(app, u1.token.token);
|
||||||
await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin');
|
const invite1 = await inviteUser(
|
||||||
await inviteUser(app, u1.token.token, workspace.id, u3.email, 'Admin');
|
app,
|
||||||
|
u1.token.token,
|
||||||
|
workspace.id,
|
||||||
|
u2.email,
|
||||||
|
'Admin'
|
||||||
|
);
|
||||||
|
const invite2 = await inviteUser(
|
||||||
|
app,
|
||||||
|
u1.token.token,
|
||||||
|
workspace.id,
|
||||||
|
u3.email,
|
||||||
|
'Admin'
|
||||||
|
);
|
||||||
|
|
||||||
await acceptInvite(app, u2.token.token, workspace.id);
|
await acceptInviteById(app, workspace.id, invite1, false);
|
||||||
await acceptInvite(app, u3.token.token, workspace.id);
|
await acceptInviteById(app, workspace.id, invite2, false);
|
||||||
|
|
||||||
const firstPageWorkspace = await getWorkspace(
|
const firstPageWorkspace = await getWorkspace(
|
||||||
app,
|
app,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { StorageProvide } from '../src/storage';
|
|||||||
import { FakePrisma } from './utils';
|
import { FakePrisma } from './utils';
|
||||||
|
|
||||||
class FakePermission {
|
class FakePermission {
|
||||||
async tryCheck() {
|
async tryCheckWorkspace() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
async getWorkspaceOwner() {
|
async getWorkspaceOwner() {
|
||||||
@@ -37,7 +37,7 @@ test.beforeEach(async t => {
|
|||||||
})
|
})
|
||||||
.overrideProvider(PrismaService)
|
.overrideProvider(PrismaService)
|
||||||
.useValue({
|
.useValue({
|
||||||
userWorkspacePermission: {
|
workspaceUserPermission: {
|
||||||
async findMany() {
|
async findMany() {
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ import request from 'supertest';
|
|||||||
|
|
||||||
import { AppModule } from '../src/app';
|
import { AppModule } from '../src/app';
|
||||||
import {
|
import {
|
||||||
acceptInvite,
|
acceptInviteById,
|
||||||
createWorkspace,
|
createWorkspace,
|
||||||
currentUser,
|
currentUser,
|
||||||
getPublicWorkspace,
|
getPublicWorkspace,
|
||||||
getWorkspaceSharedPages,
|
getWorkspacePublicPages,
|
||||||
inviteUser,
|
inviteUser,
|
||||||
revokePage,
|
publishPage,
|
||||||
sharePage,
|
revokePublicPage,
|
||||||
signUp,
|
signUp,
|
||||||
updateWorkspace,
|
updateWorkspace,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
@@ -122,15 +122,19 @@ test('should share a page', async t => {
|
|||||||
|
|
||||||
const workspace = await createWorkspace(app, u1.token.token);
|
const workspace = await createWorkspace(app, u1.token.token);
|
||||||
|
|
||||||
const share = await sharePage(app, u1.token.token, workspace.id, 'page1');
|
const share = await publishPage(app, u1.token.token, workspace.id, 'page1');
|
||||||
t.true(share, 'failed to share page');
|
t.is(share.id, 'page1', 'failed to share page');
|
||||||
const pages = await getWorkspaceSharedPages(
|
const pages = await getWorkspacePublicPages(
|
||||||
app,
|
app,
|
||||||
u1.token.token,
|
u1.token.token,
|
||||||
workspace.id
|
workspace.id
|
||||||
);
|
);
|
||||||
t.is(pages.length, 1, 'failed to get shared pages');
|
t.is(pages.length, 1, 'failed to get shared pages');
|
||||||
t.is(pages[0], 'page1', 'failed to get shared page: page1');
|
t.deepEqual(
|
||||||
|
pages[0],
|
||||||
|
{ id: 'page1', mode: 'Page' },
|
||||||
|
'failed to get shared page: page1'
|
||||||
|
);
|
||||||
|
|
||||||
const resp1 = await request(app.getHttpServer())
|
const resp1 = await request(app.getHttpServer())
|
||||||
.get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`)
|
.get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`)
|
||||||
@@ -139,7 +143,7 @@ test('should share a page', async t => {
|
|||||||
const resp2 = await request(app.getHttpServer()).get(
|
const resp2 = await request(app.getHttpServer()).get(
|
||||||
`/api/workspaces/${workspace.id}/docs/${workspace.id}`
|
`/api/workspaces/${workspace.id}/docs/${workspace.id}`
|
||||||
);
|
);
|
||||||
t.is(resp2.statusCode, 200, 'should not get root doc without token');
|
t.is(resp2.statusCode, 200, 'failed to get root doc with public pages');
|
||||||
|
|
||||||
const resp3 = await request(app.getHttpServer())
|
const resp3 = await request(app.getHttpServer())
|
||||||
.get(`/api/workspaces/${workspace.id}/docs/page1`)
|
.get(`/api/workspaces/${workspace.id}/docs/page1`)
|
||||||
@@ -152,32 +156,55 @@ test('should share a page', async t => {
|
|||||||
// 404 because we don't put the page doc to server
|
// 404 because we don't put the page doc to server
|
||||||
t.is(resp4.statusCode, 404, 'should not get shared doc without token');
|
t.is(resp4.statusCode, 404, 'should not get shared doc without token');
|
||||||
|
|
||||||
const msg1 = await sharePage(app, u2.token.token, 'not_exists_ws', 'page2');
|
const msg1 = await publishPage(app, u2.token.token, 'not_exists_ws', 'page2');
|
||||||
t.is(msg1, 'Permission denied', 'unauthorized user can share page');
|
t.is(msg1, 'Permission denied', 'unauthorized user can share page');
|
||||||
const msg2 = await revokePage(app, u2.token.token, 'not_exists_ws', 'page2');
|
const msg2 = await revokePublicPage(
|
||||||
|
app,
|
||||||
|
u2.token.token,
|
||||||
|
'not_exists_ws',
|
||||||
|
'page2'
|
||||||
|
);
|
||||||
t.is(msg2, 'Permission denied', 'unauthorized user can share page');
|
t.is(msg2, 'Permission denied', 'unauthorized user can share page');
|
||||||
|
|
||||||
await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin');
|
await acceptInviteById(
|
||||||
await acceptInvite(app, u2.token.token, workspace.id);
|
app,
|
||||||
const invited = await sharePage(app, u2.token.token, workspace.id, 'page2');
|
workspace.id,
|
||||||
t.true(invited, 'failed to share page');
|
await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin')
|
||||||
|
);
|
||||||
|
const invited = await publishPage(app, u2.token.token, workspace.id, 'page2');
|
||||||
|
t.is(invited.id, 'page2', 'failed to share page');
|
||||||
|
|
||||||
const revoke = await revokePage(app, u1.token.token, workspace.id, 'page1');
|
const revoke = await revokePublicPage(
|
||||||
t.true(revoke, 'failed to revoke page');
|
app,
|
||||||
const pages2 = await getWorkspaceSharedPages(
|
u1.token.token,
|
||||||
|
workspace.id,
|
||||||
|
'page1'
|
||||||
|
);
|
||||||
|
t.false(revoke.public, 'failed to revoke page');
|
||||||
|
const pages2 = await getWorkspacePublicPages(
|
||||||
app,
|
app,
|
||||||
u1.token.token,
|
u1.token.token,
|
||||||
workspace.id
|
workspace.id
|
||||||
);
|
);
|
||||||
t.is(pages2.length, 1, 'failed to get shared pages');
|
t.is(pages2.length, 1, 'failed to get shared pages');
|
||||||
t.is(pages2[0], 'page2', 'failed to get shared page: page2');
|
t.is(pages2[0].id, 'page2', 'failed to get shared page: page2');
|
||||||
|
|
||||||
const msg3 = await revokePage(app, u1.token.token, workspace.id, 'page3');
|
const msg3 = await revokePublicPage(
|
||||||
t.false(msg3, 'can revoke non-exists page');
|
app,
|
||||||
|
u1.token.token,
|
||||||
|
workspace.id,
|
||||||
|
'page3'
|
||||||
|
);
|
||||||
|
t.is(msg3, 'Page is not public');
|
||||||
|
|
||||||
const msg4 = await revokePage(app, u1.token.token, workspace.id, 'page2');
|
const msg4 = await revokePublicPage(
|
||||||
t.true(msg4, 'failed to revoke page');
|
app,
|
||||||
const page3 = await getWorkspaceSharedPages(
|
u1.token.token,
|
||||||
|
workspace.id,
|
||||||
|
'page2'
|
||||||
|
);
|
||||||
|
t.false(msg4.public, 'failed to revoke page');
|
||||||
|
const page3 = await getWorkspacePublicPages(
|
||||||
app,
|
app,
|
||||||
u1.token.token,
|
u1.token.token,
|
||||||
workspace.id
|
workspace.id
|
||||||
@@ -211,13 +238,17 @@ test('should can get workspace doc', async t => {
|
|||||||
.auth(u2.token.token, { type: 'bearer' })
|
.auth(u2.token.token, { type: 'bearer' })
|
||||||
.expect(403);
|
.expect(403);
|
||||||
|
|
||||||
await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin');
|
|
||||||
await request(app.getHttpServer())
|
await request(app.getHttpServer())
|
||||||
.get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`)
|
.get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`)
|
||||||
.auth(u2.token.token, { type: 'bearer' })
|
.auth(u2.token.token, { type: 'bearer' })
|
||||||
.expect(403);
|
.expect(403);
|
||||||
|
|
||||||
await acceptInvite(app, u2.token.token, workspace.id);
|
await acceptInviteById(
|
||||||
|
app,
|
||||||
|
workspace.id,
|
||||||
|
await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin')
|
||||||
|
);
|
||||||
|
|
||||||
const res2 = await request(app.getHttpServer())
|
const res2 = await request(app.getHttpServer())
|
||||||
.get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`)
|
.get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`)
|
||||||
.auth(u2.token.token, { type: 'bearer' })
|
.auth(u2.token.token, { type: 'bearer' })
|
||||||
|
|||||||
@@ -665,14 +665,3 @@ mutation acceptInviteByInviteId($workspaceId: String!, $inviteId: String!, $send
|
|||||||
)
|
)
|
||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const acceptInviteByWorkspaceIdMutation = {
|
|
||||||
id: 'acceptInviteByWorkspaceIdMutation' as const,
|
|
||||||
operationName: 'acceptInviteByWorkspaceId',
|
|
||||||
definitionName: 'acceptInvite',
|
|
||||||
containsFile: false,
|
|
||||||
query: `
|
|
||||||
mutation acceptInviteByWorkspaceId($workspaceId: String!) {
|
|
||||||
acceptInvite(workspaceId: $workspaceId)
|
|
||||||
}`,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
mutation acceptInviteByWorkspaceId($workspaceId: String!) {
|
|
||||||
acceptInvite(workspaceId: $workspaceId)
|
|
||||||
}
|
|
||||||
@@ -52,6 +52,12 @@ export enum Permission {
|
|||||||
Write = 'Write',
|
Write = 'Write',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The mode which the public page default in */
|
||||||
|
export enum PublicPageMode {
|
||||||
|
Edgeless = 'Edgeless',
|
||||||
|
Page = 'Page',
|
||||||
|
}
|
||||||
|
|
||||||
export enum SubscriptionPlan {
|
export enum SubscriptionPlan {
|
||||||
Enterprise = 'Enterprise',
|
Enterprise = 'Enterprise',
|
||||||
Free = 'Free',
|
Free = 'Free',
|
||||||
@@ -615,15 +621,6 @@ export type AcceptInviteByInviteIdMutation = {
|
|||||||
acceptInviteById: boolean;
|
acceptInviteById: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AcceptInviteByWorkspaceIdMutationVariables = Exact<{
|
|
||||||
workspaceId: Scalars['String']['input'];
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type AcceptInviteByWorkspaceIdMutation = {
|
|
||||||
__typename?: 'Mutation';
|
|
||||||
acceptInvite: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Queries =
|
export type Queries =
|
||||||
| {
|
| {
|
||||||
name: 'checkBlobSizesQuery';
|
name: 'checkBlobSizesQuery';
|
||||||
@@ -856,9 +853,4 @@ export type Mutations =
|
|||||||
name: 'acceptInviteByInviteIdMutation';
|
name: 'acceptInviteByInviteIdMutation';
|
||||||
variables: AcceptInviteByInviteIdMutationVariables;
|
variables: AcceptInviteByInviteIdMutationVariables;
|
||||||
response: AcceptInviteByInviteIdMutation;
|
response: AcceptInviteByInviteIdMutation;
|
||||||
}
|
|
||||||
| {
|
|
||||||
name: 'acceptInviteByWorkspaceIdMutation';
|
|
||||||
variables: AcceptInviteByWorkspaceIdMutationVariables;
|
|
||||||
response: AcceptInviteByWorkspaceIdMutation;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -79,10 +79,9 @@ export async function addUserToWorkspace(
|
|||||||
if (workspace == null) {
|
if (workspace == null) {
|
||||||
throw new Error(`workspace ${workspaceId} not found`);
|
throw new Error(`workspace ${workspaceId} not found`);
|
||||||
}
|
}
|
||||||
await client.userWorkspacePermission.create({
|
await client.workspaceUserPermission.create({
|
||||||
data: {
|
data: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
subPageId: null,
|
|
||||||
userId,
|
userId,
|
||||||
accepted: true,
|
accepted: true,
|
||||||
type: permission,
|
type: permission,
|
||||||
|
|||||||
Reference in New Issue
Block a user