mirror of
https://github.com/colanode/colanode.git
synced 2025-12-29 00:25:03 +01:00
Improve workspace user role checks
This commit is contained in:
@@ -67,6 +67,7 @@ export class ServerNodeSyncMutationHandler
|
||||
socketManager.sendMessage(workspace.account_id, {
|
||||
type: 'local_node_sync',
|
||||
nodeId: input.id,
|
||||
userId: userId,
|
||||
versionId: input.versionId,
|
||||
workspaceId: input.workspaceId,
|
||||
});
|
||||
@@ -112,6 +113,7 @@ export class ServerNodeSyncMutationHandler
|
||||
socketManager.sendMessage(workspace.account_id, {
|
||||
type: 'local_node_sync',
|
||||
nodeId: input.id,
|
||||
userId: userId,
|
||||
versionId: input.versionId,
|
||||
workspaceId: input.workspaceId,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export type LocalNodeSyncMessageInput = {
|
||||
type: 'local_node_sync';
|
||||
nodeId: string;
|
||||
userId: string;
|
||||
versionId: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
@@ -36,6 +36,7 @@ interface WorkspaceFormProps {
|
||||
isSaving: boolean;
|
||||
onCancel?: () => void;
|
||||
saveText: string;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
export const WorkspaceForm = ({
|
||||
@@ -44,6 +45,7 @@ export const WorkspaceForm = ({
|
||||
isSaving,
|
||||
onCancel,
|
||||
saveText,
|
||||
readOnly = false,
|
||||
}: WorkspaceFormProps) => {
|
||||
const account = useAccount();
|
||||
|
||||
@@ -71,7 +73,7 @@ export const WorkspaceForm = ({
|
||||
<div
|
||||
className="group relative cursor-pointer"
|
||||
onClick={async () => {
|
||||
if (isPending || isFileDialogOpen) {
|
||||
if (isPending || isFileDialogOpen || readOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -122,7 +124,8 @@ export const WorkspaceForm = ({
|
||||
<div
|
||||
className={cn(
|
||||
`absolute left-0 top-0 hidden h-32 w-32 items-center justify-center overflow-hidden bg-gray-50 group-hover:inline-flex`,
|
||||
isPending ? 'inline-flex' : 'hidden'
|
||||
isPending ? 'inline-flex' : 'hidden',
|
||||
readOnly && 'hidden group-hover:hidden'
|
||||
)}
|
||||
>
|
||||
{isPending ? (
|
||||
@@ -141,7 +144,7 @@ export const WorkspaceForm = ({
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>Name *</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Name" {...field} />
|
||||
<Input readOnly={readOnly} placeholder="Name" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -155,6 +158,7 @@ export const WorkspaceForm = ({
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
readOnly={readOnly}
|
||||
placeholder="Write a short description about the workspace"
|
||||
{...field}
|
||||
/>
|
||||
@@ -165,26 +169,27 @@ export const WorkspaceForm = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!readOnly && (
|
||||
<div className="flex flex-row justify-end gap-2">
|
||||
{onCancel && (
|
||||
<Button
|
||||
type="button"
|
||||
disabled={isPending || isSaving}
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
onCancel();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div className="flex flex-row justify-end gap-2">
|
||||
{onCancel && (
|
||||
<Button
|
||||
type="button"
|
||||
disabled={isPending || isSaving}
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
onCancel();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
<Button type="submit" disabled={isPending || isSaving}>
|
||||
{isSaving && <Spinner className="mr-1" />}
|
||||
{saveText}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button type="submit" disabled={isPending || isSaving}>
|
||||
{isSaving && <Spinner className="mr-1" />}
|
||||
{saveText}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
|
||||
@@ -35,6 +35,8 @@ export const WorkspaceSettingsDialog = ({
|
||||
const workspace = useWorkspace();
|
||||
const [tab, setTab] = React.useState<'info' | 'users' | 'delete'>('info');
|
||||
|
||||
const canDelete = workspace.role === 'owner';
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
@@ -87,15 +89,17 @@ export const WorkspaceSettingsDialog = ({
|
||||
<span>Users</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
isActive={tab === 'delete'}
|
||||
onClick={() => setTab('delete')}
|
||||
>
|
||||
<Trash2 className="mr-2 size-4" />
|
||||
<span>Delete</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
{canDelete && (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
isActive={tab === 'delete'}
|
||||
onClick={() => setTab('delete')}
|
||||
>
|
||||
<Trash2 className="mr-2 size-4" />
|
||||
<span>Delete</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
|
||||
@@ -6,9 +6,11 @@ import { WorkspaceForm } from './workspace-form';
|
||||
export const WorkspaceUpdate = () => {
|
||||
const workspace = useWorkspace();
|
||||
const { mutate, isPending } = useMutation();
|
||||
const canEdit = workspace.role === 'owner';
|
||||
|
||||
return (
|
||||
<WorkspaceForm
|
||||
readOnly={!canEdit}
|
||||
values={{
|
||||
name: workspace.name,
|
||||
description: workspace.description ?? '',
|
||||
|
||||
@@ -20,15 +20,15 @@ interface WorkspaceRoleItem {
|
||||
|
||||
const roles: WorkspaceRoleItem[] = [
|
||||
{
|
||||
name: 'Admin',
|
||||
value: 'admin',
|
||||
description: 'Administration access',
|
||||
name: 'Owner',
|
||||
value: 'owner',
|
||||
description: 'Full access',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'Editor',
|
||||
value: 'editor',
|
||||
description: 'Can edit content',
|
||||
name: 'Admin',
|
||||
value: 'admin',
|
||||
description: 'Administration access',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
@@ -38,26 +38,42 @@ const roles: WorkspaceRoleItem[] = [
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'Viewer',
|
||||
value: 'viewer',
|
||||
name: 'Guest',
|
||||
value: 'guest',
|
||||
description: 'Can view content',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'No access',
|
||||
value: 'none',
|
||||
description: 'No access to workspace',
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
interface WorkspaceUserRoleDropdownProps {
|
||||
userId: string;
|
||||
value: WorkspaceRole;
|
||||
canEdit: boolean;
|
||||
}
|
||||
|
||||
export const WorkspaceUserRoleDropdown = ({
|
||||
userId,
|
||||
value,
|
||||
canEdit,
|
||||
}: WorkspaceUserRoleDropdownProps) => {
|
||||
const workspace = useWorkspace();
|
||||
const { mutate, isPending } = useMutation();
|
||||
console.log('canEdit', canEdit);
|
||||
|
||||
const currentRole = roles.find((role) => role.value === value);
|
||||
|
||||
if (!canEdit) {
|
||||
return (
|
||||
<p className="p-1 text-sm text-muted-foreground">{currentRole?.name}</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -70,46 +86,52 @@ export const WorkspaceUserRoleDropdown = ({
|
||||
)}
|
||||
</p>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{roles.map((role) => (
|
||||
<DropdownMenuItem
|
||||
key={role.value}
|
||||
onSelect={() => {
|
||||
if (isPending) {
|
||||
return;
|
||||
}
|
||||
<DropdownMenuContent className="w-56">
|
||||
{roles
|
||||
.filter((role) => role.enabled)
|
||||
.map((role) => (
|
||||
<DropdownMenuItem
|
||||
key={role.value}
|
||||
onSelect={() => {
|
||||
if (isPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
mutate({
|
||||
input: {
|
||||
type: 'workspace_user_role_update',
|
||||
userToUpdateId: userId,
|
||||
role: role.value,
|
||||
userId: workspace.userId,
|
||||
},
|
||||
onError() {
|
||||
toast({
|
||||
title: 'Failed to update role',
|
||||
description:
|
||||
'Something went wrong updating user role. Please try again!',
|
||||
variant: 'destructive',
|
||||
});
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full flex-row items-center justify-between">
|
||||
<div className="flex w-full flex-col">
|
||||
<p className="mb-1 text-sm font-medium leading-none">
|
||||
{role.name}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{role.description}
|
||||
</p>
|
||||
if (role.value === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
mutate({
|
||||
input: {
|
||||
type: 'workspace_user_role_update',
|
||||
userToUpdateId: userId,
|
||||
role: role.value,
|
||||
userId: workspace.userId,
|
||||
},
|
||||
onError() {
|
||||
toast({
|
||||
title: 'Failed to update role',
|
||||
description:
|
||||
'Something went wrong updating user role. Please try again!',
|
||||
variant: 'destructive',
|
||||
});
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full flex-row items-center justify-between">
|
||||
<div className="flex flex-1 w-full flex-col">
|
||||
<p className="mb-1 text-sm font-medium leading-none">
|
||||
{role.name}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{role.description}
|
||||
</p>
|
||||
</div>
|
||||
{value === role.value && <Check className="size-4" />}
|
||||
</div>
|
||||
{value === role.value && <Check className="mr-2 size-4" />}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import { useInfiniteQuery } from '@/renderer/hooks/use-infinite-query';
|
||||
import { Separator } from '@/renderer/components/ui/separator';
|
||||
import { WorkspaceUserInvite } from '@/renderer/components/workspaces/workspace-user-invite';
|
||||
@@ -12,6 +13,7 @@ const USERS_PER_PAGE = 50;
|
||||
|
||||
export const WorkspaceUsers = () => {
|
||||
const workspace = useWorkspace();
|
||||
const canEditUsers = workspace.role === 'owner' || workspace.role === 'admin';
|
||||
|
||||
const { data, isPending, hasNextPage, fetchNextPage, isFetchingNextPage } =
|
||||
useInfiniteQuery({
|
||||
@@ -47,8 +49,12 @@ export const WorkspaceUsers = () => {
|
||||
const users = data?.flatMap((page) => page) ?? [];
|
||||
return (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<WorkspaceUserInvite />
|
||||
<Separator />
|
||||
{canEditUsers && (
|
||||
<React.Fragment>
|
||||
<WorkspaceUserInvite />
|
||||
<Separator />
|
||||
</React.Fragment>
|
||||
)}
|
||||
<div>
|
||||
<p>Users</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
@@ -75,7 +81,11 @@ export const WorkspaceUsers = () => {
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">{email}</p>
|
||||
</div>
|
||||
<WorkspaceUserRoleDropdown userId={user.id} value={role} />
|
||||
<WorkspaceUserRoleDropdown
|
||||
userId={user.id}
|
||||
value={role}
|
||||
canEdit={canEditUsers}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -3,9 +3,9 @@ import { ServerNode } from '@/types/nodes';
|
||||
export type WorkspaceRole =
|
||||
| 'owner'
|
||||
| 'admin'
|
||||
| 'editor'
|
||||
| 'collaborator'
|
||||
| 'viewer';
|
||||
| 'guest'
|
||||
| 'none';
|
||||
|
||||
export type Workspace = {
|
||||
id: string;
|
||||
@@ -41,6 +41,10 @@ export type WorkspaceUserRoleUpdateOutput = {
|
||||
user: ServerNode;
|
||||
};
|
||||
|
||||
export type WorkspaceUserRemoveOutput = {
|
||||
user: ServerNode;
|
||||
};
|
||||
|
||||
export type SidebarNode = {
|
||||
id: string;
|
||||
type: string;
|
||||
|
||||
@@ -234,13 +234,18 @@ const createDeviceNodesTable: Migration = {
|
||||
await db.schema
|
||||
.createTable('device_nodes')
|
||||
.addColumn('device_id', 'varchar(30)', (col) => col.notNull())
|
||||
.addColumn('user_id', 'varchar(30)', (col) => col.notNull())
|
||||
.addColumn('node_id', 'varchar(30)', (col) => col.notNull())
|
||||
.addColumn('workspace_id', 'varchar(30)', (col) => col.notNull())
|
||||
.addColumn('node_version_id', 'varchar(30)')
|
||||
.addColumn('user_node_version_id', 'varchar(30)')
|
||||
.addColumn('node_synced_at', 'timestamptz')
|
||||
.addColumn('user_node_synced_at', 'timestamptz')
|
||||
.addPrimaryKeyConstraint('device_nodes_pkey', ['device_id', 'node_id'])
|
||||
.addPrimaryKeyConstraint('device_nodes_pkey', [
|
||||
'device_id',
|
||||
'user_id',
|
||||
'node_id',
|
||||
])
|
||||
.execute();
|
||||
},
|
||||
down: async (db) => {
|
||||
|
||||
@@ -132,8 +132,9 @@ export type CreateUserNode = Insertable<UserNodeTable>;
|
||||
export type UpdateUserNode = Updateable<UserNodeTable>;
|
||||
|
||||
interface DeviceNodeTable {
|
||||
node_id: ColumnType<string, string, never>;
|
||||
device_id: ColumnType<string, string, never>;
|
||||
user_id: ColumnType<string, string, never>;
|
||||
node_id: ColumnType<string, string, never>;
|
||||
workspace_id: ColumnType<string, string, string>;
|
||||
node_version_id: ColumnType<string | null, string | null, string | null>;
|
||||
user_node_version_id: ColumnType<string | null, string | null, string | null>;
|
||||
|
||||
@@ -59,7 +59,7 @@ export const createDefaultWorkspace = async (account: SelectAccount) => {
|
||||
id: user.id,
|
||||
account_id: account.id,
|
||||
workspace_id: workspaceId,
|
||||
role: WorkspaceRole.Owner,
|
||||
role: 'owner',
|
||||
created_at: createdAt,
|
||||
created_by: account.id,
|
||||
status: WorkspaceUserStatus.Active,
|
||||
|
||||
@@ -3,7 +3,12 @@ import { redisConfig } from '@/data/redis';
|
||||
import { CreateUserNode } from '@/data/schema';
|
||||
import { filesStorage } from '@/data/storage';
|
||||
import { BUCKET_NAMES } from '@/data/storage';
|
||||
import { generateId, IdType, NodeAttributes, NodeTypes } from '@colanode/core';
|
||||
import {
|
||||
extractNodeCollaborators,
|
||||
generateId,
|
||||
IdType,
|
||||
NodeTypes,
|
||||
} from '@colanode/core';
|
||||
import { fetchNodeCollaborators, fetchWorkspaceUsers } from '@/lib/nodes';
|
||||
import { synapse } from '@/services/synapse';
|
||||
import {
|
||||
@@ -15,6 +20,7 @@ import {
|
||||
import { DeleteObjectCommand } from '@aws-sdk/client-s3';
|
||||
import { Job, Queue, Worker } from 'bullmq';
|
||||
import { difference } from 'lodash-es';
|
||||
import { enqueueTask } from './tasks';
|
||||
|
||||
const eventQueue = new Queue('events', {
|
||||
connection: {
|
||||
@@ -71,6 +77,8 @@ const handleNodeUpdatedEvent = async (
|
||||
event: NodeUpdatedEvent
|
||||
): Promise<void> => {
|
||||
await checkForCollaboratorsChange(event);
|
||||
await checkForUserRoleChange(event);
|
||||
|
||||
await synapse.sendSynapseMessage({
|
||||
type: 'node_update',
|
||||
nodeId: event.id,
|
||||
@@ -194,8 +202,16 @@ const createUserNodes = async (event: NodeCreatedEvent): Promise<void> => {
|
||||
const checkForCollaboratorsChange = async (
|
||||
event: NodeUpdatedEvent
|
||||
): Promise<void> => {
|
||||
const beforeCollaborators = extractCollaboratorIds(event.beforeAttributes);
|
||||
const afterCollaborators = extractCollaboratorIds(event.afterAttributes);
|
||||
const beforeCollaborators = Object.keys(
|
||||
extractNodeCollaborators(event.beforeAttributes)
|
||||
);
|
||||
const afterCollaborators = Object.keys(
|
||||
extractNodeCollaborators(event.afterAttributes)
|
||||
);
|
||||
|
||||
if (beforeCollaborators.length === 0 && afterCollaborators.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const addedCollaborators = difference(
|
||||
afterCollaborators,
|
||||
@@ -289,10 +305,28 @@ const checkForCollaboratorsChange = async (
|
||||
}
|
||||
};
|
||||
|
||||
const extractCollaboratorIds = (collaborators: NodeAttributes) => {
|
||||
if ('collaborators' in collaborators && collaborators.collaborators) {
|
||||
return Object.keys(collaborators.collaborators).sort();
|
||||
const checkForUserRoleChange = async (
|
||||
event: NodeUpdatedEvent
|
||||
): Promise<void> => {
|
||||
if (
|
||||
event.beforeAttributes.type !== 'user' ||
|
||||
event.afterAttributes.type !== 'user'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return [];
|
||||
const beforeRole = event.beforeAttributes.role;
|
||||
const afterRole = event.afterAttributes.role;
|
||||
|
||||
if (beforeRole === afterRole) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (afterRole === 'none') {
|
||||
await enqueueTask({
|
||||
type: 'clean_user_device_nodes',
|
||||
userId: event.id,
|
||||
workspaceId: event.workspaceId,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { database } from '@/data/database';
|
||||
import { redisConfig } from '@/data/redis';
|
||||
import { CleanDeviceDataTask, SendEmailTask, Task } from '@/types/tasks';
|
||||
import {
|
||||
CleanDeviceDataTask,
|
||||
CleanUserDeviceNodesTask,
|
||||
SendEmailTask,
|
||||
Task,
|
||||
} from '@/types/tasks';
|
||||
import { Job, Queue, Worker } from 'bullmq';
|
||||
import { sendEmail } from '@/services/email';
|
||||
|
||||
@@ -39,11 +44,13 @@ const handleTaskJob = async (job: Job) => {
|
||||
return handleCleanDeviceDataTask(task);
|
||||
case 'send_email':
|
||||
return handleSendEmailTask(task);
|
||||
case 'clean_user_device_nodes':
|
||||
return handleCleanUserDeviceNodesTask(task);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCleanDeviceDataTask = async (
|
||||
task: CleanDeviceDataTask,
|
||||
task: CleanDeviceDataTask
|
||||
): Promise<void> => {
|
||||
const device = await database
|
||||
.selectFrom('devices')
|
||||
@@ -62,8 +69,39 @@ const handleCleanDeviceDataTask = async (
|
||||
.execute();
|
||||
};
|
||||
|
||||
const handleSendEmailTask = async (
|
||||
task: SendEmailTask
|
||||
): Promise<void> => {
|
||||
const handleSendEmailTask = async (task: SendEmailTask): Promise<void> => {
|
||||
await sendEmail(task.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCleanUserDeviceNodesTask = async (
|
||||
task: CleanUserDeviceNodesTask
|
||||
): Promise<void> => {
|
||||
const workspaceUser = await database
|
||||
.selectFrom('workspace_users')
|
||||
.where('id', '=', task.userId)
|
||||
.where('workspace_id', '=', task.workspaceId)
|
||||
.selectAll()
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!workspaceUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const devices = await database
|
||||
.selectFrom('devices')
|
||||
.select('id')
|
||||
.where('account_id', '=', task.userId)
|
||||
.execute();
|
||||
|
||||
if (devices.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deviceIds = devices.map((d) => d.id);
|
||||
|
||||
await database
|
||||
.deleteFrom('device_nodes')
|
||||
.where('device_id', 'in', deviceIds)
|
||||
.where('user_id', '=', task.userId)
|
||||
.execute();
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@ import { getNameFromEmail } from '@/lib/utils';
|
||||
import { AccountStatus } from '@/types/accounts';
|
||||
import { ServerNode } from '@/types/nodes';
|
||||
import { mapServerNode } from '@/lib/nodes';
|
||||
import { NodeCreatedEvent } from '@/types/events';
|
||||
import { NodeCreatedEvent, NodeUpdatedEvent } from '@/types/events';
|
||||
import { enqueueEvent } from '@/queues/events';
|
||||
import { YDoc } from '@colanode/crdt';
|
||||
|
||||
@@ -91,7 +91,7 @@ workspacesRouter.post(
|
||||
avatar: account.avatar,
|
||||
email: account.email,
|
||||
accountId: account.id,
|
||||
role: WorkspaceRole.Owner,
|
||||
role: 'owner',
|
||||
parentId: workspaceId,
|
||||
};
|
||||
|
||||
@@ -118,7 +118,7 @@ workspacesRouter.post(
|
||||
id: userId,
|
||||
account_id: account.id,
|
||||
workspace_id: workspaceId,
|
||||
role: WorkspaceRole.Owner,
|
||||
role: 'owner',
|
||||
created_at: createdAt,
|
||||
created_by: account.id,
|
||||
status: WorkspaceUserStatus.Active,
|
||||
@@ -190,7 +190,7 @@ workspacesRouter.post(
|
||||
user: {
|
||||
id: userId,
|
||||
accountId: account.id,
|
||||
role: WorkspaceRole.Owner,
|
||||
role: 'owner',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -238,7 +238,7 @@ workspacesRouter.put(
|
||||
});
|
||||
}
|
||||
|
||||
if (workspaceUser.role !== WorkspaceRole.Owner) {
|
||||
if (workspaceUser.role !== 'owner') {
|
||||
return res.status(403).json({
|
||||
code: ApiError.Forbidden,
|
||||
message: 'Forbidden.',
|
||||
@@ -321,7 +321,7 @@ workspacesRouter.delete(
|
||||
});
|
||||
}
|
||||
|
||||
if (workspaceUser.role !== WorkspaceRole.Owner) {
|
||||
if (workspaceUser.role !== 'owner') {
|
||||
return res.status(403).json({
|
||||
code: ApiError.Forbidden,
|
||||
message: 'Forbidden.',
|
||||
@@ -492,10 +492,7 @@ workspacesRouter.post(
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
workspaceUser.role !== WorkspaceRole.Owner &&
|
||||
workspaceUser.role !== WorkspaceRole.Admin
|
||||
) {
|
||||
if (workspaceUser.role !== 'owner' && workspaceUser.role !== 'admin') {
|
||||
return res.status(403).json({
|
||||
code: ApiError.Forbidden,
|
||||
message: 'Forbidden.',
|
||||
@@ -593,7 +590,7 @@ workspacesRouter.post(
|
||||
name: account!.name,
|
||||
avatar: account!.avatar,
|
||||
email: account!.email,
|
||||
role: WorkspaceRole.Collaborator,
|
||||
role: 'collaborator',
|
||||
accountId: account!.id,
|
||||
parentId: workspace.id,
|
||||
};
|
||||
@@ -604,7 +601,7 @@ workspacesRouter.post(
|
||||
id: userId,
|
||||
account_id: account!.id,
|
||||
workspace_id: workspace.id,
|
||||
role: WorkspaceRole.Collaborator,
|
||||
role: 'collaborator',
|
||||
created_at: new Date(),
|
||||
created_by: req.account.id,
|
||||
status: WorkspaceUserStatus.Active,
|
||||
@@ -724,8 +721,8 @@ workspacesRouter.put(
|
||||
}
|
||||
|
||||
if (
|
||||
currentWorkspaceUser.role !== WorkspaceRole.Owner &&
|
||||
currentWorkspaceUser.role !== WorkspaceRole.Admin
|
||||
currentWorkspaceUser.role !== 'owner' &&
|
||||
currentWorkspaceUser.role !== 'admin'
|
||||
) {
|
||||
return res.status(403).json({
|
||||
code: ApiError.Forbidden,
|
||||
@@ -817,6 +814,20 @@ workspacesRouter.put(
|
||||
.execute();
|
||||
});
|
||||
|
||||
const event: NodeUpdatedEvent = {
|
||||
type: 'node_updated',
|
||||
id: userNode.id,
|
||||
workspaceId: userNode.workspaceId,
|
||||
beforeAttributes: attributes,
|
||||
afterAttributes: userDoc.getAttributes(),
|
||||
updatedBy: currentWorkspaceUser.id,
|
||||
updatedAt: updatedAt.toISOString(),
|
||||
serverUpdatedAt: updatedAt.toISOString(),
|
||||
versionId: userNode.versionId,
|
||||
};
|
||||
|
||||
await enqueueEvent(event);
|
||||
|
||||
return res.status(200).json({
|
||||
user: userNode,
|
||||
});
|
||||
|
||||
@@ -112,6 +112,7 @@ class SynapseService {
|
||||
.insertInto('device_nodes')
|
||||
.values({
|
||||
node_id: message.nodeId,
|
||||
user_id: message.userId,
|
||||
device_id: connection.deviceId,
|
||||
node_version_id: message.versionId,
|
||||
user_node_version_id: null,
|
||||
@@ -120,7 +121,7 @@ class SynapseService {
|
||||
node_synced_at: new Date(),
|
||||
})
|
||||
.onConflict((cb) =>
|
||||
cb.columns(['node_id', 'device_id']).doUpdateSet({
|
||||
cb.columns(['node_id', 'user_id', 'device_id']).doUpdateSet({
|
||||
workspace_id: message.workspaceId,
|
||||
node_version_id: message.versionId,
|
||||
node_synced_at: new Date(),
|
||||
@@ -132,6 +133,7 @@ class SynapseService {
|
||||
.insertInto('device_nodes')
|
||||
.values({
|
||||
node_id: message.nodeId,
|
||||
user_id: message.userId,
|
||||
device_id: connection.deviceId,
|
||||
node_version_id: null,
|
||||
user_node_version_id: message.versionId,
|
||||
@@ -140,7 +142,7 @@ class SynapseService {
|
||||
node_synced_at: new Date(),
|
||||
})
|
||||
.onConflict((cb) =>
|
||||
cb.columns(['node_id', 'device_id']).doUpdateSet({
|
||||
cb.columns(['node_id', 'user_id', 'device_id']).doUpdateSet({
|
||||
workspace_id: message.workspaceId,
|
||||
user_node_version_id: message.versionId,
|
||||
user_node_synced_at: new Date(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export type LocalNodeSyncMessageInput = {
|
||||
type: 'local_node_sync';
|
||||
nodeId: string;
|
||||
userId: string;
|
||||
versionId: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { EmailMessage } from '@/types/email';
|
||||
|
||||
export type Task = CleanDeviceDataTask | SendEmailTask;
|
||||
export type Task =
|
||||
| CleanDeviceDataTask
|
||||
| SendEmailTask
|
||||
| CleanUserDeviceNodesTask;
|
||||
|
||||
export type CleanDeviceDataTask = {
|
||||
type: 'clean_device_data';
|
||||
@@ -10,4 +13,10 @@ export type CleanDeviceDataTask = {
|
||||
export type SendEmailTask = {
|
||||
type: 'send_email';
|
||||
message: EmailMessage;
|
||||
};
|
||||
};
|
||||
|
||||
export type CleanUserDeviceNodesTask = {
|
||||
type: 'clean_user_device_nodes';
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
@@ -3,12 +3,12 @@ export enum WorkspaceStatus {
|
||||
Inactive = 2,
|
||||
}
|
||||
|
||||
export enum WorkspaceRole {
|
||||
Owner = 'owner',
|
||||
Admin = 'admin',
|
||||
Collaborator = 'collaborator',
|
||||
Viewer = 'viewer',
|
||||
}
|
||||
export type WorkspaceRole =
|
||||
| 'owner'
|
||||
| 'admin'
|
||||
| 'collaborator'
|
||||
| 'guest'
|
||||
| 'none';
|
||||
|
||||
export enum WorkspaceUserStatus {
|
||||
Active = 1,
|
||||
|
||||
@@ -8,7 +8,7 @@ export const userAttributesSchema = z.object({
|
||||
email: z.string().email(),
|
||||
avatar: z.string().nullable(),
|
||||
accountId: z.string(),
|
||||
role: z.enum(['owner', 'admin', 'editor', 'collaborator', 'viewer']),
|
||||
role: z.enum(['owner', 'admin', 'collaborator', 'guest', 'none']),
|
||||
});
|
||||
|
||||
export type UserAttributes = z.infer<typeof userAttributesSchema>;
|
||||
|
||||
Reference in New Issue
Block a user