diff --git a/api/main_endpoints/models/PermissionRequest.js b/api/main_endpoints/models/PermissionRequest.js index 5d42f4362..956bbe36c 100644 --- a/api/main_endpoints/models/PermissionRequest.js +++ b/api/main_endpoints/models/PermissionRequest.js @@ -1,6 +1,7 @@ const mongoose = require('mongoose'); const Schema = mongoose.Schema; const PermissionRequestTypes = require('../util/permissionRequestTypes'); +const PermissionRequestStatus = require('../../util/constants').PERMISSION_REQUEST_STATUS; const PermissionRequestSchema = new Schema( { @@ -14,16 +15,20 @@ const PermissionRequestSchema = new Schema( enum: Object.values(PermissionRequestTypes), required: true, }, - deletedAt: { - type: Date, - default: null, + status: { + type: String, + enum: Object.values(PermissionRequestStatus), + default: PermissionRequestStatus.PENDING, }, }, { timestamps: { createdAt: true, updatedAt: false } } ); // Compound unique index prevents duplicate active requests per user+type -PermissionRequestSchema.index({ userId: 1, type: 1 }, { unique: true, partialFilterExpression: { deletedAt: null }}); +PermissionRequestSchema.index( + { userId: 1, type: 1 }, + { unique: true, partialFilterExpression: { status: PermissionRequestStatus.PENDING } } +); module.exports = mongoose.model('PermissionRequest', PermissionRequestSchema); diff --git a/api/main_endpoints/routes/PermissionRequest.js b/api/main_endpoints/routes/PermissionRequest.js index 1393ffe89..d4c7ae9b9 100644 --- a/api/main_endpoints/routes/PermissionRequest.js +++ b/api/main_endpoints/routes/PermissionRequest.js @@ -1,7 +1,10 @@ const express = require('express'); const router = express.Router(); const PermissionRequest = require('../models/PermissionRequest'); -const { OK, UNAUTHORIZED, SERVER_ERROR, NOT_FOUND, BAD_REQUEST, CONFLICT } = require('../../util/constants').STATUS_CODES; +const { + STATUS_CODES: { OK, UNAUTHORIZED, SERVER_ERROR, NOT_FOUND, BAD_REQUEST, CONFLICT }, + PERMISSION_REQUEST_STATUS: PermissionRequestStatus, +} = require('../../util/constants'); const membershipState = require('../../util/constants.js').MEMBERSHIP_STATE; const { decodeToken } = require('../util/token-functions.js'); const logger = require('../../util/logger'); @@ -17,11 +20,23 @@ router.post('/create', async (req, res) => { } try { - await PermissionRequest.create({ + const existingRequest = await PermissionRequest.findOne({ userId: decoded.token._id, type, }); - res.sendStatus(OK); + + if (existingRequest) { + if (existingRequest.status === PermissionRequestStatus.PENDING) { + return res.sendStatus(CONFLICT); + } + existingRequest.status = PermissionRequestStatus.PENDING; + existingRequest.createdAt = new Date(); + await existingRequest.save(); + return res.sendStatus(OK); + } + + await PermissionRequest.create({ userId: decoded.token._id, type }); + return res.sendStatus(OK); } catch (error) { if (error.code === 11000) return res.sendStatus(CONFLICT); logger.error('Failed to create permission request:', error); @@ -37,13 +52,17 @@ router.get('/', async (req, res) => { const isOfficer = decoded.token.accessLevel >= membershipState.OFFICER; try { - const query = { deletedAt: null }; + const query = { status: { $ne: PermissionRequestStatus.DENIED } }; if (queryUserId) { query.userId = queryUserId; + // For member's own request, return it regardless of approval status } if (!isOfficer) { query.userId = decoded.token._id.toString(); + } else if (!queryUserId) { + // Officers viewing the list should only see pending requests + query.status = PermissionRequestStatus.PENDING; } // If there is a type, filter by it @@ -62,6 +81,36 @@ router.get('/', async (req, res) => { } }); +router.post('/approve', async (req, res) => { + const decoded = await decodeToken(req, membershipState.OFFICER); + if (decoded.status !== OK) return res.sendStatus(decoded.status); + + const { type, _id } = req.body; + if (!type || !Object.keys(PermissionRequestTypes).includes(type)) { + return res.status(BAD_REQUEST).send({ error: 'Invalid type' }); + } + + if (!_id) { + return res.status(BAD_REQUEST).send({ error: '_id is required' }); + } + + try { + const request = await PermissionRequest.findOne({ + _id, + type, + status: PermissionRequestStatus.PENDING, + }); + + if (!request) return res.sendStatus(NOT_FOUND); + request.status = PermissionRequestStatus.APPROVED; + await request.save(); + res.sendStatus(OK); + } catch (error) { + logger.error('Failed to approve permission request:', error); + res.sendStatus(SERVER_ERROR); + } +}); + router.post('/delete', async (req, res) => { const decoded = await decodeToken(req, membershipState.MEMBER); if (decoded.status !== OK) return res.sendStatus(decoded.status); @@ -86,13 +135,13 @@ router.post('/delete', async (req, res) => { const query = { _id: idToUse, type, - deletedAt: null, + status: { $ne: PermissionRequestStatus.DENIED }, }; const request = await PermissionRequest.findOne(query); if (!request) return res.sendStatus(NOT_FOUND); - request.deletedAt = new Date(); + request.status = PermissionRequestStatus.DENIED; await request.save(); res.sendStatus(OK); } catch (error) { diff --git a/api/util/constants.js b/api/util/constants.js index 7b7e6b68a..99844ed6d 100644 --- a/api/util/constants.js +++ b/api/util/constants.js @@ -33,10 +33,17 @@ const consoleColors = { // 60 seconds per minute * 60 minutes per hour * 12 hours const PASSWORD_RESET_EXPIRATION = 60 * 60 * 12; +const PERMISSION_REQUEST_STATUS = { + PENDING: 'PENDING', + APPROVED: 'APPROVED', + DENIED: 'DENIED', +}; + module.exports = { STATUS_CODES, MEMBERSHIP_STATE, MESSAGES_API, consoleColors, PASSWORD_RESET_EXPIRATION, + PERMISSION_REQUEST_STATUS, }; diff --git a/src/APIFunctions/PermissionRequest.js b/src/APIFunctions/PermissionRequest.js index 528226ad3..dc7ccee3a 100644 --- a/src/APIFunctions/PermissionRequest.js +++ b/src/APIFunctions/PermissionRequest.js @@ -1,10 +1,13 @@ import { ApiResponse } from './ApiResponses'; import { BASE_API_URL } from '../Enums'; -export async function getPermissionRequest(type, token) { +export async function getPermissionRequest(type, userId, token) { const status = new ApiResponse(); const url = new URL('/api/PermissionRequest/', BASE_API_URL); url.searchParams.append('type', type); + if (userId) { + url.searchParams.append('userId', userId); + } try { const res = await fetch(url.toString(), { @@ -13,10 +16,12 @@ export async function getPermissionRequest(type, token) { }, }); - status.error = !!res.ok; + status.error = !res.ok; if (res.ok) { const data = await res.json(); status.responseData = Array.isArray(data) && data.length > 0 ? data[0] : null; + } else { + status.responseData = null; } } catch (err) { status.responseData = err; @@ -40,11 +45,97 @@ export async function createPermissionRequest(type, token) { body: JSON.stringify({ type }), }); - status.error = !!res.ok; - if (res.ok || res.status === 409) { - // Backend sends 200 with no body on success, so fetch the created request - const existingRequest = await getPermissionRequest(type, token); - status.responseData = existingRequest.responseData; + if (res.ok) { + // API returns 200 with no body, so we just mark success + status.responseData = true; + } else if (res.status === 409) { + // CONFLICT - duplicate request + status.error = true; + status.responseData = 'Request already exists'; + } else { + status.error = true; + } + } catch (err) { + status.responseData = err; + status.error = true; + } + + return status; +} + +export async function getAllPermissionRequests(type, token) { + const status = new ApiResponse(); + const url = new URL('/api/PermissionRequest/', BASE_API_URL); + if (type) { + url.searchParams.append('type', type); + } + + try { + const res = await fetch(url.toString(), { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (res.ok) { + const data = await res.json(); + status.responseData = data; + } else { + status.error = true; + } + } catch (err) { + status.responseData = err; + status.error = true; + } + + return status; +} + +export async function approvePermissionRequest(type, id, token) { + const status = new ApiResponse(); + const url = new URL('/api/PermissionRequest/approve', BASE_API_URL); + + try { + const res = await fetch(url.toString(), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ type, _id: id }), + }); + + if (res.ok) { + status.responseData = true; + } else { + status.error = true; + } + } catch (err) { + status.responseData = err; + status.error = true; + } + + return status; +} + +export async function deletePermissionRequest(type, id, token) { + const status = new ApiResponse(); + const url = new URL('/api/PermissionRequest/delete', BASE_API_URL); + + try { + const res = await fetch(url.toString(), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ type, _id: id }), + }); + + if (res.ok) { + status.responseData = true; + } else { + status.error = true; } } catch (err) { status.responseData = err; diff --git a/src/Pages/LedSign/LedSign.js b/src/Pages/LedSign/LedSign.js index 272a77961..fc92fe805 100644 --- a/src/Pages/LedSign/LedSign.js +++ b/src/Pages/LedSign/LedSign.js @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; import { healthCheck, updateSignText } from '../../APIFunctions/LedSign'; -import { getPermissionRequest, createPermissionRequest } from '../../APIFunctions/PermissionRequest'; +import { getPermissionRequest, createPermissionRequest, getAllPermissionRequests, approvePermissionRequest, deletePermissionRequest } from '../../APIFunctions/PermissionRequest'; import { useSCE } from '../../Components/context/SceContext'; import { membershipState } from '../../Enums'; @@ -25,6 +25,10 @@ function LedSign() { const [permissionRequest, setPermissionRequest] = useState(null); const [checkingPermission, setCheckingPermission] = useState(false); const [requestingPermission, setRequestingPermission] = useState(false); + const isOfficer = user.accessLevel >= membershipState.OFFICER; + const [tab, setTab] = useState('sign'); + const [allPermissionRequests, setAllPermissionRequests] = useState([]); + const [loadingRequests, setLoadingRequests] = useState(false); const inputArray = [ { title: 'Sign Text:', @@ -198,9 +202,9 @@ function LedSign() { useEffect(() => { async function checkSignHealth() { setLoading(true); + setSignHealthy(true); const status = await healthCheck(user.firstName); if (status.responseData && !status.error) { - setSignHealthy(true); const { responseData } = status; if (Object.keys(responseData).length > 0) { setText(responseData.text); @@ -211,8 +215,7 @@ function LedSign() { setBorderColor(responseData.borderColor); setExistingExpirationFromSign(responseData.expiration); } - } else { - setSignHealthy(false); + } setLoading(false); } @@ -220,7 +223,7 @@ function LedSign() { async function checkPermission() { if (user.accessLevel < membershipState.OFFICER) { setCheckingPermission(true); - const result = await getPermissionRequest('LED_SIGN', user.token); + const result = await getPermissionRequest('LED_SIGN', user._id, user.token); if (!result.error && result.responseData) { setPermissionRequest(result.responseData); } @@ -231,7 +234,14 @@ function LedSign() { checkSignHealth(); checkPermission(); // eslint-disable-next-line - }, []) + }, []); + + useEffect(() => { + if (isOfficer && tab === 'requests') { + fetchAllPermissionRequests(); + } + // eslint-disable-next-line + }, [tab, isOfficer]); if (loading) { return ( @@ -252,8 +262,11 @@ function LedSign() { async function handleRequestAccess() { setRequestingPermission(true); const result = await createPermissionRequest('LED_SIGN', user.token); - if (!result.error) { - setPermissionRequest(result.responseData); + if (!result.error || result.responseData === 'Request already exists') { + const fetchResult = await getPermissionRequest('LED_SIGN', user._id, user.token); + if (!fetchResult.error && fetchResult.responseData) { + setPermissionRequest(fetchResult.responseData); + } } setRequestingPermission(false); } @@ -271,7 +284,7 @@ function LedSign() { ); } - if (permissionRequest) { + if (permissionRequest && permissionRequest.status === 'PENDING') { return (
@@ -308,6 +321,95 @@ function LedSign() { return (11 - scrollSpeed); } + function getSelectedClassName(currTab) { + return currTab === tab + ? 'p-2 bg-gray-100 dark:bg-gray-700 rounded-md dark:text-white text-gray-700' + : 'p-2 hover:bg-gray-400 rounded-md dark:text-white text-gray-700'; + } + + async function fetchAllPermissionRequests() { + if (!isOfficer) return; + setLoadingRequests(true); + const result = await getAllPermissionRequests('LED_SIGN', user.token); + if (!result.error && result.responseData) { + setAllPermissionRequests(result.responseData); + } + setLoadingRequests(false); + } + + async function handleApprove(requestId) { + const result = await approvePermissionRequest('LED_SIGN', requestId, user.token); + if (!result.error) await fetchAllPermissionRequests(); + } + + async function handleDeny(requestId) { + const result = await deletePermissionRequest('LED_SIGN', requestId, user.token); + if (!result.error) await fetchAllPermissionRequests(); + } + + function formatUserName(userData) { + if (!userData) return 'Unknown User'; + const name = `${userData.firstName || ''} ${userData.lastName || ''}`.trim(); + return name || 'Unknown User'; + } + + function MaybeRenderListOfSignRequests() { + if (!isOfficer) return null; + if (loadingRequests) { + return ( +
Loading requests...
+| User Name | +Request Date | +Actions | +|
|---|---|---|---|
| + No pending requests + | +|||
| + {formatUserName(request.userId)} + | +{request.userId?.email || 'N/A'} | +{getFormattedTime(request.createdAt)} | +
+
+
+
+
+ |
+