diff --git a/client/package.json b/client/package.json
index c7a4275..ec87914 100644
--- a/client/package.json
+++ b/client/package.json
@@ -22,6 +22,7 @@
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
+ "@radix-ui/react-portal": "^1.0.4",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slider": "^1.1.2",
diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml
index 17a972a..6f75f11 100644
--- a/client/pnpm-lock.yaml
+++ b/client/pnpm-lock.yaml
@@ -35,6 +35,9 @@ dependencies:
'@radix-ui/react-label':
specifier: ^2.0.2
version: 2.0.2(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-portal':
+ specifier: ^1.0.4
+ version: 1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-select':
specifier: ^2.0.0
version: 2.0.0(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0)
diff --git a/client/src/components/lib/BoardHeader.tsx b/client/src/components/lib/BoardHeader.tsx
index 004a359..abaee50 100644
--- a/client/src/components/lib/BoardHeader.tsx
+++ b/client/src/components/lib/BoardHeader.tsx
@@ -132,6 +132,8 @@ const BoardHeader = ({
folder: '',
tags: [],
collabID: '',
+ users: [],
+ permission: '',
});
}}
>
@@ -159,96 +161,101 @@ const BoardHeader = ({
{/* Avatars of active tenants */}
{/* Comment section button */}
-
+ {boardMeta.permission !== 'view' && (
+
+ )}
{/* Share Dialog */}
-
+ {boardMeta.permission !== 'view' && (
+
+
+ {' '}
+
+ )}
diff --git a/client/src/components/lib/BoardScroll.tsx b/client/src/components/lib/BoardScroll.tsx
index 2612a74..e6da30c 100644
--- a/client/src/components/lib/BoardScroll.tsx
+++ b/client/src/components/lib/BoardScroll.tsx
@@ -82,6 +82,7 @@ export const BoardScroll = () => {
return (
+ {/* eslint-disable-next-line sonarjs/cognitive-complexity */}
{canvases.map((board) => (
{
try {
let collabID = ' ';
+ let users = [];
+ let permission = '';
if (!isSelected) {
//todo
const boardState = await axios.get(REST.board.getBoard, {
@@ -101,6 +104,8 @@ export const BoardScroll = () => {
});
collabID = boardState.data.collabID;
+ users = boardState.data.users;
+ permission = boardState.data.permissionLevel;
const state = createStateWithRoughElement(
boardState.data.board.serialized,
@@ -164,6 +169,8 @@ export const BoardScroll = () => {
tags: isSelected ? [] : board.tags,
collabID: isSelected ? '' : collabID,
collaboratorAvatars: collaboratorAvatarUrlsMap,
+ users: isSelected ? '' : users,
+ permission: isSelected ? '' : permission,
});
} catch (error) {
toast({
diff --git a/client/src/components/lib/Canvas.tsx b/client/src/components/lib/Canvas.tsx
index e62247b..73f21b9 100644
--- a/client/src/components/lib/Canvas.tsx
+++ b/client/src/components/lib/Canvas.tsx
@@ -23,6 +23,7 @@ import { getScaleOffset } from '@/lib/canvasElements/render';
import {
IS_ELECTRON_INSTANCE,
PERIPHERAL_CODES,
+ REST,
SECONDS_TO_MS,
WS_TOPICS,
} from '@/constants';
@@ -42,6 +43,7 @@ import { useAuthStore } from '@/stores/AuthStore';
import CursorPresence from './CursorPresence';
import { throttle } from 'lodash';
import { idToColour } from '@/lib/userColours';
+import axios from 'axios';
/**
* Main Canvas View
@@ -142,7 +144,7 @@ export default function Canvas() {
'setTenants',
'clearTenants',
]);
- const { boardMeta } = useCanvasBoardStore(['boardMeta']);
+ const { boardMeta, addUser } = useCanvasBoardStore(['boardMeta', 'addUser']);
const { userEmail } = useAuthStore(['userEmail']);
// Id of the element currently being drawn.
@@ -164,7 +166,7 @@ export default function Canvas() {
socket?.on(WS_TOPICS.NOTIFY_JOIN_ROOM, initTenants);
socket?.on(WS_TOPICS.NOTIFY_LEAVE_ROOM, initTenants);
return clearTenants;
- }, [socket]);
+ }, [socket, boardMeta.users]);
/**
* Called everytime someone joins or leaves the room, and once we initially join, to maintain
@@ -174,6 +176,22 @@ export default function Canvas() {
*/
const initTenants = async () => {
const tenantIds = (await tenancy.get(boardMeta.roomID)) as string[];
+
+ tenantIds
+ .map((id) => extractCollabID(id) as string)
+ .filter(
+ (id) => !new Set(boardMeta.users.map((user) => user.collabID)).has(id),
+ )
+ .forEach(async (collabID) => {
+ if (!collabID) return;
+ const userInfo = await axios.get(REST.collaborator.getUserInfo, {
+ params: {
+ id: collabID,
+ },
+ });
+ addUser(userInfo.data.userInfo);
+ });
+
const activeTenants = tenantIds.reduce(
(acc, id) => {
const collabId = extractCollabID(id);
diff --git a/client/src/components/lib/DeleteCanvasDialog.tsx b/client/src/components/lib/DeleteCanvasDialog.tsx
index a135abc..2c86cd9 100644
--- a/client/src/components/lib/DeleteCanvasDialog.tsx
+++ b/client/src/components/lib/DeleteCanvasDialog.tsx
@@ -60,6 +60,8 @@ export const DeleteCanavasDialog = ({
folder: '',
tags: [],
collabID: '',
+ users: [],
+ permission: '',
});
axios.delete(REST.board.deleteBoard, {
params: { id: boardMeta.id },
diff --git a/client/src/components/lib/ShareBoardDialog.tsx b/client/src/components/lib/ShareBoardDialog.tsx
index fc9f400..9af781c 100644
--- a/client/src/components/lib/ShareBoardDialog.tsx
+++ b/client/src/components/lib/ShareBoardDialog.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
import {
AlertDialog,
AlertDialogAction,
@@ -21,7 +21,14 @@ import {
SelectTrigger,
SelectValue,
} from '../ui/select';
-import { User } from '@/stores/WebSocketStore';
+import { SharedUser, useCanvasBoardStore } from '@/stores/CanavasBoardStore';
+import axios from 'axios';
+import { useAppStore } from '@/stores/AppStore';
+import { setCursor } from '@/lib/misc';
+import { useCanvasElementStore } from '@/stores/CanvasElementsStore';
+import { useWebSocketStore } from '@/stores/WebSocketStore';
+import { REST } from '@/constants';
+import { useToast } from '../ui/use-toast';
/**
* An alert dialog that is controlled by the `open` prop. It displays a list of users
@@ -38,10 +45,38 @@ const ShareBoardDialog = ({
open: boolean;
setOpen: (value: boolean) => void;
boardLink: string;
- users: User[];
+ users: SharedUser[];
}) => {
/* Controls visibility of the addition input. */
const [isAddUserOpen, setIsAddUserOpen] = useState(false);
+ const { boardMeta, updatePermission, addUser } = useCanvasBoardStore([
+ 'boardMeta',
+ 'updatePermission',
+ 'addUser',
+ ]);
+ const { setSelectedElements, selectedElementIds } = useCanvasElementStore([
+ 'setSelectedElements',
+ 'selectedElementIds',
+ ]);
+ const { setTool } = useAppStore(['setTool']);
+ const { socket, setWebsocketAction } = useWebSocketStore([
+ 'socket',
+ 'setWebsocketAction',
+ ]);
+ const newUserEmail = useRef
(null);
+ const newUserPerm = useRef(null);
+ const { toast } = useToast();
+
+ useEffect(() => {
+ socket?.on('changePermission', (msg) => {
+ const { collabID, permission } = (
+ msg as { payload: { collabID: string; permission: string } }
+ ).payload;
+ const isOwnPerm = boardMeta.collabID === collabID;
+ if (isOwnPerm) setSelectedElements([]);
+ updatePermission(collabID, permission, isOwnPerm);
+ });
+ }, [socket, boardMeta.collabID, updatePermission]);
return (
@@ -85,17 +120,62 @@ const ShareBoardDialog = ({
- Confirm
+ {
+ const email = newUserEmail.current?.value;
+ let isAlreadyShared = false;
+ for (const user of boardMeta.users) {
+ if (user.email === email) {
+ isAlreadyShared = true;
+ }
+ }
+
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailRegex.test(newUserEmail.current?.value as string)) {
+ toast({
+ title: 'Oops! The email you entered is not valid.',
+ description: 'Please check and try again.',
+ });
+ } else if (isAlreadyShared) {
+ toast({
+ title:
+ 'Oops! This user seems to already be a Collaborator.',
+ description: 'Please try again.',
+ });
+ (newUserEmail.current as HTMLInputElement).value = '';
+ } else {
+ try {
+ const response = await axios.put(REST.board.addUser, {
+ boardId: boardMeta.id,
+ email: email,
+ perm: newUserPerm.current?.textContent?.toLocaleLowerCase(),
+ });
+ (newUserEmail.current as HTMLInputElement).value = '';
+ addUser(response.data.user);
+ setWebsocketAction(response.data.user, 'addNewCollab');
+ } catch {
+ toast({
+ title: "Oops! We couldn't find a user with that email.",
+ description: 'Please try again.',
+ });
+ }
+ }
+ }}
+ >
+ Confirm
+
)}
{/* The user list */}
@@ -103,7 +183,9 @@ const ShareBoardDialog = ({
{users.map((user) => (
-
+
{user.initials}
@@ -113,13 +195,44 @@ const ShareBoardDialog = ({
{user.email}
-
diff --git a/client/src/components/lib/ToolBar.tsx b/client/src/components/lib/ToolBar.tsx
index a417a09..d95c65d 100644
--- a/client/src/components/lib/ToolBar.tsx
+++ b/client/src/components/lib/ToolBar.tsx
@@ -22,6 +22,7 @@ import { fileOpen } from '@/lib/fs';
import { injectImageElement } from '@/lib/image';
import { useWebSocketStore } from '@/stores/WebSocketStore';
import { IS_ELECTRON_INSTANCE } from '@/constants';
+import { useCanvasBoardStore } from '@/stores/CanavasBoardStore';
/**
* This is the toolbar that is displayed on the canvas.
@@ -170,6 +171,8 @@ const ToolGroup = ({
*/
const ToolBar = () => {
const { tool, isTransparent } = useAppStore(['tool', 'isTransparent']);
+ const { boardMeta } = useCanvasBoardStore(['boardMeta']);
+ const allowEdit = boardMeta.permission !== 'view';
return (
{
width: 'fit-content',
}}
>
-
-
+ {allowEdit && (
+
+
+
+
+ )}
);
};
diff --git a/client/src/components/lib/TopBar.tsx b/client/src/components/lib/TopBar.tsx
index 713fd96..b256d86 100644
--- a/client/src/components/lib/TopBar.tsx
+++ b/client/src/components/lib/TopBar.tsx
@@ -97,6 +97,8 @@ export const TopBar = () => {
folder: boardData.folder,
tags: boardData.tags,
collabID: data.data.collabID,
+ users: data.data.users,
+ permission: data.data.permissionLevel,
});
setMode('canvas');
diff --git a/client/src/components/ui/toast.tsx b/client/src/components/ui/toast.tsx
index 76861dc..21a484f 100644
--- a/client/src/components/ui/toast.tsx
+++ b/client/src/components/ui/toast.tsx
@@ -2,6 +2,7 @@ import * as React from 'react';
import { Cross2Icon } from '@radix-ui/react-icons';
import * as ToastPrimitives from '@radix-ui/react-toast';
import { cva, type VariantProps } from 'class-variance-authority';
+import * as Portal from '@radix-ui/react-portal';
import { cn } from '@/lib/utils';
@@ -11,14 +12,16 @@ const ToastViewport = React.forwardRef<
React.ElementRef
,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
-
+
+
+
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
diff --git a/client/src/constants.ts b/client/src/constants.ts
index 338dc90..87db459 100644
--- a/client/src/constants.ts
+++ b/client/src/constants.ts
@@ -34,6 +34,7 @@ export const REST = {
getBoard: `${REST_ROOT}/board/getBoard`,
deleteBoard: `${REST_ROOT}/board/deleteBoard`,
updateBoard: `${REST_ROOT}/board/updateBoard`,
+ addUser: `${REST_ROOT}/board/addUser`,
},
collaborators: {
getAvatar: `${REST_ROOT}/collaborator/getCollaboratorAvatars`,
@@ -44,6 +45,10 @@ export const REST = {
update: `${REST_ROOT}/comment/handleLike`,
delete: `${REST_ROOT}/comment/deleteComment`,
},
+ collaborator: {
+ update: `${REST_ROOT}/collaborator/updateCollaborator`,
+ getUserInfo: `${REST_ROOT}/collaborator/getCollaborator`,
+ },
};
export const HTTP_STATUS = {
SUCCESS: 200,
diff --git a/client/src/hooks/useSocket.tsx b/client/src/hooks/useSocket.tsx
index 6e1c019..ef07b36 100644
--- a/client/src/hooks/useSocket.tsx
+++ b/client/src/hooks/useSocket.tsx
@@ -10,8 +10,11 @@ import { useAuthStore } from '@/stores/AuthStore';
import { fileCache } from '@/lib/cache';
import { dataURLToFile } from '@/lib/bytes';
import { commitImageToCache, isSupportedImageFile } from '@/lib/image';
-import { useCanvasBoardStore } from '@/stores/CanavasBoardStore';
+import { SharedUser, useCanvasBoardStore } from '@/stores/CanavasBoardStore';
import { Vector2 } from '@/types';
+import axios from 'axios';
+import { REST } from '@/constants';
+import { createStateWithRoughElement } from '@/components/lib/BoardScroll';
/**
* Defines a hook that controls all socket related activities
@@ -37,6 +40,7 @@ export const useSocket = () => {
]);
const {
+ setCanvasElementState,
addCanvasShape,
addCanvasFreehand,
editCanvasElement,
@@ -63,6 +67,7 @@ export const useSocket = () => {
textStrings,
fileIds,
} = useCanvasElementStore([
+ 'setCanvasElementState',
'addCanvasShape',
'addCanvasFreehand',
'editCanvasElement',
@@ -90,11 +95,13 @@ export const useSocket = () => {
'fileIds',
]);
- const { boardMeta, setBoardMeta, updateCanvas } = useCanvasBoardStore([
- 'boardMeta',
- 'setBoardMeta',
- 'updateCanvas',
- ]);
+ const { boardMeta, setBoardMeta, updateCanvas, addUser } =
+ useCanvasBoardStore([
+ 'boardMeta',
+ 'setBoardMeta',
+ 'updateCanvas',
+ 'addUser',
+ ]);
const socket = useRef();
@@ -218,15 +225,27 @@ export const useSocket = () => {
redoCanvasHistory: () => {
redoCanvasHistory();
},
- updateUpdatedTime: (fields: UpdatedTimeMessage) => {
+ updateUpdatedTime: async (fields: UpdatedTimeMessage) => {
setBoardMeta({ lastModified: fields.lastModified });
updateCanvas(fields.boardID, fields.lastModified);
+
+ const boardState = await axios.get(REST.board.getBoard, {
+ params: { id: fields.boardID },
+ });
+
+ const state = createStateWithRoughElement(
+ boardState.data.board.serialized,
+ );
+ setCanvasElementState(state);
},
updateCursorPosition: (cursorPosition: Vector2 & { userId: string }) =>
setCursorPosition(cursorPosition.userId, {
x: cursorPosition.x,
y: cursorPosition.y,
}),
+ addNewCollab: (newUser: SharedUser) => {
+ addUser(newUser);
+ },
};
// Intialize socket
diff --git a/client/src/stores/CanavasBoardStore.ts b/client/src/stores/CanavasBoardStore.ts
index 19f1b1c..7842de8 100644
--- a/client/src/stores/CanavasBoardStore.ts
+++ b/client/src/stores/CanavasBoardStore.ts
@@ -1,6 +1,7 @@
import { create } from 'zustand';
import { SetState } from './types';
import { createStoreWithSelectors } from './utils';
+// import { User } from 'firebase/auth';
/**
* Define Global CanvasBoard states and reducers
@@ -23,6 +24,15 @@ export interface Canvas {
roomID: string;
}
+export interface SharedUser {
+ email: string;
+ avatar: string;
+ initials: string;
+ username: string;
+ permission: string;
+ collabID: string;
+}
+
/** Definitions */
interface CanvasBoardState {
canvases: Canvas[];
@@ -41,6 +51,8 @@ interface CanvasBoardState {
tags: string[];
collabID: string;
collaboratorAvatars: Record;
+ users: SharedUser[];
+ permission: string;
};
}
@@ -57,6 +69,12 @@ interface CanvasBoardActions {
folder: string,
tags: string[],
) => void;
+ updatePermission: (
+ collabID: string,
+ permission: string,
+ isOwnPerm: boolean,
+ ) => void;
+ addUser: (user: SharedUser) => void;
setBoardMeta: (meta: Partial) => void;
setTag: (tags: Array) => void;
}
@@ -78,6 +96,8 @@ export const initialCanvasState: CanvasBoardState = {
tags: [],
collabID: '',
collaboratorAvatars: {},
+ users: [],
+ permission: '',
},
};
@@ -125,6 +145,26 @@ const updateCanvasInfo =
return { ...state, canvases };
});
+const updatePermission =
+ (set: SetState) =>
+ (collabID: string, permission: string, isOwnPerm: boolean) =>
+ set((state) => {
+ const users = state.boardMeta.users.map((user) =>
+ user.collabID === collabID ? { ...user, permission } : user,
+ );
+
+ if (isOwnPerm)
+ return { boardMeta: { ...state.boardMeta, users, permission } };
+ return { boardMeta: { ...state.boardMeta, users } };
+ });
+
+const addUser = (set: SetState) => (user: SharedUser) =>
+ set((state) => {
+ const users = [...state.boardMeta.users, user];
+
+ return { boardMeta: { ...state.boardMeta, users } };
+ });
+
const setBoardMeta =
(set: SetState) =>
(meta: Partial) => {
@@ -143,6 +183,8 @@ const CanvasBoardStore = create()((set) => ({
removeCanvas: removeCanvas(set),
updateCanvas: updateCanvas(set),
updateCanvasInfo: updateCanvasInfo(set),
+ updatePermission: updatePermission(set),
+ addUser: addUser(set),
setBoardMeta: setBoardMeta(set),
setTag: setTag(set),
}));
diff --git a/client/src/stores/WebSocketStore.ts b/client/src/stores/WebSocketStore.ts
index a3686d7..8e097b4 100644
--- a/client/src/stores/WebSocketStore.ts
+++ b/client/src/stores/WebSocketStore.ts
@@ -22,6 +22,8 @@ export const Actions = [
'updateComment',
'addComment',
'removeComment',
+ 'changePermission',
+ 'addNewCollab',
] as const;
export type ActionsType = typeof Actions;
@@ -55,7 +57,8 @@ interface WebSocketState {
| string
| string[]
| UpdatedTimeMessage
- | { elemID: string; comment: Partial };
+ | { elemID: string; comment: Partial }
+ | { collabID: string; permission: string };
// The current active tenants
activeTenants: Record;
// The current cursor positions keyed by user ID
@@ -73,7 +76,8 @@ interface WebSocketActions {
| string
| string[]
| UpdatedTimeMessage
- | { elemID: string; comment: Partial },
+ | { elemID: string; comment: Partial }
+ | { collabID: string; permission: string },
action: string,
) => void;
// Set the socket reference
@@ -114,7 +118,8 @@ const setWebsocketAction =
| string
| string[]
| UpdatedTimeMessage
- | { elemID: string; comment: Partial },
+ | { elemID: string; comment: Partial }
+ | { collabID: string; permission: string },
action: string,
) =>
set(() => {
diff --git a/client/src/views/SignInPage.tsx b/client/src/views/SignInPage.tsx
index 89f0e65..eeb8126 100644
--- a/client/src/views/SignInPage.tsx
+++ b/client/src/views/SignInPage.tsx
@@ -23,7 +23,11 @@ import axios from 'axios';
import { REST } from '@/constants';
import { ACCESS_TOKEN_TAG } from '@/constants';
import { useAuthStore } from '@/stores/AuthStore';
-import { useCanvasBoardStore, Canvas } from '@/stores/CanavasBoardStore';
+import {
+ useCanvasBoardStore,
+ Canvas,
+ SharedUser,
+} from '@/stores/CanavasBoardStore';
import { getStorage, ref, getDownloadURL } from 'firebase/storage';
import { createStateWithRoughElement } from '@/components/lib/BoardScroll';
import {
@@ -85,6 +89,8 @@ export async function getUserDetails(
folder: string;
tags: string[];
collabID: string;
+ users: SharedUser[];
+ permission: string;
}>,
) => void,
setCanvasElementState: (element: CanvasElementState) => void,
@@ -133,6 +139,8 @@ export const checkURL = async (
folder: string;
tags: string[];
collabID: string;
+ users: SharedUser[];
+ permission: string;
}>,
) => void,
setCanvasElementState: (element: CanvasElementState) => void,
@@ -149,6 +157,9 @@ export const checkURL = async (
fields: { collaborators: userID },
});
+ console.log('users ', board.data.users);
+ console.log('permissions', board.data.permission);
+ console.log(board);
setBoardMeta({
roomID: board.data.roomID,
title: board.data.title,
@@ -158,6 +169,8 @@ export const checkURL = async (
folder: board.data.folder,
tags: board.data.tags,
collabID: board.data.collabID,
+ users: board.data.users,
+ permission: board.data.permission,
});
setCanvasElementState(createStateWithRoughElement(board.data.serialized));
diff --git a/client/src/views/Viewport.tsx b/client/src/views/Viewport.tsx
index 9413186..ae8e6b4 100644
--- a/client/src/views/Viewport.tsx
+++ b/client/src/views/Viewport.tsx
@@ -19,7 +19,6 @@ import { isDrawingTool } from '@/lib/misc';
import { Separator } from '@/components/ui/separator';
import BoardHeader from '@/components/lib/BoardHeader';
import ShareBoardDialog from '@/components/lib/ShareBoardDialog';
-import { users } from '@/stores/WebSocketStore';
import { useCanvasBoardStore } from '@/stores/CanavasBoardStore';
import EditBoardDataDialog from '@/components/lib/EditBoardDataDialog';
@@ -61,7 +60,7 @@ const Viewport = () => {
open={isShareDialogOpen}
setOpen={setIsShareDialogOpen}
boardLink={boardMeta.shareUrl}
- users={users}
+ users={boardMeta.users}
/>
{
}}
>
-
+ {boardMeta.permission !== 'view' && }
-
+ {boardMeta.permission !== 'view' && }
{IS_ELECTRON_INSTANCE && }
diff --git a/node/src/api/board/board.controller.ts b/node/src/api/board/board.controller.ts
index a08873f..3e2c91a 100644
--- a/node/src/api/board/board.controller.ts
+++ b/node/src/api/board/board.controller.ts
@@ -5,16 +5,20 @@ import {
updateBoard,
deleteBoard,
findBoardsByCollaboratorsId,
+ Board,
} from '../../models/board';
import { HTTP_STATUS } from '../../constants';
import {
+ Collaborator,
createCollaborator,
deleteCollaborator,
findCollaboratorById,
findCollaboratorByIdAndBoard,
findCollaboratorsById,
} from '../../models/collaborator';
-import { generateRandId } from '../../utils/misc';
+import { generateRandId, getInitials } from '../../utils/misc';
+import { findUserById } from '../../models/user';
+import { findUserByEmail } from '../user/user.controller';
/**
* Firebase API controllers, logic for endpoint routes.
@@ -29,13 +33,31 @@ export const handleCreateBoard = async (req: Request, res: Response) => {
const boardID = generateRandId();
const { user, serialized, title, shareUrl } = req.body; // The board parameters are in the body.
- const collaborator = await createCollaborator('edit', user, boardID);
+ const {
+ id: collabID,
+ permissionLevel,
+ uid,
+ } = await createCollaborator('owner', user, boardID);
const board = await createBoard(boardID, serialized, title, shareUrl, [
- collaborator.uid,
+ uid,
]);
- res.status(HTTP_STATUS.SUCCESS).json({ board, collabID: collaborator.id });
+ const userInfo = await findUserById(user);
+ const users = [
+ {
+ email: userInfo?.email,
+ avatar: userInfo?.avatar,
+ initials: getInitials(userInfo?.firstname + ' ' + userInfo?.lastname),
+ username: userInfo?.firstname + ' ' + userInfo?.lastname,
+ permission: permissionLevel,
+ collabID,
+ },
+ ];
+
+ res
+ .status(HTTP_STATUS.SUCCESS)
+ .json({ board, collabID, users, permissionLevel });
} catch (error) {
console.error('Error creating board:', error);
res
@@ -63,11 +85,42 @@ export const handleFindBoardById = async (req: Request, res: Response) => {
if (!validateId(boardId, res)) return;
const board = await findBoardById(boardId as string);
- const collabID = (await findCollaboratorByIdAndBoard(userID, boardId)).pop()
- ?.id;
+
+ let collabID;
+ let permissionLevel;
+ if (userID) {
+ const { id, permissionLevel: perm } = (
+ await findCollaboratorByIdAndBoard(userID, boardId)
+ ).pop() as Collaborator;
+
+ collabID = id;
+ permissionLevel = perm;
+ } else {
+ collabID = undefined;
+ permissionLevel = undefined;
+ }
+
+ const users =
+ board &&
+ (await Promise.all(
+ board.collaborators.map(async (collabID) => {
+ const collab = await findCollaboratorById(collabID);
+ const user = await findUserById(collab?.user as string);
+ return {
+ email: user?.email,
+ avatar: user?.avatar,
+ initials: getInitials(user?.firstname + ' ' + user?.lastname),
+ username: user?.firstname + ' ' + user?.lastname,
+ permission: collab?.permissionLevel,
+ collabID: collab?.id,
+ };
+ }),
+ ));
return board
- ? res.status(HTTP_STATUS.SUCCESS).json({ board, collabID })
+ ? res
+ .status(HTTP_STATUS.SUCCESS)
+ .json({ board, collabID, users, permissionLevel })
: notFoundError(res);
} catch (error) {
console.error('Error finding board by ID:', error);
@@ -140,7 +193,32 @@ export const handleUpdateBoard = async (req: Request, res: Response) => {
if (collab.length !== 0) {
collabID = collab.pop()?.uid;
const board = await findBoardById(boardId);
- return res.status(HTTP_STATUS.SUCCESS).json({ ...board, collabID });
+ const users =
+ board &&
+ (await Promise.all(
+ board.collaborators.map(async (collabID) => {
+ const collab = await findCollaboratorById(collabID);
+ const user = await findUserById(collab?.user as string);
+ return {
+ email: user?.email,
+ avatar: user?.avatar,
+ initials: getInitials(user?.firstname + ' ' + user?.lastname),
+ username: user?.firstname + ' ' + user?.lastname,
+ permission: collab?.permissionLevel,
+ collabID: collab?.id,
+ };
+ }),
+ ));
+
+ const { permissionLevel: permission } = (await findCollaboratorById(
+ collabID as string,
+ )) as Collaborator;
+ return res.status(HTTP_STATUS.SUCCESS).json({
+ ...board,
+ collabID,
+ users,
+ permission: permission,
+ });
}
collabID = (
@@ -155,8 +233,27 @@ export const handleUpdateBoard = async (req: Request, res: Response) => {
}
const update = await updateBoard(board, updatedFields);
+ const users =
+ board &&
+ (await Promise.all(
+ (update.collaborators as string[]).map(async (collabID) => {
+ const collab = await findCollaboratorById(collabID);
+ const user = await findUserById(collab?.user as string);
+ return {
+ email: user?.email,
+ avatar: user?.avatar,
+ initials: getInitials(user?.firstname + ' ' + user?.lastname),
+ username: user?.firstname + ' ' + user?.lastname,
+ permission: collab?.permissionLevel,
+ collabID: collab?.id,
+ };
+ }),
+ ));
+
const { fastFireOptions: _fastFireOptions, ...fields } = update; // TODO(yousef): Should make a helper method to extract the options
- return res.status(HTTP_STATUS.SUCCESS).json({ ...fields, collabID });
+ return res
+ .status(HTTP_STATUS.SUCCESS)
+ .json({ ...fields, collabID, users, permission: 'edit' });
} else {
return notFoundError(res);
}
@@ -168,6 +265,39 @@ export const handleUpdateBoard = async (req: Request, res: Response) => {
}
};
+// Update board
+export const handleAddUserbyEmail = async (req: Request, res: Response) => {
+ try {
+ // The board ID and new parameters are in the body.
+ const { boardId, email, perm } = req.body;
+ if (!validateId(boardId, res)) return;
+ const board = await findBoardById(boardId);
+ const user = await findUserByEmail(email);
+ if (user === null) return res.status(HTTP_STATUS.ERROR).json();
+
+ const collab = await createCollaborator(perm, user.uid, boardId);
+ await updateBoard(board as Board, {
+ collaborators: collab.id,
+ });
+
+ return res.status(HTTP_STATUS.SUCCESS).json({
+ user: {
+ email: user.email,
+ avatar: user.avatar,
+ initials: getInitials(user.firstname + ' ' + user.lastname),
+ username: user.firstname + ' ' + user.lastname,
+ permission: perm,
+ collabID: collab.id,
+ },
+ });
+ } catch (error) {
+ console.error('Error adding user: ', error);
+ res
+ .status(HTTP_STATUS.INTERNAL_SERVER_ERROR)
+ .json({ error: 'Failed to update board' });
+ }
+};
+
// Delete board
export const handleDeleteBoard = async (req: Request, res: Response) => {
try {
diff --git a/node/src/api/board/board.route.ts b/node/src/api/board/board.route.ts
index 10a819a..4fa6f9e 100644
--- a/node/src/api/board/board.route.ts
+++ b/node/src/api/board/board.route.ts
@@ -1,5 +1,6 @@
import express from 'express';
import {
+ handleAddUserbyEmail,
handleCreateBoard,
handleDeleteBoard,
handleFindBoardById,
@@ -27,6 +28,9 @@ router.get('/getCollaboratorsBoard', handleGetCollaboratorBoards);
// PUT update a board
router.put('/updateBoard', handleUpdateBoard);
+// PUT add user by email
+router.put('/addUser', handleAddUserbyEmail);
+
// DELETE board by ID
router.delete('/deleteBoard', handleDeleteBoard);
diff --git a/node/src/api/collaborator/collaborator.controller.ts b/node/src/api/collaborator/collaborator.controller.ts
index ec5c763..377d22a 100644
--- a/node/src/api/collaborator/collaborator.controller.ts
+++ b/node/src/api/collaborator/collaborator.controller.ts
@@ -8,6 +8,7 @@ import {
} from '../../models/collaborator';
import { HTTP_STATUS } from '../../constants';
import { findUserById } from '../../models/user';
+import { getInitials } from '../../utils/misc';
/**
* Firebase API controllers, logic for endpoint routes.
@@ -73,12 +74,22 @@ export const handleFindCollaboratorById = async (
res: Response,
) => {
try {
- const collabId = req.body.id; // The collaborator ID parameter is in the body.
+ const collabId = req.query.id as string; // The collaborator ID parameter is in the body.
if (!validateId(collabId, res)) return;
const collaborator = await findCollaboratorById(collabId as string);
+ const user = await findUserById(collaborator?.user as string);
if (collaborator) {
- res.status(HTTP_STATUS.SUCCESS).json({ collaborator });
+ res.status(HTTP_STATUS.SUCCESS).json({
+ userInfo: {
+ email: user?.email,
+ avatar: user?.avatar,
+ initials: getInitials(user?.firstname + ' ' + user?.lastname),
+ username: user?.firstname + ' ' + user?.lastname,
+ permission: collaborator.permissionLevel,
+ collabID: collaborator.id,
+ },
+ });
} else {
return notFoundError(res);
}
diff --git a/node/src/api/user/user.controller.ts b/node/src/api/user/user.controller.ts
index b1e5dda..354199a 100644
--- a/node/src/api/user/user.controller.ts
+++ b/node/src/api/user/user.controller.ts
@@ -4,6 +4,7 @@ import {
findUserById,
deleteUser,
updateUser,
+ User,
} from '../../models/user';
import { HTTP_STATUS } from '../../constants';
import { firebaseApp } from '../../firebase/firebaseApp';
@@ -15,12 +16,6 @@ import { firebaseApp } from '../../firebase/firebaseApp';
// TODO: JSDOC
-interface User {
- id: string;
- email: string;
- name: string;
-}
-
//Create user
export const handleCreateUser = async (req: Request, res: Response) => {
try {
@@ -63,6 +58,7 @@ export const handleFindUserById = async (req: Request, res: Response) => {
}
const user = await findUserByEmail(email);
+ //todo
if (user) {
res.status(HTTP_STATUS.SUCCESS).json({ user });
} else {
@@ -78,7 +74,7 @@ export const handleFindUserById = async (req: Request, res: Response) => {
}
};
-async function findUserByEmail(email: string): Promise {
+export async function findUserByEmail(email: string): Promise {
const usersCollection = firebaseApp.firestore().collection('User');
const querySnapshot = await usersCollection.where('email', '==', email).get();