Skip to content
Open
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
13 changes: 9 additions & 4 deletions api/main_endpoints/models/PermissionRequest.js
Original file line number Diff line number Diff line change
@@ -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(
{
Expand All @@ -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);

61 changes: 55 additions & 6 deletions api/main_endpoints/routes/PermissionRequest.js
Original file line number Diff line number Diff line change
@@ -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');
Expand All @@ -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);
}
Comment on lines +28 to +36
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think if it exists we just return conflict no matter what, what do you think

if its denied, we can set the status to pending


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);
Expand All @@ -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
Expand All @@ -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' });
}
Comment on lines +89 to +91
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same thing like we did before can we do

`type ${type} is invalid, valid types are ${Object.keys(PermissionRequestTypes)}`


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);
Expand All @@ -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) {
Expand Down
7 changes: 7 additions & 0 deletions api/util/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
105 changes: 98 additions & 7 deletions src/APIFunctions/PermissionRequest.js
Original file line number Diff line number Diff line change
@@ -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(), {
Expand All @@ -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;
Expand All @@ -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;
}
Comment on lines +48 to +57
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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;
}
status.error = !!res.ok;
if (res.status === 409) {
// CONFLICT - duplicate request
status.responseData = 'Request already exists';
}

} 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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets not even set responseData

for these create/delete requests we can just rely on if error is true/false for checking if it worked

} else {
status.error = true;
}
} catch (err) {
status.responseData = err;
Expand Down
Loading