From 75c7e0578673cc8921a7eb0331326cc519580c26 Mon Sep 17 00:00:00 2001 From: Ido Date: Sun, 3 Nov 2024 02:07:52 -0500 Subject: [PATCH 01/13] Approver room assignments now strictly enforced --- backend/src/api/routes/requests.ts | 2 +- backend/src/models/requestsModel.ts | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/backend/src/api/routes/requests.ts b/backend/src/api/routes/requests.ts index 25b2bb4..e801660 100644 --- a/backend/src/api/routes/requests.ts +++ b/backend/src/api/routes/requests.ts @@ -43,7 +43,7 @@ router.delete('/:id', async (req, res) => { sendResponse(res, await requestsModel.setRequestStatus(req.params.id, req.user, RequestStatus.cancelled)); }); router.put('/:id/approve', permissionMiddleware(PermissionLevel.approver), async (req, res) => { - sendResponse(res, await requestsModel.approveRequest(req.params.id, req.body.reason)); + sendResponse(res, await requestsModel.approveRequest(req.params.id, req.body.reason, req.user)); }); router.put('/:id/deny', permissionMiddleware(PermissionLevel.staff), async (req, res) => { sendResponse( diff --git a/backend/src/models/requestsModel.ts b/backend/src/models/requestsModel.ts index 9bb9a2e..fd16de5 100644 --- a/backend/src/models/requestsModel.ts +++ b/backend/src/models/requestsModel.ts @@ -299,17 +299,25 @@ export default { await triggerAdminNotification(EventTypes.ADMIN_BOOKING_STATUS_CHANGED, context); return { status: 200, data: {} }; }, - approveRequest: async (id: string, reason?: string) => { + approveRequest: async (id: string, reason?: string, approver: User) => { const request = await db.request.findUnique({ where: { id }, include: { - room: { include: { userAccess: { select: { utorid: true } } } }, - author: { select: { utorid: true } }, - }, + room: { + include: { + userAccess: { select: { utorid: true } }, + approvers: { select: { utorid: true } } + } + }, + author: { select: { utorid: true } } + } }); if (!request) { return { status: 404, message: 'Request ID not found.' }; } + if(!request.room.approvers.some(user=>user.utorid === approver.utorid)) { + return {status: 403, message: "You cannot approve requests for this room."}; + } if (request.status !== RequestStatus.pending) { return { status: 400, message: 'Request is not pending.' }; } From 9a5a208c96a236d368c237485c45ea49704f5ea7 Mon Sep 17 00:00:00 2001 From: Ido Date: Sun, 3 Nov 2024 02:20:46 -0500 Subject: [PATCH 02/13] Fixed eslint --- backend/src/api/routes/requests.ts | 2 +- backend/src/models/requestsModel.ts | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/backend/src/api/routes/requests.ts b/backend/src/api/routes/requests.ts index e801660..cd295b4 100644 --- a/backend/src/api/routes/requests.ts +++ b/backend/src/api/routes/requests.ts @@ -43,7 +43,7 @@ router.delete('/:id', async (req, res) => { sendResponse(res, await requestsModel.setRequestStatus(req.params.id, req.user, RequestStatus.cancelled)); }); router.put('/:id/approve', permissionMiddleware(PermissionLevel.approver), async (req, res) => { - sendResponse(res, await requestsModel.approveRequest(req.params.id, req.body.reason, req.user)); + sendResponse(res, await requestsModel.approveRequest(req.params.id, req.user, req.body.reason)); }); router.put('/:id/deny', permissionMiddleware(PermissionLevel.staff), async (req, res) => { sendResponse( diff --git a/backend/src/models/requestsModel.ts b/backend/src/models/requestsModel.ts index fd16de5..2c6ac56 100644 --- a/backend/src/models/requestsModel.ts +++ b/backend/src/models/requestsModel.ts @@ -299,24 +299,24 @@ export default { await triggerAdminNotification(EventTypes.ADMIN_BOOKING_STATUS_CHANGED, context); return { status: 200, data: {} }; }, - approveRequest: async (id: string, reason?: string, approver: User) => { + approveRequest: async (id: string, approver: User, reason?: string) => { const request = await db.request.findUnique({ where: { id }, include: { room: { include: { userAccess: { select: { utorid: true } }, - approvers: { select: { utorid: true } } - } + approvers: { select: { utorid: true } }, + }, }, - author: { select: { utorid: true } } - } + author: { select: { utorid: true } }, + }, }); if (!request) { return { status: 404, message: 'Request ID not found.' }; } - if(!request.room.approvers.some(user=>user.utorid === approver.utorid)) { - return {status: 403, message: "You cannot approve requests for this room."}; + if (!request.room.approvers.some((user) => user.utorid === approver.utorid)) { + return { status: 403, message: 'You cannot approve requests for this room.' }; } if (request.status !== RequestStatus.pending) { return { status: 400, message: 'Request is not pending.' }; @@ -358,8 +358,8 @@ export default { }); const context = { ...(await generateBaseNotificationContext(requestFetched)), - changer_utorid: requestFetched.author.utorid, - changer_full_name: requestFetched.author.name, + changer_utorid: approver.utorid, + changer_full_name: approver.name, status, reason, }; From 1f194a8467490acbe50daa8411c0caa2d5a8e4e0 Mon Sep 17 00:00:00 2001 From: Ido Date: Sun, 3 Nov 2024 03:00:48 -0500 Subject: [PATCH 03/13] Enforce room specific approvers on request deny --- backend/src/models/requestsModel.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/src/models/requestsModel.ts b/backend/src/models/requestsModel.ts index 2c6ac56..e8e01c8 100644 --- a/backend/src/models/requestsModel.ts +++ b/backend/src/models/requestsModel.ts @@ -255,6 +255,7 @@ export default { include: { group: { include: { managers: { select: { utorid: true } } } }, author: { select: { utorid: true } }, + room: { include: { approvers: { select: { utorid: true } } } }, }, }); if (!request) { @@ -271,8 +272,9 @@ export default { }; } if ( - status === RequestStatus.denied && - !([AccountRole.approver, AccountRole.admin]).includes(user.role) + (status === RequestStatus.denied && + !([AccountRole.approver, AccountRole.admin]).includes(user.role)) || + !request.room.approvers.some((x) => x.utorid == user.utorid) ) { return { status: 403, From 2b74e0cd852e4472ee83ec2d05644c3d49979939 Mon Sep 17 00:00:00 2001 From: Ido Date: Mon, 4 Nov 2024 01:34:16 -0500 Subject: [PATCH 04/13] Auto approve requests by approves --- backend/src/index.ts | 6 + backend/src/models/requestsModel.ts | 168 ++++++++++-------- .../ApproverPicker/ApproverPicker.tsx | 15 +- 3 files changed, 109 insertions(+), 80 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index 87f68fb..a2896ae 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -18,3 +18,9 @@ common.db logger.error(`${e.name}\n${e.message}\n${e.stack}`); process.exit(1); }); + +process.on('exit', () => { + common.db.$disconnect().then(() => { + process.exit(0); + }); +}); diff --git a/backend/src/models/requestsModel.ts b/backend/src/models/requestsModel.ts index e8e01c8..7791900 100644 --- a/backend/src/models/requestsModel.ts +++ b/backend/src/models/requestsModel.ts @@ -1,4 +1,4 @@ -import { AccountRole, RequestStatus, User, Request } from '@prisma/client'; +import { AccountRole, RequestStatus, User } from '@prisma/client'; import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; import db from '../common/db'; import logger from '../common/logger'; @@ -7,13 +7,12 @@ import Model from '../types/Model'; import { ModelResponseError } from '../types/ModelResponse'; import { triggerAdminNotification, triggerMassNotification, triggerTCardNotification } from '../notifications'; import EventTypes from '../types/EventTypes'; - -import { BaseBookingContext } from '../types/NotificationContext'; import { userSelector } from './utils'; import { generateBaseRequestNotificationContext as generateBaseNotificationContext, generateUserActionContext, } from '../notifications/generateContext'; + const requestCounter = () => { return { select: { @@ -75,6 +74,83 @@ const validateRequest = async (request: CreateRequest): Promise { + const request = await db.request.findUnique({ + where: { id }, + include: { + room: { + include: { + userAccess: { select: { utorid: true } }, + approvers: { select: { utorid: true } }, + }, + }, + author: { select: { utorid: true } }, + }, + }); + if (!request) { + return { status: 404, message: 'Request ID not found.' }; + } + if (!request.room.approvers.some((user: { utorid: string }) => user.utorid === approver.utorid)) { + return { status: 403, message: 'You cannot approve requests for this room.' }; + } + if (request.status !== RequestStatus.pending) { + return { status: 400, message: 'Request is not pending.' }; + } + let status: RequestStatus = RequestStatus.needTCard; + if (request.room.userAccess.some((user: { utorid: string }) => user.utorid === request.author.utorid)) { + status = RequestStatus.completed; + } + const requestFetched = await db.request.update({ + where: { id: request.id }, + data: { + reason, + status: status, + }, + include: { + group: true, + room: true, + author: true, + }, + }); + const { startDate, endDate, roomName } = request; + await db.request.updateMany({ + where: { + OR: [ + { startDate: { lte: startDate }, endDate: { gte: endDate } }, + { + startDate: { gte: startDate, lte: endDate }, + }, + { + endDate: { gte: startDate, lte: endDate }, + }, + ], + roomName: roomName, + status: RequestStatus.pending, + }, + data: { + status: RequestStatus.denied, + }, + }); + const context = { + ...(await generateBaseNotificationContext(requestFetched)), + changer_utorid: approver.utorid, + changer_full_name: approver.name, + status, + reason, + }; + await triggerMassNotification(EventTypes.BOOKING_STATUS_CHANGED, [requestFetched.author], context); + await triggerAdminNotification(EventTypes.ADMIN_BOOKING_STATUS_CHANGED, context); + if (status !== RequestStatus.completed) { + await triggerTCardNotification(EventTypes.ROOM_ACCESS_REQUESTED, { + ...generateUserActionContext(requestFetched.author), + room: requestFetched.room.roomName, + room_friendly: requestFetched.room.friendlyName, + }); + } + return { status: 200, data: {} }; +}; + export default { getRequests: async (filters: { [key: string]: string }, user: User) => { const query: { [key: string]: unknown } = {}; @@ -173,7 +249,10 @@ export default { newRequest.approvers = newRequest.approvers || []; const userFetched = await db.user.findUnique({ where: { utorid: newRequest.authorUtorid }, - include: { groups: { select: { id: true } } }, + include: { + groups: { select: { id: true } }, + roomApprover: { select: { roomName: true } }, + }, }); if (!userFetched) { return { status: 404, message: 'User not found.' }; @@ -185,6 +264,7 @@ export default { }; } request.approvers = request.approvers ?? []; + try { const madeRequest = await db.request.create({ data: { @@ -206,7 +286,11 @@ export default { }, }); const context = await generateBaseNotificationContext(madeRequest); - await triggerMassNotification(EventTypes.BOOKING_APPROVAL_REQUESTED, request.approvers, context); + if (userFetched.roomApprover.map((x) => x.roomName).includes(request.roomName)) { + await approveRequest(madeRequest.id, user, 'Request automatically accepted by approver'); + } else { + await triggerMassNotification(EventTypes.BOOKING_APPROVAL_REQUESTED, request.approvers, context); + } await triggerAdminNotification(EventTypes.ADMIN_BOOKING_CREATED, context); // Delete approver data from response const tempRequest: WithOptional = madeRequest; @@ -302,79 +386,7 @@ export default { return { status: 200, data: {} }; }, approveRequest: async (id: string, approver: User, reason?: string) => { - const request = await db.request.findUnique({ - where: { id }, - include: { - room: { - include: { - userAccess: { select: { utorid: true } }, - approvers: { select: { utorid: true } }, - }, - }, - author: { select: { utorid: true } }, - }, - }); - if (!request) { - return { status: 404, message: 'Request ID not found.' }; - } - if (!request.room.approvers.some((user) => user.utorid === approver.utorid)) { - return { status: 403, message: 'You cannot approve requests for this room.' }; - } - if (request.status !== RequestStatus.pending) { - return { status: 400, message: 'Request is not pending.' }; - } - let status: RequestStatus = RequestStatus.needTCard; - if (request.room.userAccess.some((user) => user.utorid === request.author.utorid)) { - status = RequestStatus.completed; - } - const requestFetched = await db.request.update({ - where: { id }, - data: { - reason, - status: status, - }, - include: { - group: true, - room: true, - author: true, - }, - }); - const { startDate, endDate, roomName } = request; - await db.request.updateMany({ - where: { - OR: [ - { startDate: { lte: startDate }, endDate: { gte: endDate } }, - { - startDate: { gte: startDate, lte: endDate }, - }, - { - endDate: { gte: startDate, lte: endDate }, - }, - ], - roomName: roomName, - status: RequestStatus.pending, - }, - data: { - status: RequestStatus.denied, - }, - }); - const context = { - ...(await generateBaseNotificationContext(requestFetched)), - changer_utorid: approver.utorid, - changer_full_name: approver.name, - status, - reason, - }; - await triggerMassNotification(EventTypes.BOOKING_STATUS_CHANGED, [requestFetched.author], context); - await triggerAdminNotification(EventTypes.ADMIN_BOOKING_STATUS_CHANGED, context); - if (status !== RequestStatus.completed) { - await triggerTCardNotification(EventTypes.ROOM_ACCESS_REQUESTED, { - ...generateUserActionContext(requestFetched.author), - room: requestFetched.room.roomName, - room_friendly: requestFetched.room.friendlyName, - }); - } - return { status: 200, data: {} }; + return approveRequest(id, approver, reason); }, updateRequest: async ( request: CreateRequest & { diff --git a/frontend/src/components/ApproverPicker/ApproverPicker.tsx b/frontend/src/components/ApproverPicker/ApproverPicker.tsx index 108e999..be9c37a 100644 --- a/frontend/src/components/ApproverPicker/ApproverPicker.tsx +++ b/frontend/src/components/ApproverPicker/ApproverPicker.tsx @@ -1,7 +1,8 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useContext, useMemo } from 'react'; import { Select, MenuItem, Checkbox, ListItemText } from '@mui/material'; import axios from '../../axios'; import { SelectChangeEvent } from '@mui/material/Select/SelectInput'; +import { UserContext } from '../../contexts/UserContext'; interface ApproverPickerProps { /** a function that takes in an array of approvers as utorids and sets the approvers for the form */ @@ -18,11 +19,17 @@ interface ApproverPickerProps { * @property setApprovers a function that takes in an array of approvers and sets the approvers for the form */ export const ApproverPicker = ({ setApprovers, selectedApprovers = [], roomName }: ApproverPickerProps) => { + const { userInfo } = useContext(UserContext); const [open, setOpen] = useState(false); const [selected, setSelected] = useState(selectedApprovers); /** an array of all the approvers that can be chosen */ const [approvers, setApproversBackend] = useState([]); + const needApprover = useMemo( + () => !approvers.some((approver) => approver.utorid === userInfo.utorid), + [approvers, userInfo.utorid], + ); + useEffect(() => { (async () => { await axios @@ -30,7 +37,10 @@ export const ApproverPicker = ({ setApprovers, selectedApprovers = [], roomName .then(({ data }) => { if (data.approvers) { setApproversBackend(data.approvers); - if (data.approvers.length === 1 && selectedApprovers.length === 0) { + if (!needApprover) { + setSelected([userInfo.utorid]); + setApprovers([userInfo.utorid]); + } else if (data.approvers.length === 1 && selectedApprovers.length === 0) { setSelected([data.approvers[0].utorid]); setApprovers([data.approvers[0].utorid]); } @@ -59,6 +69,7 @@ export const ApproverPicker = ({ setApprovers, selectedApprovers = [], roomName return (