Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/main_endpoints/models/PermissionRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ const PermissionRequestSchema = new Schema(
ref: 'User',
required: true,
},
status: {
type: String,
enum: ['PENDING', 'APPROVED', 'DENIED', 'REVOKED'],
default: 'PENDING'
},
type: {
type: String,
enum: Object.values(PermissionRequestTypes),
Expand Down
30 changes: 27 additions & 3 deletions api/main_endpoints/routes/LedSign.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const router = express.Router();
const {
OK,
SERVER_ERROR,
UNAUTHORIZED
FORBIDDEN,
} = require('../../util/constants').STATUS_CODES;
const { decodeToken } = require('../util/token-functions.js');
const logger = require('../../util/logger');
Expand All @@ -14,6 +14,8 @@ const {
LED_SIGN = {}
} = require('../../config/config.json');
const { MEMBERSHIP_STATE } = require('../../util/constants.js');
const PermissionRequest = require('../models/PermissionRequest');


const runningInTest = process.env.NODE_ENV === 'test';

Expand All @@ -35,8 +37,30 @@ router.get('/healthCheck', async (req, res) => {
});

router.post('/updateSignText', async (req, res) => {
const decoded = await decodeToken(req, MEMBERSHIP_STATE.OFFICER);
if (decoded.status !== OK) {
let decoded = await decodeToken(req, MEMBERSHIP_STATE.OFFICER);
if (decoded.status === FORBIDDEN) {
const memberDecoded = await decodeToken(req, MEMBERSHIP_STATE.MEMBER);
if (memberDecoded.status !== OK) {
// return whatever decoded status we originally had
return res.sendStatus(decoded.status);
}

try {
const hasPermission = await PermissionRequest.findOne({
userId: memberDecoded.token._id,
type: 'LED_SIGN',
status: 'APPROVED',
deletedAt: null
});
if (!hasPermission) {
return res.sendStatus(decoded.status);
}
// "elevate" the status to OK for the rest of the function
decoded = memberDecoded;
} catch(e) {
logger.info('looking for a possible led sign permission didnt work', e);
}
} else if (decoded.status !== OK) {
logger.warn('/updateSignText was requested with an invalid token');
return res.sendStatus(decoded.status);
}
Expand Down
57 changes: 38 additions & 19 deletions api/main_endpoints/routes/PermissionRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ router.post('/create', async (req, res) => {
await PermissionRequest.create({
userId: decoded.token._id,
type,
status: 'PENDING',
});
res.sendStatus(OK);
} catch (error) {
Expand Down Expand Up @@ -66,37 +67,55 @@ router.post('/delete', async (req, res) => {
const decoded = await decodeToken(req, membershipState.MEMBER);
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: `${type} is an invalid type, try
${Object.keys(PermissionRequestTypes)}` });
}
const { _id } = req.body;
const isOfficer = decoded.token.accessLevel >= membershipState.OFFICER;

try {
let idToUse = _id;

if (!idToUse) {
idToUse = decoded.token._id;
}

if (decoded.token.accessLevel < membershipState.OFFICER) {
idToUse = decoded.token._id;
}

const query = {
_id: idToUse,
type,
_id,
deletedAt: null,
};

const request = await PermissionRequest.findOne(query);
if (!isOfficer) {
query.userId = decoded.token._id;
query.status = 'PENDING';
}

const request = await PermissionRequest.findOne(query);
if (!request) return res.sendStatus(NOT_FOUND);

// if the officer deletes a pending request, consider it denied.
// if a user deletes their pending request, consider they gave up asking
if (request.status === 'PENDING' && isOfficer) {
request.status = 'DENIED';
} else if (request.status === 'APPROVED') {
request.status = 'REVOKED';
}

request.deletedAt = new Date();
await request.save();
res.sendStatus(OK);
} catch (error) {
logger.error('Failed to delete permission request:', error);
logger.error('Failed to mark permission request as deleted:', error);
res.sendStatus(SERVER_ERROR);
}
});

router.post('/approve', async (req, res) => {
const decoded = await decodeToken(req, membershipState.OFFICER);
if (decoded.status !== OK) return res.sendStatus(decoded.status);

const { _id } = req.body;

try {
const request = await PermissionRequest.findOne({ _id, status: 'PENDING' });
if (!request) return res.status(NOT_FOUND).send({ error: 'Pending request not found' });

request.status = 'APPROVED';
await request.save();
res.sendStatus(OK);
} catch (error) {
logger.error('Failed to approve permission request:', error);
res.sendStatus(SERVER_ERROR);
}
});
Expand Down
40 changes: 39 additions & 1 deletion src/APIFunctions/PermissionRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export async function createPermissionRequest(type, token) {
body: JSON.stringify({ type }),
});

status.error = !!res.ok;
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 getPermissionRequests(type, token);
Expand All @@ -53,3 +53,41 @@ export async function createPermissionRequest(type, token) {

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 }),
});
status.error = !res.ok;
} catch (err) {
status.error = true;
}
return status;
}

export async function deletePermissionRequest(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({ _id: id }),
});
status.error = !res.ok;
} catch (err) {
status.error = true;
}
return status;
}
20 changes: 20 additions & 0 deletions src/Components/Navbar/AdminNavbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,26 @@ export default function UserNavBar(props) {
</svg>
)
},
{
title: 'Permission Requests',
route: '/permissions',
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="w-6 h-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"
/>
</svg>
)
},
{
title: 'Audit Logs',
route: '/audit-logs',
Expand Down
40 changes: 23 additions & 17 deletions src/Pages/LedSign/LedSign.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,9 @@ function LedSign() {
setRequestingPermission(true);
const result = await createPermissionRequest('LED_SIGN', user.token);
if (!result.error) {
setPermissionRequest(result.responseData);
setPermissionRequest({
status: 'PENDING',
});
}
setRequestingPermission(false);
}
Expand All @@ -276,7 +278,24 @@ function LedSign() {
);
}

if (permissionRequest) {
if (!permissionRequest) {
return (
<div className="w-2/3 lg:w-1/2 text-center py-4 space-y-2 fade-in">
<p className="text-gray-700 dark:text-gray-300">
You need permission to access the LED sign.
</p>
<button
className="btn bg-blue-500 hover:bg-blue-400 text-white"
onClick={handleRequestAccess}
disabled={requestingPermission}
>
{requestingPermission ? 'Requesting...' : 'Request Access'}
</button>
</div>
);
}

if (permissionRequest.status === 'PENDING') {
return (
<div className="w-2/3 lg:w-1/2 text-center py-4 space-y-2 fade-in">
<p className="text-gray-700 dark:text-gray-300">
Expand All @@ -289,20 +308,7 @@ function LedSign() {
);
}

return (
<div className="w-2/3 lg:w-1/2 text-center py-4 space-y-2 fade-in">
<p className="text-gray-700 dark:text-gray-300">
You need permission to access the LED sign.
</p>
<button
className="btn bg-blue-500 hover:bg-blue-400 text-white"
onClick={handleRequestAccess}
disabled={requestingPermission}
>
{requestingPermission ? 'Requesting...' : 'Request Access'}
</button>
</div>
);
return <h1>{JSON.stringify(permissionRequest)}</h1>;
}

function renderSignControls() {
Expand Down Expand Up @@ -367,7 +373,7 @@ function LedSign() {
return (
<div className="flex justify-center items-center mt-10 w-full">
<div className="space-y-12 gap-x-6 gap-y-8 w-full flex flex-col items-center">
{user.accessLevel >= membershipState.OFFICER
{user.accessLevel >= membershipState.OFFICER || permissionRequest?.status === 'APPROVED'
? renderSignControls()
: renderPermissionRequestUI()
}
Expand Down
Loading