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
29 changes: 29 additions & 0 deletions api/main_endpoints/models/PermissionRequest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const PermissionRequestTypes = require('../util/permissionRequestTypes');

const PermissionRequestSchema = new Schema(
{
userId: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true,
},
type: {
type: String,
enum: Object.values(PermissionRequestTypes),
required: true,
},
deletedAt: {
type: Date,
default: null,
},
},
{ 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 }});

module.exports = mongoose.model('PermissionRequest', PermissionRequestSchema);

109 changes: 109 additions & 0 deletions api/main_endpoints/routes/PermissionRequest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
const express = require('express');
const router = express.Router();
const PermissionRequest = require('../models/PermissionRequest');
const { OK, UNAUTHORIZED, FORBIDDEN, SERVER_ERROR, NOT_FOUND, BAD_REQUEST, CONFLICT } = require('../../util/constants').STATUS_CODES;
const membershipState = require('../../util/constants.js').MEMBERSHIP_STATE;
const { decodeToken } = require('../util/token-functions.js');
const logger = require('../../util/logger');
const PermissionRequestTypes = require('../util/permissionRequestTypes');

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

const { type } = req.body;
if (!type || !Object.keys(PermissionRequestTypes).includes(type)) {
return res.status(BAD_REQUEST).send({ error: 'Invalid type' });
}

try {
await PermissionRequest.create({
userId: decoded.token._id,
type,
});
res.sendStatus(OK);
} catch (error) {
if (error.code === 11000) return res.sendStatus(CONFLICT);
logger.error('Failed to create permission request:', error);
res.sendStatus(SERVER_ERROR);
}
});

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

const { userId: queryUserId, type } = req.query;
const isOfficer = decoded.token.accessLevel >= membershipState.OFFICER;

try {
const query = { deletedAt: null };

// If theres no userId, return all for officers and admins
if (!queryUserId) {
if (!isOfficer) {
return res.sendStatus(UNAUTHORIZED);
}
} else {
// If there is a userId, check their perms
if (!isOfficer && queryUserId !== decoded.token._id.toString()) {
return res.sendStatus(FORBIDDEN);
}
query.userId = queryUserId;
}

// If there is a type, filter by it
if (type && Object.keys(PermissionRequestTypes).includes(type)) {
query.type = type;
}

const requests = await PermissionRequest.find(query)
.populate('userId', 'firstName lastName email')
.sort({ createdAt: -1 });

res.status(OK).send(requests);
} catch (error) {
logger.error('Failed to get permission requests:', error);
res.sendStatus(SERVER_ERROR);
}
});

router.post('/delete', async (req, res) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

should admins be able to delete records, and whatever record they want?

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: 'Invalid type' });
}

try {
let request;
// Officers or admins can delete any request by id
if (decoded.token.accessLevel >= membershipState.OFFICER && _id) {
request = await PermissionRequest.findOne({
_id,
type,
deletedAt: null,
});
} else {
// Members can delete their own requests and officers can delete their own requests without id
request = await PermissionRequest.findOne({
userId: decoded.token._id,
type,
deletedAt: null,
});
}

if (!request) return res.sendStatus(NOT_FOUND);
request.deletedAt = new Date();
await request.save();
res.sendStatus(OK);
} catch (error) {
logger.error('Failed to delete permission request:', error);
res.sendStatus(SERVER_ERROR);
}
});

module.exports = router;

6 changes: 6 additions & 0 deletions api/main_endpoints/util/permissionRequestTypes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const PermissionRequestTypes = {
LED_SIGN: 'LED_SIGN',
};

module.exports = PermissionRequestTypes;

97 changes: 97 additions & 0 deletions test/api/PermissionRequest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
process.env.NODE_ENV = 'test';

const PermissionRequest = require('../../api/main_endpoints/models/PermissionRequest');
const chai = require('chai');
const chaiHttp = require('chai-http');
const constants = require('../../api/util/constants');
const { OK, BAD_REQUEST, UNAUTHORIZED, NOT_FOUND } = constants.STATUS_CODES;
const SceApiTester = require('../../test/util/tools/SceApiTester');
const {
initializeTokenMock,
setTokenStatus,
resetTokenMock,
restoreTokenMock,
} = require('../util/mocks/TokenValidFunctions');
const mongoose = require('mongoose');
const PermissionRequestTypes = require('../../api/main_endpoints/util/permissionRequestTypes');

let app = null;
let test = null;
const expect = chai.expect;
const tools = require('../util/tools/tools.js');
chai.should();
chai.use(chaiHttp);
const token = '';

describe('PermissionRequest', () => {
before(done => {
initializeTokenMock();
app = tools.initializeServer(__dirname + '/../../api/main_endpoints/routes/PermissionRequest.js');
test = new SceApiTester(app);
tools.emptySchema(PermissionRequest);
done();
});

after(done => {
restoreTokenMock();
tools.terminateServer(done);
});

beforeEach(() => {
setTokenStatus(false);
});

afterEach(() => {
resetTokenMock();
});

describe('/POST create', () => {
it('Should return 401 when token is not sent', async () => {
const res = await test.sendPostRequest('/api/PermissionRequest/create', { type: PermissionRequestTypes.LED_SIGN });
expect(res).to.have.status(UNAUTHORIZED);
});

it('Should create permission request successfully', async () => {
const userId = new mongoose.Types.ObjectId();
setTokenStatus(true, { _id: userId, email: 'test@test.com', accessLevel: 'MEMBER' });
const res = await test.sendPostRequestWithToken(token, '/api/PermissionRequest/create', { type: PermissionRequestTypes.LED_SIGN });
expect(res).to.have.status(OK);
const request = await PermissionRequest.findOne({ userId, type: PermissionRequestTypes.LED_SIGN });
expect(request).to.exist;
expect(request.type).to.equal(PermissionRequestTypes.LED_SIGN);
});
});

describe('/GET get', () => {
it('Should return empty array when request does not exist', async () => {
const userId = new mongoose.Types.ObjectId();
setTokenStatus(true, { _id: userId, email: 'test@test.com', accessLevel: 'MEMBER' });
const res = await test.sendGetRequest('/api/PermissionRequest/get?userId=' + userId + '&type=' + PermissionRequestTypes.LED_SIGN);
expect(res).to.have.status(OK);
expect(res.body).to.be.an('array').that.is.empty;
});

it('Should return permission request when it exists', async () => {
const userId = new mongoose.Types.ObjectId();
setTokenStatus(true, { _id: userId, email: 'test@test.com', accessLevel: 'MEMBER' });
await new PermissionRequest({ userId, type: PermissionRequestTypes.LED_SIGN }).save();
const res = await test.sendGetRequest('/api/PermissionRequest/get?userId=' + userId + '&type=' + PermissionRequestTypes.LED_SIGN);
expect(res).to.have.status(OK);
expect(res.body).to.be.an('array').with.length(1);
expect(res.body[0].type).to.equal(PermissionRequestTypes.LED_SIGN);
});
});

describe('/POST delete', () => {
it('Should delete permission request successfully', async () => {
const userId = new mongoose.Types.ObjectId();
setTokenStatus(true, { _id: userId, email: 'test@test.com', accessLevel: 'MEMBER' });
const request = await new PermissionRequest({ userId, type: PermissionRequestTypes.LED_SIGN }).save();
const res = await test.sendPostRequestWithToken(token, '/api/PermissionRequest/delete', { type: PermissionRequestTypes.LED_SIGN });
expect(res).to.have.status(OK);
const deleted = await PermissionRequest.findById(request._id);
expect(deleted.deletedAt).to.not.be.null;
});
});
});