diff --git a/src/backend/src/controllers/notifications.controllers.ts b/src/backend/src/controllers/notifications.controllers.ts index bdf9c44f13..0d944632bd 100644 --- a/src/backend/src/controllers/notifications.controllers.ts +++ b/src/backend/src/controllers/notifications.controllers.ts @@ -11,4 +11,34 @@ export default class NotificationsController { next(error); } } + + static async getUserUnreadNotifications(req: Request, res: Response, next: NextFunction) { + try { + const { organization, currentUser } = req; + + const unreadNotifications = await NotificationsService.getUserUnreadNotifications( + currentUser.userId, + organization.organizationId + ); + res.status(200).json(unreadNotifications); + } catch (error: unknown) { + next(error); + } + } + + static async removeUserNotification(req: Request, res: Response, next: NextFunction) { + try { + const { notificationId } = req.body; + const { organization, currentUser } = req; + + const unreadNotifications = await NotificationsService.removeUserNotification( + currentUser.userId, + notificationId, + organization.organizationId + ); + res.status(200).json(unreadNotifications); + } catch (error: unknown) { + next(error); + } + } } diff --git a/src/backend/src/controllers/users.controllers.ts b/src/backend/src/controllers/users.controllers.ts index baa0d9228a..e9c0745b5c 100644 --- a/src/backend/src/controllers/users.controllers.ts +++ b/src/backend/src/controllers/users.controllers.ts @@ -192,33 +192,6 @@ export default class UsersController { } } - static async getUserUnreadNotifications(req: Request, res: Response, next: NextFunction) { - try { - const { organization, currentUser } = req; - - const unreadNotifications = await UsersService.getUserUnreadNotifications(currentUser.userId, organization); - res.status(200).json(unreadNotifications); - } catch (error: unknown) { - next(error); - } - } - - static async removeUserNotification(req: Request, res: Response, next: NextFunction) { - try { - const { notificationId } = req.body; - const { organization, currentUser } = req; - - const unreadNotifications = await UsersService.removeUserNotification( - currentUser.userId, - notificationId, - organization - ); - res.status(200).json(unreadNotifications); - } catch (error: unknown) { - next(error); - } - } - static async getUserUnreadAnnouncements(req: Request, res: Response, next: NextFunction) { try { const { organization, currentUser } = req; diff --git a/src/backend/src/routes/notifications.routes.ts b/src/backend/src/routes/notifications.routes.ts index 4701b0f3ea..95cde92c09 100644 --- a/src/backend/src/routes/notifications.routes.ts +++ b/src/backend/src/routes/notifications.routes.ts @@ -1,8 +1,16 @@ import express from 'express'; import NotificationsController from '../controllers/notifications.controllers'; +import { nonEmptyString } from '../utils/validation.utils'; +import { body } from 'express-validator'; const notificationsRouter = express.Router(); notificationsRouter.post('/task-deadlines', NotificationsController.sendDailySlackNotifications); +notificationsRouter.get('/current-user', NotificationsController.getUserUnreadNotifications); +notificationsRouter.post( + '/curent-user/remove', + nonEmptyString(body('notificationId')), + NotificationsController.removeUserNotification +); export default notificationsRouter; diff --git a/src/backend/src/routes/users.routes.ts b/src/backend/src/routes/users.routes.ts index b859968e5a..077e2fd02f 100644 --- a/src/backend/src/routes/users.routes.ts +++ b/src/backend/src/routes/users.routes.ts @@ -54,8 +54,6 @@ userRouter.post( validateInputs, UsersController.getManyUserTasks ); -userRouter.get('/notifications/current-user', UsersController.getUserUnreadNotifications); userRouter.get('/announcements/current-user', UsersController.getUserUnreadAnnouncements); -userRouter.post('/notifications/remove', nonEmptyString(body('notificationId')), UsersController.removeUserNotification); export default userRouter; diff --git a/src/backend/src/services/notifications.services.ts b/src/backend/src/services/notifications.services.ts index e0617301f5..3da62b744f 100644 --- a/src/backend/src/services/notifications.services.ts +++ b/src/backend/src/services/notifications.services.ts @@ -196,6 +196,52 @@ export default class NotificationsService { await Promise.all(promises); } + /** + * Gets all of a user's unread notifications + * @param userId id of user to get unread notifications from + * @param organization the user's orgainzation + * @returns the unread notifications of the user + */ + static async getUserUnreadNotifications(userId: string, organizationId: string) { + const unreadNotifications = await prisma.notification.findMany({ + where: { + users: { + some: { userId } + } + }, + ...getNotificationQueryArgs(organizationId) + }); + + if (!unreadNotifications) throw new HttpException(404, 'User Unread Notifications Not Found'); + + return unreadNotifications.map(notificationTransformer); + } + + /** + * Removes a notification from the user's unread notifications + * @param userId id of the current user + * @param notificationId id of the notification to remove + * @param organization the user's organization + * @returns the user's updated unread notifications + */ + static async removeUserNotification(userId: string, notificationId: string, organizationId: string) { + const updatedUser = await prisma.user.update({ + where: { userId }, + data: { + unreadNotifications: { + disconnect: { + notificationId + } + } + }, + include: { unreadNotifications: getNotificationQueryArgs(organizationId) } + }); + + if (!updatedUser) throw new HttpException(404, `Failed to remove notication: ${notificationId}`); + + return updatedUser.unreadNotifications.map(notificationTransformer); + } + /** * Creates and sends a notification to all users with the given userIds * @param text writing in the notification diff --git a/src/backend/src/services/users.services.ts b/src/backend/src/services/users.services.ts index b537d019d2..3dc8766113 100644 --- a/src/backend/src/services/users.services.ts +++ b/src/backend/src/services/users.services.ts @@ -38,8 +38,6 @@ import { getAuthUserQueryArgs } from '../prisma-query-args/auth-user.query-args' import authenticatedUserTransformer from '../transformers/auth-user.transformer'; import { getTaskQueryArgs } from '../prisma-query-args/tasks.query-args'; import taskTransformer from '../transformers/tasks.transformer'; -import { getNotificationQueryArgs } from '../prisma-query-args/notifications.query-args'; -import notificationTransformer from '../transformers/notifications.transformer'; import { getAnnouncementQueryArgs } from '../prisma-query-args/announcements.query.args'; import announcementTransformer from '../transformers/announcements.transformer'; @@ -571,27 +569,6 @@ export default class UsersService { return resolvedTasks.flat(); } - /** - * Gets all of a user's unread notifications - * @param userId id of user to get unread notifications from - * @param organization the user's orgainzation - * @returns the unread notifications of the user - */ - static async getUserUnreadNotifications(userId: string, organization: Organization) { - const unreadNotifications = await prisma.notification.findMany({ - where: { - users: { - some: { userId } - } - }, - ...getNotificationQueryArgs(organization.organizationId) - }); - - if (!unreadNotifications) throw new HttpException(404, 'User Unread Notifications Not Found'); - - return unreadNotifications.map(notificationTransformer); - } - /** * Gets all of a user's unread announcements * @param userId id of the current user @@ -612,29 +589,4 @@ export default class UsersService { return unreadAnnouncements.map(announcementTransformer); } - - /** - * Removes a notification from the user's unread notifications - * @param userId id of the current user - * @param notificationId id of the notification to remove - * @param organization the user's organization - * @returns the user's updated unread notifications - */ - static async removeUserNotification(userId: string, notificationId: string, organization: Organization) { - const updatedUser = await prisma.user.update({ - where: { userId }, - data: { - unreadNotifications: { - disconnect: { - notificationId - } - } - }, - include: { unreadNotifications: getNotificationQueryArgs(organization.organizationId) } - }); - - if (!updatedUser) throw new HttpException(404, `Failed to remove notication: ${notificationId}`); - - return updatedUser.unreadNotifications.map(notificationTransformer); - } } diff --git a/src/backend/src/utils/auth.utils.ts b/src/backend/src/utils/auth.utils.ts index 8520d881e4..7e7602eb6a 100644 --- a/src/backend/src/utils/auth.utils.ts +++ b/src/backend/src/utils/auth.utils.ts @@ -66,7 +66,7 @@ export const requireJwtDev = (req: Request, res: Response, next: NextFunction) = ) { next(); } else if ( - req.path.startsWith('/notifications') // task deadline notification endpoint + req.path.startsWith('/notifications/taskdeadlines') // task deadline notification endpoint ) { notificationEndpointAuth(req, res, next); } else { diff --git a/src/backend/src/utils/notifications.utils.ts b/src/backend/src/utils/notifications.utils.ts index c47ba9b7a0..445f36ed67 100644 --- a/src/backend/src/utils/notifications.utils.ts +++ b/src/backend/src/utils/notifications.utils.ts @@ -78,11 +78,7 @@ export const sendHomeCrReviewedNotification = async ( accepted: boolean, organizationId: string ) => { - const isProd = process.env.NODE_ENV === 'production'; - - const changeRequestLink = isProd - ? `https://finishlinebyner.com/change-requests/${changeRequest.crId}` - : `http://localhost:3000/change-requests/${changeRequest.crId}`; + const changeRequestLink = `/change-requests/${changeRequest.crId}`; await NotificationsService.sendNotifcationToUsers( `CR #${changeRequest.identifier} has been ${accepted ? 'approved!' : 'denied.'}`, accepted ? 'check_circle' : 'cancel', diff --git a/src/backend/tests/unmocked/notifications.test.ts b/src/backend/tests/unmocked/notifications.test.ts index d3cce68361..026099c1c3 100644 --- a/src/backend/tests/unmocked/notifications.test.ts +++ b/src/backend/tests/unmocked/notifications.test.ts @@ -56,4 +56,47 @@ describe('Notifications Tests', () => { expect(supermanWithNotifications?.unreadNotifications[0].text).toBe('test notification'); }); }); + + describe('Get Notifications', () => { + it('Succeeds and gets user notifications', async () => { + const testBatman = await createTestUser(batmanAppAdmin, orgId); + await NotificationService.sendNotifcationToUsers('test1', 'test1', [testBatman.userId], orgId); + await NotificationService.sendNotifcationToUsers('test2', 'test2', [testBatman.userId], orgId); + + const notifications = await NotificationService.getUserUnreadNotifications( + testBatman.userId, + organization.organizationId + ); + + expect(notifications).toHaveLength(2); + expect(notifications[0].text).toBe('test1'); + expect(notifications[1].text).toBe('test2'); + }); + }); + + describe('Remove Notifications', () => { + it('Succeeds and removes user notification', async () => { + const testBatman = await createTestUser(batmanAppAdmin, orgId); + await NotificationService.sendNotifcationToUsers('test1', 'test1', [testBatman.userId], orgId); + await NotificationService.sendNotifcationToUsers('test2', 'test2', [testBatman.userId], orgId); + + const notifications = await NotificationService.getUserUnreadNotifications( + testBatman.userId, + organization.organizationId + ); + + expect(notifications).toHaveLength(2); + expect(notifications[0].text).toBe('test1'); + expect(notifications[1].text).toBe('test2'); + + const updatedNotifications = await NotificationService.removeUserNotification( + testBatman.userId, + notifications[0].notificationId, + organization.organizationId + ); + + expect(updatedNotifications).toHaveLength(1); + expect(updatedNotifications[0].text).toBe('test2'); + }); + }); }); diff --git a/src/backend/tests/unmocked/users.test.ts b/src/backend/tests/unmocked/users.test.ts index a9f1488530..c78a42d76f 100644 --- a/src/backend/tests/unmocked/users.test.ts +++ b/src/backend/tests/unmocked/users.test.ts @@ -3,7 +3,6 @@ import { createTestOrganization, createTestTask, createTestUser, resetUsers } fr import { batmanAppAdmin } from '../test-data/users.test-data'; import UsersService from '../../src/services/users.services'; import { NotFoundException } from '../../src/utils/errors.utils'; -import NotificationsService from '../../src/services/notifications.services'; import AnnouncementService from '../../src/services/announcement.service'; describe('User Tests', () => { @@ -51,43 +50,6 @@ describe('User Tests', () => { }); }); - describe('Get Notifications', () => { - it('Succeeds and gets user notifications', async () => { - const testBatman = await createTestUser(batmanAppAdmin, orgId); - await NotificationsService.sendNotifcationToUsers('test1', 'test1', [testBatman.userId], orgId); - await NotificationsService.sendNotifcationToUsers('test2', 'test2', [testBatman.userId], orgId); - - const notifications = await UsersService.getUserUnreadNotifications(testBatman.userId, organization); - - expect(notifications).toHaveLength(2); - expect(notifications[0].text).toBe('test1'); - expect(notifications[1].text).toBe('test2'); - }); - }); - - describe('Remove Notifications', () => { - it('Succeeds and removes user notification', async () => { - const testBatman = await createTestUser(batmanAppAdmin, orgId); - await NotificationsService.sendNotifcationToUsers('test1', 'test1', [testBatman.userId], orgId); - await NotificationsService.sendNotifcationToUsers('test2', 'test2', [testBatman.userId], orgId); - - const notifications = await UsersService.getUserUnreadNotifications(testBatman.userId, organization); - - expect(notifications).toHaveLength(2); - expect(notifications[0].text).toBe('test1'); - expect(notifications[1].text).toBe('test2'); - - const updatedNotifications = await UsersService.removeUserNotification( - testBatman.userId, - notifications[0].notificationId, - organization - ); - - expect(updatedNotifications).toHaveLength(1); - expect(updatedNotifications[0].text).toBe('test2'); - }); - }); - describe('Get Announcements', () => { it('Succeeds and gets user announcements', async () => { const testBatman = await createTestUser(batmanAppAdmin, orgId); diff --git a/src/frontend/src/apis/notifications.api.ts b/src/frontend/src/apis/notifications.api.ts new file mode 100644 index 0000000000..a7cbd1894c --- /dev/null +++ b/src/frontend/src/apis/notifications.api.ts @@ -0,0 +1,19 @@ +import axios from 'axios'; +import { apiUrls } from '../utils/urls'; +import { Notification } from 'shared'; + +/* + * Gets all unread notifications of the user with the given id + */ +export const getNotifications = () => { + return axios.get(apiUrls.notificationsCurrentUser(), { + transformResponse: (data) => JSON.parse(data) + }); +}; + +/* + * Removes a notification from the user with the given id + */ +export const removeNotification = (notificationId: string) => { + return axios.post(apiUrls.notificationsRemoveCurrentUser(), { notificationId }); +}; diff --git a/src/frontend/src/apis/users.api.ts b/src/frontend/src/apis/users.api.ts index 4999c8cb66..afa5ea00f6 100644 --- a/src/frontend/src/apis/users.api.ts +++ b/src/frontend/src/apis/users.api.ts @@ -5,7 +5,6 @@ import axios from '../utils/axios'; import { - Notification, Project, SetUserScheduleSettingsPayload, Task, @@ -160,19 +159,3 @@ export const getManyUserTasks = (userIds: string[]) => { } ); }; - -/* - * Gets all unread notifications of the user with the given id - */ -export const getNotifications = () => { - return axios.get(apiUrls.userNotifications(), { - transformResponse: (data) => JSON.parse(data) - }); -}; - -/* - * Removes a notification from the user with the given id - */ -export const removeNotification = (notificationId: string) => { - return axios.post(apiUrls.userRemoveNotifications(), { notificationId }); -}; diff --git a/src/frontend/src/components/NotificationAlert.tsx b/src/frontend/src/components/NotificationAlert.tsx index 668c0f3cfd..53b01a5aaa 100644 --- a/src/frontend/src/components/NotificationAlert.tsx +++ b/src/frontend/src/components/NotificationAlert.tsx @@ -2,11 +2,11 @@ import { Box } from '@mui/material'; import React, { useEffect, useState } from 'react'; import { Notification } from 'shared'; import NotificationCard from './NotificationCard'; -import { useRemoveUserNotification, useUserNotifications } from '../hooks/users.hooks'; import { useHistory } from 'react-router-dom'; +import { useCurrentUserNotifications, useRemoveUserNotification } from '../hooks/notifications.hooks'; const NotificationAlert: React.FC = () => { - const { data: notifications, isLoading: notificationsIsLoading } = useUserNotifications(); + const { data: notifications, isLoading: notificationsIsLoading } = useCurrentUserNotifications(); const { mutateAsync: removeNotification, isLoading: removeIsLoading } = useRemoveUserNotification(); const [currentNotification, setCurrentNotification] = useState(); const history = useHistory(); diff --git a/src/frontend/src/hooks/notifications.hooks.ts b/src/frontend/src/hooks/notifications.hooks.ts new file mode 100644 index 0000000000..0f7daea18b --- /dev/null +++ b/src/frontend/src/hooks/notifications.hooks.ts @@ -0,0 +1,36 @@ +import { useMutation, useQuery, useQueryClient } from 'react-query'; +import { Notification } from 'shared'; +import { getNotifications, removeNotification } from '../apis/notifications.api'; + +/** + * Curstom react hook to get all unread notifications from a user + * @param userId id of user to get unread notifications from + * @returns + */ +export const useCurrentUserNotifications = () => { + return useQuery(['notifications', 'current-user'], async () => { + const { data } = await getNotifications(); + return data; + }); +}; + +/** + * Curstom react hook to remove a notification from a user's unread notifications + * @param userId id of user to get unread notifications from + * @returns + */ +export const useRemoveUserNotification = () => { + const queryClient = useQueryClient(); + return useMutation( + ['notifications', 'currentUser', 'remove'], + async (notification: Notification) => { + const { data } = await removeNotification(notification.notificationId); + return data; + }, + { + onSuccess: () => { + queryClient.invalidateQueries(['notifications', 'currentUser']); + } + } + ); +}; diff --git a/src/frontend/src/hooks/users.hooks.ts b/src/frontend/src/hooks/users.hooks.ts index ffc7a65d75..96b659c1f1 100644 --- a/src/frontend/src/hooks/users.hooks.ts +++ b/src/frontend/src/hooks/users.hooks.ts @@ -19,9 +19,7 @@ import { getUserScheduleSettings, updateUserScheduleSettings, getUserTasks, - getManyUserTasks, - getNotifications, - removeNotification + getManyUserTasks } from '../apis/users.api'; import { User, @@ -33,8 +31,7 @@ import { UserScheduleSettings, UserWithScheduleSettings, SetUserScheduleSettingsPayload, - Task, - Notification + Task } from 'shared'; import { useAuth } from './auth.hooks'; import { useContext } from 'react'; @@ -263,36 +260,3 @@ export const useManyUserTasks = (userIds: string[]) => { return data; }); }; - -/** - * Curstom react hook to get all unread notifications from a user - * @param userId id of user to get unread notifications from - * @returns - */ -export const useUserNotifications = () => { - return useQuery(['users', 'notifications'], async () => { - const { data } = await getNotifications(); - return data; - }); -}; - -/** - * Curstom react hook to remove a notification from a user's unread notifications - * @param userId id of user to get unread notifications from - * @returns - */ -export const useRemoveUserNotification = () => { - const queryClient = useQueryClient(); - return useMutation( - ['users', 'notifications', 'remove'], - async (notification: Notification) => { - const { data } = await removeNotification(notification.notificationId); - return data; - }, - { - onSuccess: () => { - queryClient.invalidateQueries(['users', 'notifications']); - } - } - ); -}; diff --git a/src/frontend/src/utils/urls.ts b/src/frontend/src/utils/urls.ts index 99c9d82c82..93d6d80cf0 100644 --- a/src/frontend/src/utils/urls.ts +++ b/src/frontend/src/utils/urls.ts @@ -26,8 +26,6 @@ const userScheduleSettings = (id: string) => `${usersById(id)}/schedule-settings const userScheduleSettingsSet = () => `${users()}/schedule-settings/set`; const userTasks = (id: string) => `${usersById(id)}/tasks`; const manyUserTasks = () => `${users()}/tasks/get-many`; -const userNotifications = () => `${users()}/notifications/current-user`; -const userRemoveNotifications = () => `${users()}/notifications/remove`; /**************** Projects Endpoints ****************/ const projects = () => `${API_URL}/projects`; @@ -196,6 +194,11 @@ const faqCreate = () => `${recruitment()}/faq/create`; const faqEdit = (id: string) => `${recruitment()}/faq/${id}/edit`; const faqDelete = (id: string) => `${recruitment()}/faq/${id}/delete`; +/************** Notification Endpoints ***************/ +const notifications = () => `${API_URL}/notifications`; +const notificationsCurrentUser = () => `${notifications()}/current-user`; +const notificationsRemoveCurrentUser = () => `${notificationsCurrentUser()}/remove`; + /**************** Other Endpoints ****************/ const version = () => `https://api.github.com/repos/Northeastern-Electric-Racing/FinishLine/releases/latest`; @@ -214,8 +217,6 @@ export const apiUrls = { userScheduleSettingsSet, userTasks, manyUserTasks, - userNotifications, - userRemoveNotifications, projects, allProjects, @@ -355,5 +356,9 @@ export const apiUrls = { faqEdit, faqDelete, + notifications, + notificationsCurrentUser, + notificationsRemoveCurrentUser, + version };