Permission checks improvements

This commit is contained in:
Hakan Shehu
2025-02-12 12:16:05 +01:00
parent d8745d84cd
commit 95999b4f9e
29 changed files with 775 additions and 700 deletions

View File

@@ -1,14 +0,0 @@
import { Migration, sql } from 'kysely';
export const createTextsTable: Migration = {
up: async (db) => {
await sql`
CREATE VIRTUAL TABLE texts USING fts5(id UNINDEXED, name, text);
`.execute(db);
},
down: async (db) => {
await sql`
DROP TABLE IF EXISTS texts;
`.execute(db);
},
};

View File

@@ -10,11 +10,10 @@ import { createCollaborationsTable } from './00007-create-collaborations-table';
import { createFilesTable } from './00008-create-files-table';
import { createMutationsTable } from './00009-create-mutations-table';
import { createTombstonesTable } from './00010-create-tombstones-table';
import { createTextsTable } from './00011-create-texts-table';
import { createCursorsTable } from './00012-create-cursors-table';
import { createMetadataTable } from './00013-create-metadata-table';
import { createDocumentsTable } from './00014-create-documents-table';
import { createDocumentUpdatesTable } from './00015-create-document-updates-table';
import { createDocumentsTable } from './00011-create-documents-table';
import { createDocumentUpdatesTable } from './00012-create-document-updates-table';
import { createCursorsTable } from './00013-create-cursors-table';
import { createMetadataTable } from './00014-create-metadata-table';
export const workspaceDatabaseMigrations: Record<string, Migration> = {
'00001-create-users-table': createUsersTable,
@@ -27,9 +26,8 @@ export const workspaceDatabaseMigrations: Record<string, Migration> = {
'00008-create-files-table': createFilesTable,
'00009-create-mutations-table': createMutationsTable,
'00010-create-tombstones-table': createTombstonesTable,
'00011-create-texts-table': createTextsTable,
'00012-create-cursors-table': createCursorsTable,
'00013-create-metadata-table': createMetadataTable,
'00014-create-documents-table': createDocumentsTable,
'00015-create-document-updates-table': createDocumentUpdatesTable,
'00011-create-documents-table': createDocumentsTable,
'00012-create-document-updates-table': createDocumentUpdatesTable,
'00013-create-cursors-table': createCursorsTable,
'00014-create-metadata-table': createMetadataTable,
};

View File

@@ -159,16 +159,6 @@ export type SelectTombsonte = Selectable<TombstoneTable>;
export type CreateTombstone = Insertable<TombstoneTable>;
export type UpdateTombstone = Updateable<TombstoneTable>;
interface TextTable {
id: ColumnType<string, string, never>;
name: ColumnType<string | null, string | null, string | null>;
text: ColumnType<string | null, string | null, string | null>;
}
export type SelectText = Selectable<TextTable>;
export type CreateText = Insertable<TextTable>;
export type UpdateText = Updateable<TextTable>;
interface CursorTable {
key: ColumnType<string, string, never>;
value: ColumnType<bigint, bigint, bigint>;
@@ -227,9 +217,8 @@ export interface WorkspaceDatabaseSchema {
files: FileTable;
mutations: MutationTable;
tombstones: TombstoneTable;
texts: TextTable;
cursors: CursorTable;
metadata: MetadataTable;
documents: DocumentTable;
document_updates: DocumentUpdateTable;
cursors: CursorTable;
metadata: MetadataTable;
}

View File

@@ -1,18 +1,10 @@
import {
canCreateNode,
generateId,
IdType,
FileAttributes,
} from '@colanode/core';
import { generateId, IdType, FileAttributes } from '@colanode/core';
import { MutationHandler } from '@/main/lib/types';
import {
FileCreateMutationInput,
FileCreateMutationOutput,
} from '@/shared/mutations/files/file-create';
import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { fetchNode } from '@/main/lib/utils';
import { mapNode } from '@/main/lib/mappers';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class FileCreateMutationHandler
@@ -23,53 +15,20 @@ export class FileCreateMutationHandler
input: FileCreateMutationInput
): Promise<FileCreateMutationOutput> {
const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const node = await fetchNode(workspace.database, input.parentId);
if (!node) {
throw new MutationError(
MutationErrorCode.NodeNotFound,
'There was an error while fetching the node. Please make sure you have access to this node.'
);
}
const root = await fetchNode(workspace.database, node.root_id);
if (!root) {
throw new MutationError(
MutationErrorCode.RootNotFound,
'There was an error while fetching the root. Please make sure you have access to this root.'
);
}
const attributes: FileAttributes = {
type: 'file',
parentId: node.id,
parentId: input.parentId,
};
if (
!canCreateNode(
{
user: {
userId: workspace.userId,
role: workspace.role,
},
root: mapNode(root),
},
'file'
)
) {
throw new MutationError(
MutationErrorCode.FileCreateForbidden,
'You are not allowed to upload a file in this node.'
);
}
const fileId = generateId(IdType.File);
await workspace.files.createFile(input.filePath, fileId, node.id, root);
await workspace.nodes.createNode({
id: fileId,
attributes,
parentId: node.id,
parentId: input.parentId,
});
await workspace.files.createFile(fileId, input.filePath);
return {
id: fileId,
};

View File

@@ -1,6 +1,6 @@
import {
canCreateNode,
EditorNodeTypes,
FileAttributes,
generateId,
IdType,
MessageAttributes,
@@ -12,10 +12,12 @@ import {
MessageCreateMutationInput,
MessageCreateMutationOutput,
} from '@/shared/mutations/messages/message-create';
import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { fetchNode } from '@/main/lib/utils';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
import { mapNode } from '@/main/lib/mappers';
interface MessageFile {
id: string;
path: string;
}
export class MessageCreateMutationHandler
extends WorkspaceMutationHandlerBase
@@ -26,43 +28,10 @@ export class MessageCreateMutationHandler
): Promise<MessageCreateMutationOutput> {
const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const node = await fetchNode(workspace.database, input.parentId);
if (!node) {
throw new MutationError(
MutationErrorCode.NodeNotFound,
'There was an error while fetching the conversation. Please make sure you have access to this conversation.'
);
}
const root = await fetchNode(workspace.database, node.root_id);
if (!root) {
throw new MutationError(
MutationErrorCode.RootNotFound,
'There was an error while fetching the root. Please make sure you have access to this root.'
);
}
if (
!canCreateNode(
{
user: {
userId: workspace.userId,
role: workspace.role,
},
root: mapNode(root),
},
'message'
)
) {
throw new MutationError(
MutationErrorCode.MessageCreateForbidden,
'You are not allowed to create a message in this conversation.'
);
}
const messageId = generateId(IdType.Message);
const editorContent = input.content.content ?? [];
const blocks = mapContentsToBlocks(messageId, editorContent, new Map());
const filesToCreate: MessageFile[] = [];
// check if there are nested nodes (files, pages, folders etc.)
for (const block of Object.values(blocks)) {
@@ -70,7 +39,10 @@ export class MessageCreateMutationHandler
const path = block.attrs?.path;
const fileId = generateId(IdType.File);
await workspace.files.createFile(path, fileId, messageId, root);
filesToCreate.push({
id: fileId,
path: path,
});
block.id = fileId;
block.type = 'file';
@@ -92,6 +64,21 @@ export class MessageCreateMutationHandler
parentId: input.parentId,
});
for (const file of filesToCreate) {
const fileAttributes: FileAttributes = {
type: 'file',
parentId: messageId,
};
await workspace.nodes.createNode({
id: file.id,
attributes: fileAttributes,
parentId: messageId,
});
await workspace.files.createFile(file.id, file.path);
}
return {
id: messageId,
};

View File

@@ -1,5 +1,3 @@
import { sql } from 'kysely';
import { WorkspaceQueryHandlerBase } from '@/main/queries/workspace-query-handler-base';
import { SelectUser } from '@/main/databases/workspace';
import { ChangeCheckResult, QueryHandler } from '@/main/lib/types';
@@ -82,26 +80,20 @@ export class UserSearchQueryHandler
input: UserSearchQueryInput
): Promise<SelectUser[]> {
const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const exclude = input.exclude ?? [];
const query = sql<SelectUser>`
SELECT u.*
FROM users u
JOIN texts t ON u.id = t.id
WHERE u.id != ${workspace.userId}
AND t.name MATCH ${input.searchQuery + '*'}
${
exclude.length > 0
? sql`AND u.id NOT IN (${sql.join(
exclude.map((id) => sql`${id}`),
sql`, `
)})`
: sql``
}
`.compile(workspace.database);
const result = await workspace.database.executeQuery(query);
return result.rows;
let queryBuilder = workspace.database
.selectFrom('users')
.selectAll()
.where('id', '!=', workspace.userId)
.where('name', 'like', `%${input.searchQuery}%`);
if (exclude.length > 0) {
queryBuilder = queryBuilder.where('id', 'not in', exclude);
}
const rows = await queryBuilder.execute();
return rows;
}
private async fetchUsers(input: UserSearchQueryInput): Promise<SelectUser[]> {

View File

@@ -16,6 +16,7 @@ import fs from 'fs';
import path from 'path';
import {
fetchNode,
fetchUserStorageUsed,
getFileMetadata,
getWorkspaceFilesDirectoryPath,
@@ -26,7 +27,7 @@ import { eventBus } from '@/shared/lib/event-bus';
import { DownloadStatus, UploadStatus } from '@/shared/types/files';
import { WorkspaceService } from '@/main/services/workspaces/workspace-service';
import { EventLoop } from '@/main/lib/event-loop';
import { SelectFile, SelectNode } from '@/main/databases/workspace';
import { SelectFile } from '@/main/databases/workspace';
import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { formatBytes } from '@/shared/lib/files';
@@ -93,12 +94,7 @@ export class FileService {
this.cleanupEventLoop.start();
}
public async createFile(
path: string,
id: string,
parentId: string,
root: SelectNode
): Promise<void> {
public async createFile(id: string, path: string): Promise<void> {
const metadata = getFileMetadata(path);
if (!metadata) {
throw new MutationError(
@@ -132,13 +128,21 @@ export class FileService {
);
}
const node = await fetchNode(this.workspace.database, id);
if (!node || node.type !== 'file') {
throw new MutationError(
MutationErrorCode.NodeNotFound,
'There was an error while creating the file. Please make sure you have access to this node.'
);
}
this.copyFileToWorkspace(path, id, metadata.extension);
const mutationData: CreateFileMutationData = {
id,
type: metadata.type,
parentId: parentId,
rootId: root.id,
parentId: node.parent_id!,
rootId: node.root_id,
name: metadata.name,
originalName: metadata.name,
extension: metadata.extension,
@@ -156,8 +160,8 @@ export class FileService {
.values({
id,
type: metadata.type,
parent_id: parentId,
root_id: root.id,
parent_id: node.parent_id!,
root_id: node.root_id,
name: metadata.name,
original_name: metadata.name,
mime_type: metadata.mimeType,

View File

@@ -3,21 +3,19 @@ import {
IdType,
createDebugger,
NodeAttributes,
Node,
canCreateNode,
extractNodeText,
canUpdateNode,
canDeleteNode,
DeleteNodeMutationData,
SyncNodeData,
SyncNodeTombstoneData,
getNodeModel,
CreateNodeMutationData,
UpdateNodeMutationData,
CanCreateNodeContext,
CanUpdateAttributesContext,
CanDeleteNodeContext,
} from '@colanode/core';
import { decodeState, encodeState, YDoc } from '@colanode/crdt';
import { fetchNode } from '@/main/lib/utils';
import { fetchNodeAncestors } from '@/main/lib/utils';
import { mapFile, mapNode } from '@/main/lib/mappers';
import { eventBus } from '@/shared/lib/event-bus';
import { WorkspaceService } from '@/main/services/workspaces/workspace-service';
@@ -49,42 +47,26 @@ export class NodeService {
public async createNode(input: CreateNodeInput): Promise<SelectNode> {
this.debug(`Creating ${Array.isArray(input) ? 'nodes' : 'node'}`);
let root: Node | null = null;
const ancestors = input.parentId
? await fetchNodeAncestors(this.workspace.database, input.parentId)
: [];
if (input.parentId) {
const parent = await fetchNode(this.workspace.database, input.parentId);
if (parent) {
if (parent.id === parent.root_id) {
root = mapNode(parent);
} else {
const rootRow = await fetchNode(
this.workspace.database,
parent.root_id
);
if (rootRow) {
root = mapNode(rootRow);
}
}
}
}
if (
!canCreateNode(
{
const model = getNodeModel(input.attributes.type);
const canCreateNodeContext: CanCreateNodeContext = {
user: {
userId: this.workspace.userId,
id: this.workspace.userId,
role: this.workspace.role,
workspaceId: this.workspace.id,
accountId: this.workspace.accountId,
},
root: root,
},
input.attributes.type
)
) {
ancestors: ancestors.map(mapNode),
attributes: input.attributes,
};
if (!model.canCreate(canCreateNodeContext)) {
throw new Error('Insufficient permissions');
}
const model = getNodeModel(input.attributes.type);
const ydoc = new YDoc();
const update = ydoc.update(model.attributesSchema, input.attributes);
@@ -93,7 +75,7 @@ export class NodeService {
}
const createdAt = new Date().toISOString();
const text = extractNodeText(input.id, input.attributes);
const rootId = ancestors[0]?.id ?? input.id;
const { createdNode, createdMutation } = await this.workspace.database
.transaction()
@@ -103,7 +85,7 @@ export class NodeService {
.returningAll()
.values({
id: input.id,
root_id: root?.id ?? input.id,
root_id: rootId,
attributes: JSON.stringify(input.attributes),
created_at: createdAt,
created_by: this.workspace.userId,
@@ -152,13 +134,6 @@ export class NodeService {
throw new Error('Failed to create mutation');
}
if (text) {
await trx
.insertInto('texts')
.values({ id: input.id, name: text.name, text: text.text })
.execute();
}
return {
createdNode,
createdMutation,
@@ -206,13 +181,9 @@ export class NodeService {
): Promise<UpdateNodeResult | null> {
this.debug(`Updating node ${nodeId}`);
const nodeRow = await fetchNode(this.workspace.database, nodeId);
if (!nodeRow) {
return 'not_found';
}
const root = await fetchNode(this.workspace.database, nodeRow.root_id);
if (!root) {
const ancestors = await fetchNodeAncestors(this.workspace.database, nodeId);
const nodeRow = ancestors[ancestors.length - 1];
if (!nodeRow || nodeRow.id !== nodeId) {
return 'not_found';
}
@@ -222,7 +193,22 @@ export class NodeService {
const updatedAttributes = updater(node.attributes as T);
const model = getNodeModel(updatedAttributes.type);
const ydoc = new YDoc();
const canUpdateAttributesContext: CanUpdateAttributesContext = {
user: {
id: this.workspace.userId,
role: this.workspace.role,
workspaceId: this.workspace.id,
accountId: this.workspace.accountId,
},
ancestors: ancestors.map(mapNode),
node: node,
attributes: updatedAttributes,
};
if (!model.canUpdateAttributes(canUpdateAttributesContext)) {
return 'unauthorized';
}
const state = await this.workspace.database
.selectFrom('node_states')
@@ -230,46 +216,31 @@ export class NodeService {
.selectAll()
.executeTakeFirst();
if (state) {
ydoc.applyUpdate(state.state);
}
const updates = await this.workspace.database
.selectFrom('node_updates')
.where('node_id', '=', nodeId)
.selectAll()
.execute();
const ydoc = new YDoc();
if (state) {
ydoc.applyUpdate(state.state);
}
for (const update of updates) {
ydoc.applyUpdate(update.data);
}
const update = ydoc.update(model.attributesSchema, updatedAttributes);
if (
!canUpdateNode(
{
user: {
userId: this.workspace.userId,
role: this.workspace.role,
},
root: mapNode(root),
node: node,
},
updatedAttributes
)
) {
return 'unauthorized';
}
if (!update) {
return 'success';
}
const attributes = ydoc.getObject<NodeAttributes>();
const text = extractNodeText(nodeId, updatedAttributes);
const localRevision = BigInt(node.localRevision) + BigInt(1);
const { updatedNode, createdMutation } = await this.workspace.database
.transaction()
.execute(async (trx) => {
@@ -328,17 +299,6 @@ export class NodeService {
throw new Error('Failed to create mutation');
}
if (text !== undefined) {
await trx.deleteFrom('texts').where('id', '=', nodeId).execute();
}
if (text) {
await trx
.insertInto('texts')
.values({ id: nodeId, name: text.name, text: text.text })
.execute();
}
return {
updatedNode,
createdMutation,
@@ -372,26 +332,28 @@ export class NodeService {
}
public async deleteNode(nodeId: string) {
const node = await fetchNode(this.workspace.database, nodeId);
if (!node) {
throw new Error('Node not found');
const ancestors = await fetchNodeAncestors(this.workspace.database, nodeId);
const nodeRow = ancestors[ancestors.length - 1];
if (!nodeRow || nodeRow.id !== nodeId) {
return 'not_found';
}
const root = await fetchNode(this.workspace.database, node.root_id);
if (!root) {
throw new Error('Root not found');
}
const node = mapNode(nodeRow);
if (
!canDeleteNode({
const model = getNodeModel(node.attributes.type);
const canDeleteNodeContext: CanDeleteNodeContext = {
user: {
userId: this.workspace.userId,
id: this.workspace.userId,
role: this.workspace.role,
workspaceId: this.workspace.id,
accountId: this.workspace.accountId,
},
root: mapNode(root),
node: mapNode(node),
})
) {
ancestors: ancestors.map(mapNode),
node: node,
};
if (!model.canDelete(canDeleteNodeContext)) {
throw new Error('Insufficient permissions');
}
@@ -419,7 +381,7 @@ export class NodeService {
const deleteMutationData: DeleteNodeMutationData = {
id: nodeId,
rootId: root.root_id,
rootId: node.rootId,
deletedAt: new Date().toISOString(),
};
@@ -477,7 +439,6 @@ export class NodeService {
const ydoc = new YDoc(node.state);
const attributes = ydoc.getObject<NodeAttributes>();
const text = extractNodeText(node.id, attributes);
const { createdNode } = await this.workspace.database
.transaction()
@@ -500,13 +461,6 @@ export class NodeService {
throw new Error('Failed to create node');
}
if (text) {
await trx
.insertInto('texts')
.values({ id: node.id, name: text.name, text: text.text })
.execute();
}
await trx
.insertInto('node_states')
.returningAll()
@@ -576,7 +530,6 @@ export class NodeService {
}
const attributes = ydoc.getObject<NodeAttributes>();
const text = extractNodeText(node.id, attributes);
const localRevision = BigInt(existingNode.local_revision) + BigInt(1);
const { updatedNode } = await this.workspace.database
@@ -609,17 +562,6 @@ export class NodeService {
.where('id', '=', node.id)
.executeTakeFirst();
if (text !== undefined) {
await trx.deleteFrom('texts').where('id', '=', node.id).execute();
}
if (text) {
await trx
.insertInto('texts')
.values({ id: node.id, name: text.name, text: text.text })
.execute();
}
return { updatedNode };
});
@@ -664,8 +606,6 @@ export class NodeService {
.where('id', '=', tombstone.id)
.execute();
await trx.deleteFrom('texts').where('id', '=', tombstone.id).execute();
await trx
.deleteFrom('node_reactions')
.where('node_id', '=', tombstone.id)
@@ -687,6 +627,16 @@ export class NodeService {
.where('id', '=', tombstone.id)
.execute();
await trx
.deleteFrom('documents')
.where('id', '=', tombstone.id)
.executeTakeFirst();
await trx
.deleteFrom('document_updates')
.where('document_id', '=', tombstone.id)
.execute();
return { deletedNode, deletedFile };
});
@@ -734,8 +684,6 @@ export class NodeService {
.where('node_id', '=', mutation.id)
.execute();
await tx.deleteFrom('texts').where('id', '=', mutation.id).execute();
await tx
.deleteFrom('node_reactions')
.where('node_id', '=', mutation.id)
@@ -745,6 +693,13 @@ export class NodeService {
.deleteFrom('node_states')
.where('id', '=', mutation.id)
.execute();
await tx.deleteFrom('documents').where('id', '=', mutation.id).execute();
await tx
.deleteFrom('document_updates')
.where('document_id', '=', mutation.id)
.execute();
});
eventBus.publish({
@@ -786,11 +741,6 @@ export class NodeService {
.where('node_id', '=', mutation.id)
.execute();
await this.workspace.database
.deleteFrom('texts')
.where('id', '=', mutation.id)
.execute();
await this.workspace.database
.deleteFrom('node_reactions')
.where('node_id', '=', mutation.id)
@@ -801,6 +751,16 @@ export class NodeService {
.where('id', '=', mutation.id)
.execute();
await this.workspace.database
.deleteFrom('documents')
.where('id', '=', mutation.id)
.execute();
await this.workspace.database
.deleteFrom('document_updates')
.where('document_id', '=', mutation.id)
.execute();
return true;
}
@@ -843,8 +803,6 @@ export class NodeService {
}
const attributes = ydoc.getObject<NodeAttributes>();
const text = extractNodeText(mutation.id, attributes);
const updatedNode = await this.workspace.database
.transaction()
.execute(async (trx) => {
@@ -862,21 +820,6 @@ export class NodeService {
return undefined;
}
if (text !== undefined) {
await trx.deleteFrom('texts').where('id', '=', mutation.id).execute();
}
if (text) {
await trx
.insertInto('texts')
.values({
id: mutation.id,
name: text.name,
text: text.text,
})
.execute();
}
await trx
.deleteFrom('node_updates')
.where('id', '=', mutation.updateId)
@@ -931,9 +874,8 @@ export class NodeService {
}
const attributes = ydoc.getObject<NodeAttributes>();
const text = extractNodeText(mutation.id, attributes);
const deletedNode = JSON.parse(tombstone.data) as SelectNode;
const createdNode = await this.workspace.database
.transaction()
.execute(async (trx) => {
@@ -962,21 +904,6 @@ export class NodeService {
.deleteFrom('tombstones')
.where('id', '=', mutation.id)
.execute();
if (text !== undefined) {
await trx.deleteFrom('texts').where('id', '=', mutation.id).execute();
}
if (text) {
await trx
.insertInto('texts')
.values({
id: mutation.id,
name: text.name,
text: text.text,
})
.execute();
}
});
if (createdNode) {

View File

@@ -50,16 +50,6 @@ export class UserService {
)
.executeTakeFirst();
await this.workspace.database
.deleteFrom('texts')
.where('id', '=', user.id)
.execute();
await this.workspace.database
.insertInto('texts')
.values({ id: user.id, name: user.name, text: null })
.execute();
if (createdUser) {
eventBus.publish({
type: 'user_created',

View File

@@ -29,9 +29,9 @@ const roles: NodeCollaboratorRole[] = [
enabled: true,
},
{
name: 'Commenter',
value: 'commenter',
description: 'Can message or comment on content',
name: 'Collaborator',
value: 'collaborator',
description: 'Can create records, messages or comments',
enabled: true,
},
{

View File

@@ -73,7 +73,7 @@ export const Conversation = ({
};
const isAdmin = hasNodeRole(role, 'admin');
const canCreateMessage = hasNodeRole(role, 'commenter');
const canCreateMessage = hasNodeRole(role, 'collaborator');
return (
<ConversationContext.Provider

View File

@@ -1,7 +1,7 @@
import {
canCreateNode,
canDeleteNode,
canUpdateNode,
CanCreateNodeContext,
CanDeleteNodeContext,
CanUpdateAttributesContext,
createDebugger,
CreateNodeMutationData,
extractNodeCollaborators,
@@ -309,35 +309,34 @@ export const createNodeFromMutation = async (
): Promise<CreateNodeOutput | null> => {
const ydoc = new YDoc(mutation.data);
const attributes = ydoc.getObject<NodeAttributes>();
let root: SelectNode | null = null;
const model = getNodeModel(attributes.type);
let parentId: string | null = null;
if (attributes.type !== 'space' && attributes.type !== 'chat') {
const parent = await fetchNode(attributes.parentId);
if (!parent) {
return null;
parentId = attributes.parentId;
}
root = await fetchNode(parent.root_id);
}
if (
!canCreateNode(
{
const ancestors = parentId ? await fetchNodeAncestors(parentId) : [];
const canCreateNodeContext: CanCreateNodeContext = {
user: {
userId: user.id,
id: user.id,
role: user.role,
workspaceId: user.workspace_id,
accountId: user.account_id,
},
root: root ? mapNode(root) : null,
},
attributes.type
)
) {
ancestors: ancestors.map(mapNode),
attributes,
};
if (!model.canCreate(canCreateNodeContext)) {
return null;
}
const rootId = ancestors[0]?.id ?? mutation.id;
const createNode: CreateNode = {
id: mutation.id,
root_id: root?.id ?? mutation.id,
root_id: rootId,
attributes: JSON.stringify(attributes),
workspace_id: user.workspace_id,
created_at: new Date(mutation.createdAt),
@@ -386,7 +385,7 @@ export const createNodeFromMutation = async (
eventBus.publish({
type: 'node_created',
nodeId: mutation.id,
rootId: root?.id ?? mutation.id,
rootId,
workspaceId: user.workspace_id,
});
@@ -431,35 +430,36 @@ const tryUpdateNodeFromMutation = async (
user: SelectUser,
mutation: UpdateNodeMutationData
): Promise<ConcurrentUpdateResult<UpdateNodeOutput>> => {
const node = await fetchNode(mutation.id);
if (!node) {
const ancestors = await fetchNodeAncestors(mutation.id);
if (ancestors.length === 0) {
return { type: 'error', output: null };
}
const root = await fetchNode(node.root_id);
if (!root) {
const node = ancestors[ancestors.length - 1];
if (!node || node.id !== mutation.id) {
return { type: 'error', output: null };
}
const model = getNodeModel(node.type);
const ydoc = new YDoc(node.state);
ydoc.applyUpdate(mutation.data);
const attributes = ydoc.getObject<NodeAttributes>();
const attributesJson = JSON.stringify(attributes);
if (
!canUpdateNode(
{
const canUpdateNodeContext: CanUpdateAttributesContext = {
user: {
userId: user.id,
id: user.id,
role: user.role,
workspaceId: user.workspace_id,
accountId: user.account_id,
},
root: mapNode(root),
ancestors: ancestors.map(mapNode),
node: mapNode(node),
},
attributes
)
) {
attributes,
};
if (!model.canUpdateAttributes(canUpdateNodeContext)) {
return { type: 'error', output: null };
}
@@ -507,7 +507,7 @@ const tryUpdateNodeFromMutation = async (
eventBus.publish({
type: 'node_updated',
nodeId: mutation.id,
rootId: root.id,
rootId: node.root_id,
workspaceId: user.workspace_id,
});
@@ -544,26 +544,29 @@ export const deleteNode = async (
user: SelectUser,
input: DeleteNodeInput
): Promise<DeleteNodeOutput | null> => {
const node = await fetchNode(input.id);
if (!node) {
const ancestors = await fetchNodeAncestors(input.id);
if (ancestors.length === 0) {
return null;
}
const root = await fetchNode(node.root_id);
if (!root) {
const node = ancestors[ancestors.length - 1];
if (!node || node.id !== input.id) {
return null;
}
if (
!canDeleteNode({
const model = getNodeModel(node.type);
const canDeleteNodeContext: CanDeleteNodeContext = {
user: {
userId: user.id,
id: user.id,
role: user.role,
workspaceId: user.workspace_id,
accountId: user.account_id,
},
root: mapNode(root),
ancestors: ancestors.map(mapNode),
node: mapNode(node),
})
) {
};
if (!model.canDelete(canDeleteNodeContext)) {
return null;
}

View File

@@ -1,124 +1,10 @@
import { isEqual } from 'lodash-es';
import {
extractNodeRole,
Node,
NodeAttributes,
WorkspaceRole,
NodeRole,
NodeType,
} from '../index';
import { extractNodeRole, Node, WorkspaceRole, NodeRole } from '../index';
export type UserInput = {
userId: string;
role: WorkspaceRole;
};
export type CanCreateNodeInput = {
user: UserInput;
root: Node | null;
};
export const canCreateNode = (
input: CanCreateNodeInput,
type: NodeType
): boolean => {
if (input.user.role === 'none') {
return false;
}
if (type === 'chat') {
return true;
}
if (type === 'space') {
return hasWorkspaceRole(input.user.role, 'collaborator');
}
const root = input.root;
if (!root) {
return false;
}
const rootRole = extractNodeRole(root, input.user.userId);
if (!rootRole) {
return false;
}
return hasNodeRole(rootRole, 'editor');
};
export type CanUpdateNodeInput = {
user: UserInput;
root: Node;
node: Node;
};
export const canUpdateNode = (
input: CanUpdateNodeInput,
attributes: NodeAttributes
): boolean => {
if (input.user.role === 'none') {
return false;
}
if (attributes.type === 'chat') {
return false;
}
const rootRole = extractNodeRole(input.root, input.user.userId);
if (!rootRole) {
return false;
}
if (attributes.type === 'space') {
if (input.node.type !== 'space') {
return false;
}
const afterCollaborators = attributes.collaborators;
const beforeCollaborators = input.node.attributes.collaborators;
if (!isEqual(afterCollaborators, beforeCollaborators)) {
return hasNodeRole(rootRole, 'admin');
}
}
return hasNodeRole(rootRole, 'editor');
};
export type CanDeleteNodeInput = {
user: UserInput;
root: Node;
node: Node;
};
export const canDeleteNode = (input: CanDeleteNodeInput): boolean => {
if (input.user.role === 'none') {
return false;
}
const node = input.node;
if (node.attributes.type === 'chat') {
return false;
}
const rootRole = extractNodeRole(input.root, input.user.userId);
if (!rootRole) {
return false;
}
if (node.attributes.type === 'record') {
return node.createdBy === input.user.userId;
}
if (node.attributes.type === 'space') {
return hasNodeRole(rootRole, 'admin');
}
return hasNodeRole(rootRole, 'editor');
};
export const hasWorkspaceRole = (
currentRole: WorkspaceRole,
targetRole: WorkspaceRole
@@ -160,11 +46,11 @@ export const hasNodeRole = (currentRole: NodeRole, targetRole: NodeRole) => {
return currentRole === 'admin' || currentRole === 'editor';
}
if (targetRole === 'commenter') {
if (targetRole === 'collaborator') {
return (
currentRole === 'admin' ||
currentRole === 'editor' ||
currentRole === 'commenter'
currentRole === 'collaborator'
);
}
@@ -172,7 +58,7 @@ export const hasNodeRole = (currentRole: NodeRole, targetRole: NodeRole) => {
return (
currentRole === 'admin' ||
currentRole === 'editor' ||
currentRole === 'commenter' ||
currentRole === 'collaborator' ||
currentRole === 'viewer'
);
}

View File

@@ -1,36 +1,6 @@
import { Block } from '../registry/block';
import { NodeAttributes } from '../registry/nodes';
export type TextResult = {
id: string;
name: string | null;
text: string | null;
};
export const extractNodeText = (
id: string,
attributes: NodeAttributes
): TextResult | undefined => {
// if (attributes.type === 'page') {
// return {
// id,
// name: attributes.name,
// text: extractBlockTexts(id, attributes.content),
// };
// }
// if (attributes.type === 'record') {
// return {
// id,
// name: attributes.name,
// text: extractBlockTexts(id, attributes.content),
// };
// }
return undefined;
};
const extractBlockTexts = (
export const extractBlockTexts = (
entryId: string,
blocks: Record<string, Block> | undefined | null
): string | null => {

View File

@@ -2,6 +2,9 @@ import { z } from 'zod';
import { NodeModel } from './core';
import { extractNodeRole } from '../../lib/nodes';
import { hasNodeRole } from '../../lib/permissions';
import { NodeAttributes } from '.';
export const channelAttributesSchema = z.object({
@@ -16,26 +19,59 @@ export type ChannelAttributes = z.infer<typeof channelAttributesSchema>;
export const channelModel: NodeModel = {
type: 'channel',
attributesSchema: channelAttributesSchema,
canCreate: async (context, _) => {
return context.hasEditorAccess();
canCreate: (context) => {
if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'editor');
},
canUpdate: async (context, _) => {
return context.hasEditorAccess();
canUpdateAttributes: (context) => {
if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'editor');
},
canDelete: async (context, _) => {
return context.hasAdminAccess();
canUpdateDocument: () => {
return false;
},
getName: function (
canDelete: (context) => {
if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'admin');
},
getName: (
_: string,
attributes: NodeAttributes
): string | null | undefined {
): string | null | undefined => {
if (attributes.type !== 'channel') {
return null;
}
return attributes.name;
},
getText: function (_: string, __: NodeAttributes): string | null | undefined {
getAttributesText: (): string | null | undefined => {
return undefined;
},
getDocumentText: (): string | null | undefined => {
return undefined;
},
};

View File

@@ -2,7 +2,7 @@ import { z } from 'zod';
import { NodeModel, nodeRoleEnum } from './core';
import { NodeAttributes } from '.';
import { hasWorkspaceRole } from '../../lib/permissions';
export const chatAttributesSchema = z.object({
type: z.literal('chat'),
@@ -14,19 +14,42 @@ export type ChatAttributes = z.infer<typeof chatAttributesSchema>;
export const chatModel: NodeModel = {
type: 'chat',
attributesSchema: chatAttributesSchema,
canCreate: async (_, __) => {
canCreate: (context) => {
if (!hasWorkspaceRole(context.user.role, 'guest')) {
return false;
}
if (context.attributes.type !== 'chat') {
return false;
}
const collaborators = context.attributes.collaborators;
if (Object.keys(collaborators).length !== 2) {
return false;
}
if (!collaborators[context.user.id]) {
return false;
}
return true;
},
canUpdate: async (_, __) => {
canUpdateAttributes: () => {
return false;
},
canDelete: async (_, __) => {
canUpdateDocument: () => {
return false;
},
getName: function (_: string, __: NodeAttributes): string | null | undefined {
canDelete: () => {
return false;
},
getName: (): string | null | undefined => {
return undefined;
},
getText: function (_: string, __: NodeAttributes): string | null | undefined {
getAttributesText: (): string | null | undefined => {
return undefined;
},
getDocumentText: (): string | null | undefined => {
return undefined;
},
};

View File

@@ -1,13 +1,17 @@
import { z, ZodSchema } from 'zod';
import { WorkspaceRole } from '../../types/workspaces';
import { hasNodeRole } from '../../lib/permissions';
import { extractNodeRole } from '../../lib/nodes';
import { DocumentContent } from '../documents';
import { Node, NodeAttributes } from '.';
export type NodeRole = 'admin' | 'editor' | 'commenter' | 'viewer';
export const nodeRoleEnum = z.enum(['admin', 'editor', 'commenter', 'viewer']);
export type NodeRole = 'admin' | 'editor' | 'collaborator' | 'viewer';
export const nodeRoleEnum = z.enum([
'admin',
'editor',
'collaborator',
'viewer',
]);
export interface NodeMutationUser {
id: string;
@@ -16,54 +20,49 @@ export interface NodeMutationUser {
accountId: string;
}
export class NodeMutationContext {
public readonly user: NodeMutationUser;
public readonly root: Node | null;
public readonly role: NodeRole | null;
constructor(user: NodeMutationUser, root: Node | null) {
this.user = user;
this.root = root;
this.role = root ? extractNodeRole(root, user.id) : null;
}
public hasAdminAccess = () => {
return this.role ? hasNodeRole(this.role, 'admin') : false;
export type CanCreateNodeContext = {
user: NodeMutationUser;
ancestors: Node[];
attributes: NodeAttributes;
};
public hasEditorAccess = () => {
return this.role ? hasNodeRole(this.role, 'editor') : false;
export type CanUpdateAttributesContext = {
user: NodeMutationUser;
ancestors: Node[];
node: Node;
attributes: NodeAttributes;
};
public hasCollaboratorAccess = () => {
return this.role ? hasNodeRole(this.role, 'commenter') : false;
export type CanUpdateDocumentContext = {
user: NodeMutationUser;
ancestors: Node[];
node: Node;
};
public hasViewerAccess = () => {
return this.role ? hasNodeRole(this.role, 'viewer') : false;
export type CanDeleteNodeContext = {
user: NodeMutationUser;
ancestors: Node[];
node: Node;
};
}
export interface NodeModel {
type: string;
attributesSchema: ZodSchema;
documentSchema?: ZodSchema;
canCreate: (
context: NodeMutationContext,
attributes: NodeAttributes
) => Promise<boolean>;
canUpdate: (
context: NodeMutationContext,
node: Node,
attributes: NodeAttributes
) => Promise<boolean>;
canDelete: (context: NodeMutationContext, node: Node) => Promise<boolean>;
canCreate: (context: CanCreateNodeContext) => boolean;
canUpdateAttributes: (context: CanUpdateAttributesContext) => boolean;
canUpdateDocument: (context: CanUpdateDocumentContext) => boolean;
canDelete: (context: CanDeleteNodeContext) => boolean;
getName: (
id: string,
attributes: NodeAttributes
) => string | null | undefined;
getText: (
getAttributesText: (
id: string,
attributes: NodeAttributes
) => string | null | undefined;
getDocumentText: (
id: string,
content: DocumentContent
) => string | null | undefined;
}

View File

@@ -2,7 +2,8 @@ import { z } from 'zod';
import { NodeModel } from './core';
import { NodeAttributes } from '.';
import { extractNodeRole } from '../../lib/nodes';
import { hasNodeRole } from '../../lib/permissions';
export const databaseViewFieldAttributesSchema = z.object({
id: z.string(),
@@ -91,26 +92,56 @@ export type DatabaseViewLayout = 'table' | 'board' | 'calendar';
export const databaseViewModel: NodeModel = {
type: 'database_view',
attributesSchema: databaseViewAttributesSchema,
canCreate: async (context, _) => {
return context.hasEditorAccess();
canCreate: (context) => {
if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'editor');
},
canUpdate: async (context, _) => {
return context.hasEditorAccess();
canUpdateAttributes: (context) => {
if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'editor');
},
canDelete: async (context, _) => {
return context.hasEditorAccess();
canUpdateDocument: () => {
return false;
},
getName: function (
_: string,
attributes: NodeAttributes
): string | null | undefined {
canDelete: (context) => {
if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'editor');
},
getName: (_, attributes) => {
if (attributes.type !== 'database_view') {
return null;
return undefined;
}
return attributes.name;
},
getText: function (_: string, __: NodeAttributes): string | null | undefined {
getAttributesText: () => {
return undefined;
},
getDocumentText: () => {
return undefined;
},
};

View File

@@ -3,7 +3,8 @@ import { z } from 'zod';
import { fieldAttributesSchema } from './field';
import { NodeModel } from './core';
import { NodeAttributes } from '.';
import { extractNodeRole } from '../../lib/nodes';
import { hasNodeRole } from '../../lib/permissions';
export const databaseAttributesSchema = z.object({
type: z.literal('database'),
@@ -18,26 +19,56 @@ export type DatabaseAttributes = z.infer<typeof databaseAttributesSchema>;
export const databaseModel: NodeModel = {
type: 'database',
attributesSchema: databaseAttributesSchema,
canCreate: async (context, _) => {
return context.hasEditorAccess();
canCreate: (context) => {
if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'editor');
},
canUpdate: async (context, _) => {
return context.hasEditorAccess();
canUpdateAttributes: (context) => {
if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'editor');
},
canDelete: async (context, _) => {
return context.hasEditorAccess();
canUpdateDocument: () => {
return false;
},
getName: function (
_: string,
attributes: NodeAttributes
): string | null | undefined {
canDelete: (context) => {
if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'editor');
},
getName: (_, attributes) => {
if (attributes.type !== 'database') {
return null;
return undefined;
}
return attributes.name;
},
getText: function (_: string, __: NodeAttributes): string | null | undefined {
getAttributesText: () => {
return undefined;
},
getDocumentText: () => {
return undefined;
},
};

View File

@@ -2,7 +2,8 @@ import { z } from 'zod';
import { NodeModel } from './core';
import { NodeAttributes } from '.';
import { extractNodeRole } from '../../lib/nodes';
import { hasNodeRole } from '../../lib/permissions';
export const fileAttributesSchema = z.object({
type: z.literal('file'),
@@ -16,26 +17,71 @@ export type FileAttributes = z.infer<typeof fileAttributesSchema>;
export const fileModel: NodeModel = {
type: 'file',
attributesSchema: fileAttributesSchema,
canCreate: async (context, _) => {
return context.hasEditorAccess();
canCreate: (context) => {
if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
const parent = context.ancestors[context.ancestors.length - 1]!;
if (parent.type === 'message') {
return hasNodeRole(role, 'collaborator');
}
return hasNodeRole(role, 'editor');
},
canUpdate: async (context, _) => {
return context.hasEditorAccess();
canUpdateAttributes: (context) => {
if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
const parent = context.ancestors[context.ancestors.length - 1]!;
if (parent.type === 'message') {
return parent.createdBy === context.user.id || hasNodeRole(role, 'admin');
}
return hasNodeRole(role, 'editor');
},
canDelete: async (context, _) => {
return context.hasEditorAccess();
canUpdateDocument: () => {
return false;
},
getName: function (
_: string,
attributes: NodeAttributes
): string | null | undefined {
canDelete: (context) => {
if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
const parent = context.ancestors[context.ancestors.length - 1]!;
if (parent.type === 'message') {
return parent.createdBy === context.user.id || hasNodeRole(role, 'admin');
}
return hasNodeRole(role, 'editor');
},
getName: (_, attributes) => {
if (attributes.type !== 'file') {
return null;
}
return attributes.name;
},
getText: function (_: string, __: NodeAttributes): string | null | undefined {
getAttributesText: () => {
return undefined;
},
getDocumentText: () => {
return undefined;
},
};

View File

@@ -2,7 +2,8 @@ import { z } from 'zod';
import { NodeModel } from './core';
import { NodeAttributes } from '.';
import { extractNodeRole } from '../../lib/nodes';
import { hasNodeRole } from '../../lib/permissions';
export const folderAttributesSchema = z.object({
type: z.literal('folder'),
@@ -16,26 +17,56 @@ export type FolderAttributes = z.infer<typeof folderAttributesSchema>;
export const folderModel: NodeModel = {
type: 'folder',
attributesSchema: folderAttributesSchema,
canCreate: async (context, _) => {
return context.hasEditorAccess();
canCreate: (context) => {
if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'editor');
},
canUpdate: async (context, _) => {
return context.hasEditorAccess();
canUpdateAttributes: (context) => {
if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'editor');
},
canDelete: async (context, _) => {
return context.hasEditorAccess();
canUpdateDocument: () => {
return false;
},
getName: function (
_: string,
attributes: NodeAttributes
): string | null | undefined {
canDelete: (context) => {
if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'admin');
},
getName: (_, attributes) => {
if (attributes.type !== 'folder') {
return null;
return undefined;
}
return attributes.name;
},
getText: function (_: string, __: NodeAttributes): string | null | undefined {
getAttributesText: () => {
return undefined;
},
getDocumentText: () => {
return undefined;
},
};

View File

@@ -3,8 +3,9 @@ import { z } from 'zod';
import { NodeModel } from './core';
import { blockSchema } from '../block';
import { NodeAttributes } from '.';
import { extractBlockTexts } from '../../lib/texts';
import { extractNodeRole } from '../../lib/nodes';
import { hasNodeRole } from '../../lib/permissions';
export const messageAttributesSchema = z.object({
type: z.literal('message'),
@@ -20,26 +21,67 @@ export type MessageAttributes = z.infer<typeof messageAttributesSchema>;
export const messageModel: NodeModel = {
type: 'message',
attributesSchema: messageAttributesSchema,
canCreate: async (context, _) => {
return context.hasCollaboratorAccess();
canCreate: (context) => {
if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'collaborator');
},
canUpdate: async (context, _) => {
return context.hasCollaboratorAccess();
canUpdateAttributes: (context) => {
if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return context.node.createdBy === context.user.id;
},
canDelete: async (context, _) => {
return context.hasCollaboratorAccess();
canUpdateDocument: () => {
return false;
},
getName: function (
_: string,
attributes: NodeAttributes
): string | null | undefined {
canDelete: (context) => {
if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return (
context.node.createdBy === context.user.id || hasNodeRole(role, 'admin')
);
},
getName: (_, attributes) => {
if (attributes.type !== 'message') {
return null;
return undefined;
}
return attributes.name;
},
getText: function (_: string, __: NodeAttributes): string | null | undefined {
getAttributesText: (id, attributes) => {
if (attributes.type !== 'message') {
return undefined;
}
const text = extractBlockTexts(id, attributes.content);
if (!text) {
return null;
}
return text;
},
getDocumentText: () => {
return undefined;
},
};

View File

@@ -3,8 +3,8 @@ import { z } from 'zod';
import { NodeModel } from './core';
import { richTextContentSchema } from '../documents/rich-text';
import { NodeAttributes } from '.';
import { extractNodeRole } from '../../lib/nodes';
import { hasNodeRole } from '../../lib/permissions';
export const pageAttributesSchema = z.object({
type: z.literal('page'),
@@ -19,26 +19,69 @@ export const pageModel: NodeModel = {
type: 'page',
attributesSchema: pageAttributesSchema,
documentSchema: richTextContentSchema,
canCreate: async (context, _) => {
return context.hasEditorAccess();
canCreate: (context) => {
if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'editor');
},
canUpdate: async (context, _) => {
return context.hasEditorAccess();
canUpdateAttributes: (context) => {
if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'editor');
},
canDelete: async (context, _) => {
return context.hasEditorAccess();
canUpdateDocument: (context) => {
if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'editor');
},
getName: function (
_: string,
attributes: NodeAttributes
): string | null | undefined {
canDelete: (context) => {
if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'admin');
},
getName: (_, attributes) => {
if (attributes.type !== 'page') {
return null;
return undefined;
}
return attributes.name;
},
getText: function (_: string, __: NodeAttributes): string | null | undefined {
getAttributesText: (id, attributes) => {
if (attributes.type !== 'page') {
return undefined;
}
return attributes.name;
},
getDocumentText: () => {
return undefined;
},
};

View File

@@ -4,8 +4,8 @@ import { fieldValueSchema } from './field-value';
import { NodeModel } from './core';
import { richTextContentSchema } from '../documents/rich-text';
import { NodeAttributes } from '.';
import { extractNodeRole } from '../../lib/nodes';
import { hasNodeRole } from '../../lib/permissions';
export const recordAttributesSchema = z.object({
type: z.literal('record'),
@@ -22,26 +22,81 @@ export const recordModel: NodeModel = {
type: 'record',
attributesSchema: recordAttributesSchema,
documentSchema: richTextContentSchema,
canCreate: async (context, _) => {
return context.hasCollaboratorAccess();
canCreate: (context) => {
if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'collaborator');
},
canUpdate: async (context, _) => {
return context.hasCollaboratorAccess();
canUpdateAttributes: (context) => {
if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
if (context.node.createdBy === context.user.id) {
return true;
}
return hasNodeRole(role, 'editor');
},
canDelete: async (context, _) => {
return context.hasCollaboratorAccess();
canUpdateDocument: (context) => {
if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
if (context.node.createdBy === context.user.id) {
return true;
}
return hasNodeRole(role, 'editor');
},
getName: function (
_: string,
attributes: NodeAttributes
): string | null | undefined {
canDelete: (context) => {
if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
if (context.node.createdBy === context.user.id) {
return true;
}
return hasNodeRole(role, 'admin');
},
getName: (_, attributes) => {
if (attributes.type !== 'record') {
return null;
return undefined;
}
return attributes.name;
},
getText: function (_: string, __: NodeAttributes): string | null | undefined {
getAttributesText: (id, attributes) => {
if (attributes.type !== 'record') {
return undefined;
}
return attributes.name;
},
getDocumentText: () => {
return undefined;
},
};

View File

@@ -2,7 +2,8 @@ import { z } from 'zod';
import { NodeModel, nodeRoleEnum } from './core';
import { NodeAttributes } from '.';
import { extractNodeRole } from '../../lib/nodes';
import { hasNodeRole, hasWorkspaceRole } from '../../lib/permissions';
export const spaceAttributesSchema = z.object({
type: z.literal('space'),
@@ -18,26 +19,72 @@ export type SpaceAttributes = z.infer<typeof spaceAttributesSchema>;
export const spaceModel: NodeModel = {
type: 'space',
attributesSchema: spaceAttributesSchema,
canCreate: async (context, _) => {
return context.hasCollaboratorAccess();
canCreate: (context) => {
if (context.ancestors.length > 0) {
return false;
}
if (!hasWorkspaceRole(context.user.role, 'collaborator')) {
return false;
}
if (context.attributes.type !== 'space') {
return false;
}
const collaborators = context.attributes.collaborators;
if (Object.keys(collaborators).length === 0) {
return false;
}
if (collaborators[context.user.id] !== 'admin') {
return false;
}
return true;
},
canUpdate: async (context, _) => {
return context.hasCollaboratorAccess();
canUpdateAttributes: (context) => {
if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'admin');
},
canDelete: async (context, _) => {
return context.hasCollaboratorAccess();
canUpdateDocument: () => {
return false;
},
getName: function (
_: string,
attributes: NodeAttributes
): string | null | undefined {
canDelete: (context) => {
if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'admin');
},
getName: (_, attributes) => {
if (attributes.type !== 'space') {
return null;
return undefined;
}
return attributes.name;
},
getText: function (_: string, __: NodeAttributes): string | null | undefined {
getAttributesText: (id, attributes) => {
if (attributes.type !== 'space') {
return undefined;
}
return attributes.name;
},
getDocumentText: () => {
return undefined;
},
};