diff --git a/backend/src/API/coffeeChatAPI.ts b/backend/src/API/coffeeChatAPI.ts new file mode 100644 index 00000000..e13ee074 --- /dev/null +++ b/backend/src/API/coffeeChatAPI.ts @@ -0,0 +1,105 @@ +import isEqual from 'lodash.isequal'; +import CoffeeChatDao from '../dao/CoffeeChatDao'; +import PermissionsManager from '../utils/permissionsManager'; +import { PermissionError } from '../utils/errors'; + +const coffeeChatDao = new CoffeeChatDao(); + +/** + * Gets all coffee chats + */ +export const getAllCoffeeChats = (): Promise => coffeeChatDao.getAllCoffeeChats(); + +/** + * Gets all coffee chats for a user + * @param user - user whose coffee chats should be fetched + */ +export const getCoffeeChatsByUser = async (user: IdolMember): Promise => + coffeeChatDao.getCoffeeChatsByUser(user); + +/** + * Creates a new coffee chat for member + * @param coffeeChat - Newly created CoffeeChat object + * A member can not coffee chat themselves. + * A member can not coffee chat the same person from previous semesters. + */ +export const createCoffeeChat = async (coffeeChat: CoffeeChat): Promise => { + const [member1, member2] = coffeeChat.members; + + if (isEqual(member1, member2)) { + throw new Error('Cannot create coffee chat with yourself.'); + } + + const prevChats1 = await coffeeChatDao.getCoffeeChatsByUser(member1); + const prevChats2 = await coffeeChatDao.getCoffeeChatsByUser(member2); + + const prevChats = [...prevChats1, ...prevChats2]; + + const chatExists = prevChats.some((chat) => { + const chatMembers = chat.members.map((m) => m.email).sort(); + const currentMembers = [member1.email, member2.email].sort(); + return isEqual(chatMembers, currentMembers); + }); + + if (chatExists) { + throw new Error( + 'Cannot create coffee chat with member. Previous coffee chats from previous semesters exist.' + ); + } + + await coffeeChatDao.createCoffeeChat(coffeeChat); + return coffeeChat; +}; + +/** + * Updates a coffee chat (if the user has permission) + * @param coffeeChat - The updated CoffeeChat object + * @param user - The user that is requesting to update the coffee chat + */ +export const updateCoffeeChat = async ( + coffeeChat: CoffeeChat, + user: IdolMember +): Promise => { + const canEditCoffeeChat = await PermissionsManager.canEditCoffeeChat(user); + if (!canEditCoffeeChat) { + throw new PermissionError( + `User with email ${user.email} does not have permissions to update coffee chat.` + ); + } + + await coffeeChatDao.updateCoffeeChat(coffeeChat); + return coffeeChat; +}; + +/** + * Deletes a coffee chat (if the user has permission) + * @param uuid - DB uuid of CoffeeChat + * @param user - The user that is requesting to delete a coffee chat + */ +export const deleteCoffeeChat = async (uuid: string, user: IdolMember): Promise => { + const isLeadOrAdmin = await PermissionsManager.isLeadOrAdmin(user); + const coffeeChat = await coffeeChatDao.getCoffeeChat(uuid); + + if (!coffeeChat) return; + + if (!isLeadOrAdmin) { + throw new PermissionError( + `User with email ${user.email} does not have sufficient permissions to delete coffee chat.` + ); + } + await coffeeChatDao.deleteCoffeeChat(uuid); +}; + +/** + * Deletes all coffee chats (if the user has permission) + * @param user - The user that is requesting to delete all coffee chats + */ +export const clearAllCoffeeChats = async (user: IdolMember): Promise => { + const isLeadOrAdmin = await PermissionsManager.isLeadOrAdmin(user); + if (!isLeadOrAdmin) { + throw new PermissionError( + `User with email ${user.email} does not have sufficient permissions to delete all coffee chats.` + ); + } + await CoffeeChatDao.clearAllCoffeeChats(); +}; diff --git a/backend/src/api.ts b/backend/src/api.ts index 6285e27e..bea0ccd1 100644 --- a/backend/src/api.ts +++ b/backend/src/api.ts @@ -33,6 +33,14 @@ import { hideShoutout, deleteShoutout } from './API/shoutoutAPI'; +import { + createCoffeeChat, + getAllCoffeeChats, + updateCoffeeChat, + getCoffeeChatsByUser, + deleteCoffeeChat, + clearAllCoffeeChats +} from './API/coffeeChatAPI'; import { allSignInForms, createSignInForm, @@ -280,6 +288,33 @@ loginCheckedDelete('/shoutout/:uuid', async (req, user) => { return {}; }); +loginCheckedGet('/coffee-chat', async () => ({ + coffeeChats: await getAllCoffeeChats() +})); + +loginCheckedPost('/coffee-chat', async (req) => ({ + coffeeChats: await createCoffeeChat(req.body) +})); + +loginCheckedDelete('/coffee-chat', async (_, user) => { + await clearAllCoffeeChats(user); + return {}; +}); + +loginCheckedDelete('/coffee-chat/:uuid', async (req, user) => { + await deleteCoffeeChat(req.params.uuid, user); + return {}; +}); + +loginCheckedGet('/coffee-chat/:email', async (_, user) => { + const coffeeChats = await getCoffeeChatsByUser(user); + return { coffeeChats }; +}); + +loginCheckedPut('/coffee-chat', async (req, user) => ({ + coffeeChats: await updateCoffeeChat(req.body, user) +})); + // Pull from IDOL loginCheckedPost('/pullIDOLChanges', (_, user) => requestIDOLPullDispatch(user)); loginCheckedGet('/getIDOLChangesPR', (_, user) => getIDOLChangesPR(user)); diff --git a/backend/src/dao/CoffeeChatDao.ts b/backend/src/dao/CoffeeChatDao.ts new file mode 100644 index 00000000..59dcf013 --- /dev/null +++ b/backend/src/dao/CoffeeChatDao.ts @@ -0,0 +1,99 @@ +import { v4 as uuidv4 } from 'uuid'; +import { memberCollection, coffeeChatsCollection, db } from '../firebase'; +import { DBCoffeeChat } from '../types/DataTypes'; +import { getMemberFromDocumentReference } from '../utils/memberUtil'; +import BaseDao from './BaseDao'; +import { deleteCollection } from '../utils/firebase-utils'; + +async function materializeCoffeeChat(dbCoffeeChat: DBCoffeeChat): Promise { + const member1 = await getMemberFromDocumentReference(dbCoffeeChat.members[0]); + const member2 = await getMemberFromDocumentReference(dbCoffeeChat.members[1]); + + return { + ...dbCoffeeChat, + members: [member1, member2] + }; +} + +async function serializeCoffeeChat(coffeeChat: CoffeeChat): Promise { + const member1Data = memberCollection.doc(coffeeChat.members[0].email); + const member2Data = memberCollection.doc(coffeeChat.members[1].email); + + return { + ...coffeeChat, + members: [member1Data, member2Data] + }; +} + +export default class CoffeeChatDao extends BaseDao { + constructor() { + super(coffeeChatsCollection, materializeCoffeeChat, serializeCoffeeChat); + } + + /** + * Creates a new coffee chat for member + * @param coffeeChat - Newly created CoffeeChat object. + * If provided, the object uuid will be used. If not, a new one will be generated. + * The pending field will be set to true by default. + */ + async createCoffeeChat(coffeeChat: CoffeeChat): Promise { + const coffeeChatWithUUID = { + ...coffeeChat, + status: 'pending' as Status, + uuid: coffeeChat.uuid ? coffeeChat.uuid : uuidv4() + }; + return this.createDocument(coffeeChatWithUUID.uuid, coffeeChatWithUUID); + } + + /** + * Gets the coffee chats + * @param uuid - DB uuid of coffee chat + */ + async getCoffeeChat(uuid: string): Promise { + return this.getDocument(uuid); + } + + /** + * Gets all coffee chats + */ + async getAllCoffeeChats(): Promise { + return this.getDocuments(); + } + + /** + * Updates a coffee chat + * @param coffeeChat - updated Coffee Chat object + */ + async updateCoffeeChat(coffeeChat: CoffeeChat): Promise { + return this.updateDocument(coffeeChat.uuid, coffeeChat); + } + + /** + * Gets all coffee chat for a user + * @param user - user whose coffee chats should be fetched + */ + async getCoffeeChatsByUser(user: IdolMember): Promise { + return this.getDocuments([ + { + field: 'members', + comparisonOperator: 'array-contains', + value: memberCollection.doc(user.email) + } + ]); + } + + /** + * Deletes a coffee chat + * @param uuid - DB uuid of CoffeeChat + */ + async deleteCoffeeChat(uuid: string): Promise { + await this.deleteDocument(uuid); + } + + /** + * Deletes all coffee chats for all users + */ + static async clearAllCoffeeChats(): Promise { + await deleteCollection(db, 'coffee-chats', 500); + } +} diff --git a/backend/src/utils/permissionsManager.ts b/backend/src/utils/permissionsManager.ts index 2b8ed1bf..9bd2865d 100644 --- a/backend/src/utils/permissionsManager.ts +++ b/backend/src/utils/permissionsManager.ts @@ -37,6 +37,10 @@ export default class PermissionsManager { return this.isLeadOrAdmin(mem); } + static async canEditCoffeeChat(mem: IdolMember): Promise { + return this.isLeadOrAdmin(mem); + } + static async canEditDevPortfolio(mem: IdolMember): Promise { return this.isLeadOrAdmin(mem); }