diff --git a/node/src/api/board/board.controller.ts b/node/src/api/board/board.controller.ts index 1bef19b..90523c1 100644 --- a/node/src/api/board/board.controller.ts +++ b/node/src/api/board/board.controller.ts @@ -4,6 +4,7 @@ import { findBoardById, updateBoard, deleteBoard, + findBoardsByCollaboratorId, } from '../../models/board'; import { HTTP_STATUS } from '../../constants'; @@ -64,6 +65,30 @@ export const handleFindBoardById = async (req: Request, res: Response) => { } }; +// Function to get boards associated with a collaborator +export const handleGetCollaboratorBoards = async ( + req: Request, + res: Response, +) => { + try { + const collaboratorId = req.body.collaboratorId; + if (!collaboratorId) { + return res + .status(HTTP_STATUS.ERROR) + .json({ error: 'No collaborator ID provided' }); + } + + const boards = await findBoardsByCollaboratorId(collaboratorId); + + return res.status(HTTP_STATUS.SUCCESS).json({ boards }); + } catch (error) { + console.error('Error getting collaborator boards:', error); + res + .status(HTTP_STATUS.INTERNAL_SERVER_ERROR) + .json({ error: 'Failed to get collaborator boards' }); + } +}; + // Update board export const handleUpdateBoard = 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 e905831..10a819a 100644 --- a/node/src/api/board/board.route.ts +++ b/node/src/api/board/board.route.ts @@ -3,6 +3,7 @@ import { handleCreateBoard, handleDeleteBoard, handleFindBoardById, + handleGetCollaboratorBoards, handleUpdateBoard, } from './board.controller'; @@ -20,6 +21,9 @@ router.post('/createBoard', handleCreateBoard); // GET board by ID router.get('/getBoard', handleFindBoardById); +// GET board by ID +router.get('/getCollaboratorsBoard', handleGetCollaboratorBoards); + // PUT update a board router.put('/updateBoard', handleUpdateBoard); diff --git a/node/src/api/image/image.controller.ts b/node/src/api/image/image.controller.ts new file mode 100644 index 0000000..972c9c3 --- /dev/null +++ b/node/src/api/image/image.controller.ts @@ -0,0 +1,57 @@ +import { Response, Request } from 'express'; +import { createImage, findImageById } from '../../models/image'; +import { HTTP_STATUS } from '../../constants'; + +/** + * Firebase API controllers, logic for endpoint routes. + * @author Ibrahim Almalki + */ + +// TODO: JSDOC + +// Create image +export const handleCreateImage = async (req: Request, res: Response) => { + try { + const { imageEncoded } = req.body; + const image = await createImage(imageEncoded); + + res.status(HTTP_STATUS.SUCCESS).json({ image }); + } catch (error) { + console.error('Error creating image:', error); + res + .status(HTTP_STATUS.INTERNAL_SERVER_ERROR) + .json({ error: 'Failed to create image' }); + } +}; + +const validateId = (id: string, res: Response): id is string => { + if (id === undefined) { + res.status(HTTP_STATUS.ERROR).json({ error: 'NO ID PROVIDED' }); + return false; + } + return true; +}; + +const notFoundError = (res: Response) => + res.status(HTTP_STATUS.ERROR).json({ error: 'image not found' }); + +// Get image +export const handleFindImageById = async (req: Request, res: Response) => { + try { + const imageId = req.body.id; // The comment ID parameter is in the body. + if (!validateId(imageId, res)) return; + + const image = await findImageById(imageId as string); + + if (image) { + res.status(HTTP_STATUS.SUCCESS).json({ image }); + } else { + return notFoundError(res); + } + } catch (error) { + console.error('Error finding image by ID:', error); + res + .status(HTTP_STATUS.INTERNAL_SERVER_ERROR) + .json({ error: 'Failed to find image' }); + } +}; diff --git a/node/src/api/image/image.route.ts b/node/src/api/image/image.route.ts new file mode 100644 index 0000000..c6805fa --- /dev/null +++ b/node/src/api/image/image.route.ts @@ -0,0 +1,18 @@ +import express from 'express'; +import { handleCreateImage, handleFindImageById } from './image.controller'; + +/** + * Defines image routes. + * @authors Ibrahim Almalki + */ + +// The express router +const router = express.Router(); + +// POST create a new Image +router.post('/createImage', handleCreateImage); + +// GET Image by ID +router.get('/getImage', handleFindImageById); + +export default router; diff --git a/node/src/app.ts b/node/src/app.ts index 3d31049..d066108 100644 --- a/node/src/app.ts +++ b/node/src/app.ts @@ -17,6 +17,7 @@ import boardRoutes from './api/board/board.route'; import authRoutes from './api/auth/auth.route'; import sfuRoutes from './api/sfu/sfu.route'; import tenancyRoutes from './api/tenancy/tenancy.route'; +import imageRoutes from './api/image/image.route'; const mainLogger = new Logger('MainModule', LOG_LEVEL); const port = 3005; @@ -40,6 +41,7 @@ app.use('/board', boardRoutes); app.use('/auth', authRoutes); app.use('/sfu', sfuRoutes); app.use('/tenancy', tenancyRoutes); +app.use('/image', imageRoutes); server.listen(port, () => { mainLogger.info(`Example app listening on port ${port}.`); diff --git a/node/src/models/board.ts b/node/src/models/board.ts index 11bce2f..c4fccb5 100644 --- a/node/src/models/board.ts +++ b/node/src/models/board.ts @@ -1,11 +1,11 @@ import { DocumentFields } from 'fastfire/dist/types'; -import { Collaborator } from './collaborator'; import { FastFire, FastFireCollection, FastFireField, FastFireDocument, } from 'fastfire'; +import { generateRandId } from '../utils/misc'; /** * Defines Board class. @@ -15,6 +15,8 @@ import { //TODO: add createdAt and updatedAt @FastFireCollection('Board') export class Board extends FastFireDocument { + @FastFireField({ required: true }) + uid!: string; @FastFireField({ required: true }) serialized!: string; @FastFireField({ required: true }) @@ -24,7 +26,11 @@ export class Board extends FastFireDocument { @FastFireField({ required: true }) shareUrl!: string; @FastFireField({ required: true }) - collaborators!: Collaborator[]; + collaborators!: string[]; // Array of collaborator IDs + @FastFireField({ required: true }) + createdAt!: Date; + @FastFireField({ required: true }) + updatedAt!: Date; } // Function to create a board @@ -33,15 +39,25 @@ export async function createBoard( title: string, tags: string[], shareUrl: string, - collaborators: Collaborator[], + collaborators: string[], ) { - return FastFire.create(Board, { - serialized, - title, - tags, - shareUrl, - collaborators, - }); + const uid = generateRandId(); + const createdAt = new Date(); + const updatedAt = new Date(); + return FastFire.create( + Board, + { + uid, + serialized, + title, + tags, + shareUrl, + collaborators, + createdAt, + updatedAt, + }, + uid, + ); } // Function to find a board by ID @@ -53,6 +69,7 @@ export const updateBoard = async ( board: Board, updatedFields: Partial>, ) => { + updatedFields.updatedAt = new Date(); const { fastFireOptions: _fastFireOptions, id: _id, ...boardFields } = board; const updatedBoard = { ...boardFields, ...updatedFields }; await board.update(updatedBoard); @@ -61,3 +78,12 @@ export const updateBoard = async ( // Function to delete a board export const deleteBoard = async (board: Board) => await board.delete(); + +export const findBoardsByCollaboratorId = async (collaboratorId: string) => { + return await FastFire.where( + Board, + 'collaborators', + 'array-contains', + collaboratorId, + ).get(); +}; diff --git a/node/src/models/collaborator.ts b/node/src/models/collaborator.ts index b366cb2..fe85351 100644 --- a/node/src/models/collaborator.ts +++ b/node/src/models/collaborator.ts @@ -1,11 +1,11 @@ import { DocumentFields } from 'fastfire/dist/types'; -import { User } from './user'; import { FastFire, FastFireCollection, FastFireField, FastFireDocument, } from 'fastfire'; +import { generateRandId } from '../utils/misc'; /** * Defines collaborator class. @@ -16,18 +16,37 @@ import { //TODO: change permissionLevel to an enum @FastFireCollection('Collaborator') export class Collaborator extends FastFireDocument { + @FastFireField({ required: true }) + uid!: string; @FastFireField({ required: true }) permissionLevel!: string; @FastFireField({ required: true }) - user!: User; + user!: string; + @FastFireField({ required: true }) + createdAt!: Date; + @FastFireField({ required: true }) + updatedAt!: Date; } // Function to create a collaborator -export async function createCollaborator(permissionLevel: string, user: User) { - return await FastFire.create(Collaborator, { - permissionLevel, - user, - }); +export async function createCollaborator( + permissionLevel: string, + user: string, +) { + const uid = generateRandId(); + const createdAt = new Date(); + const updatedAt = new Date(); + return await FastFire.create( + Collaborator, + { + uid, + permissionLevel, + user, + createdAt, + updatedAt, + }, + uid, + ); } // Function to find a collaborator by ID @@ -39,6 +58,7 @@ export const updateCollaborator = async ( collaborator: Collaborator, updatedFields: Partial>, ) => { + updatedFields.updatedAt = new Date(); const { fastFireOptions: _fastFireOptions, id: _id, diff --git a/node/src/models/comment.ts b/node/src/models/comment.ts index 7a0a290..a7a453f 100644 --- a/node/src/models/comment.ts +++ b/node/src/models/comment.ts @@ -1,11 +1,11 @@ import { DocumentFields } from 'fastfire/dist/types'; -import { Collaborator } from './collaborator'; import { FastFire, FastFireCollection, FastFireField, FastFireDocument, } from 'fastfire'; +import { generateRandId } from '../utils/misc'; /** * Defines comment class. @@ -15,18 +15,34 @@ import { //TODO: add createdAt and updatedAt @FastFireCollection('Comment') export class Comment extends FastFireDocument { + @FastFireField({ required: true }) + uid!: string; @FastFireField({ required: true }) text!: string; @FastFireField({ required: true }) - collaborator!: Collaborator; + collaborator!: string; // Collaborator ID + @FastFireField({ required: true }) + createdAt!: Date; + @FastFireField({ required: true }) + updatedAt!: Date; } // Function to create a comment -export async function createComment(text: string, collaborator: Collaborator) { - return await FastFire.create(Comment, { - text, - collaborator, - }); +export async function createComment(text: string, collaborator: string) { + const uid = generateRandId(); + const createdAt = new Date(); + const updatedAt = new Date(); + return await FastFire.create( + Comment, + { + uid, + text, + collaborator, + createdAt, + updatedAt, + }, + uid, + ); } // Function to find a comment by ID @@ -38,6 +54,7 @@ export const updateComment = async ( comment: Comment, updatedFields: Partial>, ) => { + updatedFields.updatedAt = new Date(); const { fastFireOptions: _fastFireOptions, id: _id, diff --git a/node/src/models/image.ts b/node/src/models/image.ts new file mode 100644 index 0000000..d7cc462 --- /dev/null +++ b/node/src/models/image.ts @@ -0,0 +1,42 @@ +import { + FastFire, + FastFireCollection, + FastFireField, + FastFireDocument, +} from 'fastfire'; +import { generateRandId } from '../utils/misc'; + +/** + * Defines image class. + * @authors Ibrahim Almalki + */ + +//TODO: add createdAt and updatedAt +@FastFireCollection('Image') +export class Image extends FastFireDocument { + @FastFireField({ required: true }) + uid!: string; + @FastFireField({ required: true }) + imageEncoded!: string; + @FastFireField({ required: true }) + createdAt!: Date; +} + +// Function to create an image +export async function createImage(imageEncoded: string) { + const uid = generateRandId(); + const createdAt = new Date(); + return await FastFire.create( + Image, + { + uid, + imageEncoded, + createdAt, + }, + uid, + ); +} + +// Function to find a image by ID +export const findImageById = async (imageId: string) => + FastFire.findById(Image, imageId); diff --git a/node/src/models/user.ts b/node/src/models/user.ts index 790de49..c89662d 100644 --- a/node/src/models/user.ts +++ b/node/src/models/user.ts @@ -5,6 +5,7 @@ import { FastFireField, FastFireDocument, } from 'fastfire'; +import { generateRandId } from '../utils/misc'; /** * Defines user class. @@ -14,6 +15,8 @@ import { //TODO: add createdAt and updatedAt @FastFireCollection('User') export class User extends FastFireDocument { + @FastFireField({ required: true }) + uid!: string; @FastFireField({ required: true }) username!: string; @FastFireField({ required: true }) @@ -26,6 +29,10 @@ export class User extends FastFireDocument { password!: string; @FastFireField() avatar!: string; + @FastFireField({ required: true }) + createdAt!: Date; + @FastFireField({ required: true }) + updatedAt!: Date; } // Function to create a user @@ -37,14 +44,24 @@ export async function createUser( password: string, avatar: string, ) { - return await FastFire.create(User, { - username, - firstname, - lastname, - email, - password, - avatar, - }); + const uid = generateRandId(); + const createdAt = new Date(); + const updatedAt = new Date(); + return await FastFire.create( + User, + { + uid, + username, + firstname, + lastname, + email, + password, + avatar, + createdAt, + updatedAt, + }, + uid, + ); } // Function to find a user by ID @@ -56,6 +73,7 @@ export const updateUser = async ( user: User, updatedFields: Partial>, ) => { + updatedFields.updatedAt = new Date(); const { fastFireOptions: _fastFireOptions, id: _id, ...userFields } = user; const updatedUser = { ...userFields, ...updatedFields }; await user.update(updatedUser); diff --git a/node/src/utils/misc.ts b/node/src/utils/misc.ts index 34e85ea..fb7ec28 100644 --- a/node/src/utils/misc.ts +++ b/node/src/utils/misc.ts @@ -1,3 +1,4 @@ +import crypto from 'crypto'; /** * Defines miscellaneous helpers used across the application. * @authors Yousef Yassin @@ -18,3 +19,8 @@ export const truncateString = (input: string, n: number): string => { } return input.length <= n ? input : input.slice(-n); }; +/** + * Genertates a random id value. + * @returns A random UUID. + */ +export const generateRandId = () => crypto.randomUUID();