diff --git a/api/materialized_views/search/redactedRecordSubset.js b/api/materialized_views/search/redactedRecordSubset.js index 04f9ef849..e9090886d 100644 --- a/api/materialized_views/search/redactedRecordSubset.js +++ b/api/materialized_views/search/redactedRecordSubset.js @@ -47,7 +47,7 @@ async function update(defaultLog) { skipRedact: { $cond: { if: { - $in: [{ $arrayElemAt: ['$fullRecord._schemaName', 0] }, ['MineBCMI', 'CollectionBCMI']] + $in: [{ $arrayElemAt: ['$fullRecord._schemaName', 0] }, ['MineBCMI', 'CollectionBCMI', 'MapLayerInfo']] }, then: true, else: false @@ -164,7 +164,7 @@ async function update(defaultLog) { $addFields: { 'issuedTo.read': { $cond: { - if: notAuthorizedCondition, + if: { $and: [{ $eq: ['$skipRedact', false] }, notAuthorizedCondition] }, then: { $filter: { input: '$issuedTo.read', @@ -184,7 +184,7 @@ async function update(defaultLog) { $addFields: { documents: { $cond: { - if: notAuthorizedCondition, + if: { $and: [{ $eq: ['$skipRedact', false] }, notAuthorizedCondition] }, then: [], else: '$documents' } diff --git a/api/src/controllers/map-info-controller.js b/api/src/controllers/map-info-controller.js new file mode 100644 index 000000000..6c57cb673 --- /dev/null +++ b/api/src/controllers/map-info-controller.js @@ -0,0 +1,220 @@ +// const mongoose = require('mongoose'); +// const ObjectID = require('mongodb').ObjectID; + +const defaultLog = require('../utils/logger')('mapLayerInfo'); +const queryActions = require('../utils/query-actions'); +const { ApplicationRoles } = require('../utils/constants/misc'); +const Post = require('../controllers/post/post'); +const Get = require('../controllers/get/get'); +const Delete = require('../controllers/delete/delete'); +const PutUtils = require('../utils/put-utils'); +const { mapLayerInfo: MapLayerInfo } = require('../models/index'); + +exports.protectedOptions = function(args, res, next) { + res.status(200).send(); +}; + +exports.publicGet = async function(args, res, next) { + let mapInfoId = null; + let errorMsg = null; + if (args.swagger.params.mapInfoId && args.swagger.params.mapInfoId.value) { + mapInfoId = args.swagger.params.mapInfoId.value; + } else { + errorMsg = `publicGet - you must provide an id to get`; + defaultLog.info(errorMsg); + return queryActions.sendResponse(res, 400, errorMsg); + } + + let obj = null; + try { + obj = await Get.findById(mapInfoId); + } catch (error) { + errorMsg = `publicGet - error getting map info: ${mapInfoId}`; + defaultLog.info(errorMsg); + defaultLog.debug(error); + return queryActions.sendResponse(res, 400, errorMsg); + } + + if (!obj) { + errorMsg = `protectedGet - map info not found: ${mapInfoId}`; + defaultLog.info(errorMsg); + return queryActions.sendResponse(res, 404, errorMsg); + } + + return queryActions.sendResponse(res, 200, obj); +}; + +exports.protectedGet = async function(args, res, next) { + let mapInfoId = null; + let errorMsg = null; + if (args.swagger.params.mapInfoId && args.swagger.params.mapInfoId.value) { + mapInfoId = args.swagger.params.mapInfoId.value; + } else { + errorMsg = `protectedGet - you must provide an id to get`; + defaultLog.info(errorMsg); + return queryActions.sendResponse(res, 400, errorMsg); + } + + let obj = null; + try { + obj = await Get.findById(mapInfoId); + } catch (error) { + errorMsg = `protectedGet - error getting map info: ${mapInfoId}`; + defaultLog.info(errorMsg); + defaultLog.debug(error); + return queryActions.sendResponse(res, 400, errorMsg); + } + + if (!obj) { + errorMsg = `protectedGet - map info not found: ${mapInfoId}`; + defaultLog.info(errorMsg); + return queryActions.sendResponse(res, 404, errorMsg); + } + + return queryActions.sendResponse(res, 200, obj); +}; + +async function getMapLayerInfoSegment(segment) { + return await MapLayerInfo.findOne({ _schemaName: 'MapLayerInfo', segment }); +} + +exports.protectedPost = async function(args, res, next) { + let incomingObj = {}; + let errorMsg = null; + // Only accpet applicaiton: 'LNG' for now + if (args.swagger.params.mapInfo && args.swagger.params.mapInfo.value) { + incomingObj = args.swagger.params.mapInfo.value; + } else { + errorMsg = `protectedPost - error: invalid post body`; + defaultLog.info(errorMsg); + return queryActions.sendResponse(res, 400, errorMsg); + } + + if (!(incomingObj.application && incomingObj.application === 'LNG')) { + errorMsg = `protectedPost - error: application type not accepted`; + defaultLog.info(errorMsg); + return queryActions.sendResponse(res, 400, errorMsg); + } + + if (!(incomingObj.data && incomingObj.data.segment)) { + errorMsg = `protectedPost - error: missing segment`; + defaultLog.info(errorMsg); + return queryActions.sendResponse(res, 400, errorMsg); + } + + const data = incomingObj.data; + + // Check for existing segment info and prevent creating duplicates + const existingMapInfo = await getMapLayerInfoSegment(data.segment); + if (existingMapInfo) { + errorMsg = `protectedPost - error: segment info already exists`; + defaultLog.info(errorMsg); + return queryActions.sendResponse(res, 400, errorMsg); + } + + let map = new MapLayerInfo(); + + data.segment && (map.segment = data.segment); + data.description && (map.description = data.description); + data.location && (map.location = data.location); + data.length && (map.length = data.length); + + map.read = [ApplicationRoles.ADMIN, ApplicationRoles.ADMIN_LNG, ApplicationRoles.PUBLIC]; + map.write = [ApplicationRoles.ADMIN, ApplicationRoles.ADMIN_LNG]; + + map.dateAdded = Date.now(); + map.dateUpdated = Date.now(); + map.updatedBy = args.swagger.params.auth_payload.displayName; + + let obj = null; + try { + obj = await Post.insert(map); + } catch (error) { + errorMsg = `protectedPost - error inserting map layer info: ${map}`; + defaultLog.info(errorMsg); + defaultLog.debug(error); + return queryActions.sendResponse(res, 400, errorMsg); + } + + return queryActions.sendResponse(res, 200, obj.ops[0]); +}; + +exports.protectedPut = async function(args, res, next) { + let incomingObj = {}; + let errorMsg = null; + + if (args.swagger.params.mapInfo && args.swagger.params.mapInfo.value) { + incomingObj = args.swagger.params.mapInfo.value; + } else { + errorMsg = `protectedPut - you must provide mapInfo data to put`; + defaultLog.info(errorMsg); + return queryActions.sendResponse(res, 400, errorMsg); + } + + let mapInfoId = null; + if (args.swagger.params.mapInfoId && args.swagger.params.mapInfoId.value) { + mapInfoId = args.swagger.params.mapInfoId.value; + } else { + errorMsg = `protectedPut - you must provide mapInfoId to put`; + defaultLog.info(errorMsg); + return queryActions.sendResponse(res, 400, errorMsg); + } + + // Prevent changing the segment value + delete incomingObj.segment; + delete incomingObj._id; + delete incomingObj.read; + delete incomingObj.write; + + const sanitizedObj = PutUtils.validateObjectAgainstModel(MapLayerInfo, incomingObj); + + if (!sanitizedObj || sanitizedObj === {}) { + // skip, as there are no changes to master record + return; + } + + sanitizedObj.dateUpdated = new Date(); + sanitizedObj.updatedBy = args.swagger.params.auth_payload.displayName; + const dotNotatedObj = PutUtils.getDotNotation(sanitizedObj); + const updateObj = { $set: dotNotatedObj }; + + let obj = null; + try { + obj = await MapLayerInfo.findOneAndUpdate( + { + _id: mapInfoId, + write: { $in: args.swagger.params.auth_payload.realm_access.roles } + }, + updateObj, + { new: true } + ); + } catch (error) { + errorMsg = `protectedPut - error updating map info: ${updateObj}`; + defaultLog.info(errorMsg); + defaultLog.debug(error); + return queryActions.sendResponse(res, 400, errorMsg); + } + + return queryActions.sendResponse(res, 200, obj); +}; + +exports.protectedDelete = async function(args, res, next) { + let mapInfoId = null; + if (args.swagger.params.mapInfoId && args.swagger.params.mapInfoId.value) { + mapInfoId = args.swagger.params.mapInfoId.value; + } else { + defaultLog.info(`protectedDelete - you must provide an id to delete`); + return queryActions.sendResponse(res, 400, {}); + } + + let obj = null; + try { + obj = await Delete.deleteById(mapInfoId); + } catch (error) { + defaultLog.info(`protectedDelete - error deleting mapInfo: ${mapInfoId}`); + defaultLog.debug(error); + return queryActions.sendResponse(res, 400, {}); + } + + return queryActions.sendResponse(res, 200, obj); +}; diff --git a/api/src/models/index.js b/api/src/models/index.js index d15a62535..357a93c16 100644 --- a/api/src/models/index.js +++ b/api/src/models/index.js @@ -6,6 +6,7 @@ exports.task = require('./task'); exports.document = require('./document'); exports.epicProject = require('./epicProject'); exports.communicationPackage = require('./communicationPackage'); +exports.mapLayerInfo = require('./mapLayerInfo'); // master require('./master'); diff --git a/api/src/models/mapLayerInfo.js b/api/src/models/mapLayerInfo.js new file mode 100644 index 000000000..abf3f9034 --- /dev/null +++ b/api/src/models/mapLayerInfo.js @@ -0,0 +1,26 @@ +module.exports = require('../utils/model-schema-generator')( + 'MapLayerInfo', + { + _schemaName: { type: String, default: 'MapLayerInfo', index: true }, + + segment: { + type: String, + required: true, + enum: ['Section 1', 'Section 2', 'Section 3', 'Section 4', 'Section 5', 'Section 6', 'Section 7', 'Section 8'] + }, + + location: { type: String, default: null }, + length: { type: String, default: null }, + description: { type: String, default: null }, + + dateAdded: { type: Date, default: Date.now() }, + + updatedBy: { type: String, default: null }, + dateUpdated: { type: Date, default: null }, + + // Permissions + write: [{ type: String, trim: true, default: 'sysadmin' }], + read: [{ type: String, trim: true, default: 'sysadmin' }] + }, + 'nrpti' +); diff --git a/api/src/swagger/swagger.yaml b/api/src/swagger/swagger.yaml index b5c8ab70f..cd5cdba35 100644 --- a/api/src/swagger/swagger.yaml +++ b/api/src/swagger/swagger.yaml @@ -100,6 +100,10 @@ definitions: CollectionRequestObject: type: object + ### Map Info Type Definitions + MapInfoRequestObject: + type: object + ### News Type Definitions NewsRequestObject: type: object @@ -1612,3 +1616,166 @@ paths: description: 'Access Denied' schema: $ref: '#/definitions/Error' + ### + ### map layer info Routes + ### + /map-info: + x-swagger-router-controller: map-info-controller + options: + tags: + - map-info + summary: 'Pre-flight request' + operationId: protectedOptions + description: 'Options on authenticated map-info route' + responses: + '200': + description: 'Success' + '403': + description: 'Access Denied' + schema: + $ref: '#/definitions/Error' + post: + tags: + - map-info + summary: 'Post a map info' + operationId: protectedPost + description: "Creates a new map info" + security: + - Bearer: [] + x-security-scopes: + - sysadmin + - admin:lng + parameters: + - name: mapInfo + in: body + description: 'Map info to create' + required: true + schema: + $ref: '#/definitions/MapInfoRequestObject' + responses: + '200': + description: 'Success' + '403': + description: 'Access Denied' + schema: + $ref: '#/definitions/Error' + /map-info/{mapInfoId}: + x-swagger-router-controller: map-info-controller + options: + tags: + - map-info + summary: 'Pre-flight request' + operationId: protectedOptions + description: 'Options on authenticated map info route' + parameters: + - name: mapInfoId + in: path + description: '_id of map info' + required: true + type: string + responses: + '200': + description: 'Success' + '403': + description: 'Access Denied' + schema: + $ref: '#/definitions/Error' + get: + tags: + - map-info + summary: 'Get map info' + operationId: protectedGet + description: "Get a map info via _id." + security: + - Bearer: [] + x-security-scopes: + - sysadmin + - admin:lng + parameters: + - name: mapInfoId + in: path + description: '_id of map info to get' + required: true + type: string + responses: + '200': + description: 'Success' + '403': + description: 'Access Denied' + schema: + $ref: '#/definitions/Error' + put: + tags: + - map-info + summary: 'Put a map info' + operationId: protectedPut + description: "Updates a map info via _id." + security: + - Bearer: [] + x-security-scopes: + - sysadmin + - admin:lng + parameters: + - name: mapInfoId + in: path + description: '_id of map info to update' + required: true + type: string + - name: mapInfo + in: body + description: 'map info to update' + required: true + schema: + $ref: '#/definitions/MapInfoRequestObject' + responses: + '200': + description: 'Success' + '403': + description: 'Access Denied' + schema: + $ref: '#/definitions/Error' + delete: + tags: + - map-info + summary: 'Delete a map info' + operationId: protectedDelete + description: "Deletes a map info via _id." + security: + - Bearer: [] + x-security-scopes: + - sysadmin + - admin:lng + parameters: + - name: mapInfoId + in: path + description: '_id of map info to delete' + required: true + type: string + responses: + '200': + description: 'Success' + '403': + description: 'Access Denied' + schema: + $ref: '#/definitions/Error' + /public/map-info/{mapInfoId}: + x-swagger-router-controller: map-info-controller + get: + tags: + - map-info + summary: 'Get map info' + operationId: publicGet + description: "Get a map info via _id." + parameters: + - name: mapInfoId + in: path + description: '_id of map info to get' + required: true + type: string + responses: + '200': + description: 'Success' + '403': + description: 'Access Denied' + schema: + $ref: '#/definitions/Error' \ No newline at end of file diff --git a/api/test/tests/controllers/map-info-controller.test.js b/api/test/tests/controllers/map-info-controller.test.js new file mode 100644 index 000000000..896b31250 --- /dev/null +++ b/api/test/tests/controllers/map-info-controller.test.js @@ -0,0 +1,221 @@ +const request = require('supertest'); +const qs = require('qs'); +const ObjectId = require('mongodb').ObjectId; + +const test_util = require('../../test-utils'); +const mapInfo = require('../../../src/controllers/map-info-controller'); +const { ApplicationRoles } = require('../../../src/utils/constants/misc'); + +// mock next function +function next() { + return; +} + +const app = test_util.app; +const endpoint = '/map-info'; + +describe('Map-Info Controller Testing', () => { + const testUser = 'testUser'; + const testObjectId = new ObjectId(); + const testObj = { + _id: testObjectId, + _schemaName: 'MapLayerInfo', + location: 'Nile River', + length: '48.0 km', + description: 'paragraph of formatted text', + dateAdded: '2021-02-24T23:18:14.521Z', + updatedBy: 'test user', + dateUpdated: '2021-02-24T23:18:14.521Z', + write: ['sysadmin', 'admin:lng'], + read: ['sysadmin', 'admin:lng', 'public'], + segment: 'Section 8' + }; + + beforeAll(async () => { + const MongoClient = require('mongodb').MongoClient; + + const client = await MongoClient.connect(process.env.MONGO_URI); + const db = client.db(process.env.MONGODB_DATABASE || 'nrpti-dev'); + const nrptiCollection = db.collection('nrpti'); + nrptiCollection.insertOne(testObj); + }); + + test('Protectd post returns 400 with invalid post body', async done => { + const roles = ['sysadmin']; + + app.post(endpoint, (req, res) => { + const params = test_util.buildParams(req.body); + const paramsWithValues = test_util.createSwaggerParams(params, roles, testUser); + return mapInfo.protectedPost(paramsWithValues, res, next); + }); + + request(app) + .post(endpoint) + .send({}) + .expect(400) + .end((err, res) => { + if (err) { + console.log(err); + return done(err); + } + + expect(res.body).toMatch('protectedPost - error: invalid post body'); + return done(); + }); + }); + + test('Protectd post returns 200 with invalid post body', async done => { + const roles = ['sysadmin']; + const postObj = { + application: 'LNG', + data: { + location: 'Nile River', + length: '48.0 km', + description: 'paragraph of formatted text', + segment: 'Section 1' + } + }; + + app.post(endpoint, (req, res) => { + const params = test_util.buildParams({ mapInfo: req.body }); + const paramsWithValues = test_util.createSwaggerParams(params, roles, testUser); + return mapInfo.protectedPost(paramsWithValues, res, next); + }); + + request(app) + .post(endpoint) + .send(postObj) + .expect(200) + .end((err, res) => { + if (err) { + console.log(err); + return done(err); + } + + expect(res.body._schemaName).toMatch('MapLayerInfo'); + expect(res.body.location).toMatch(postObj.data.location); + expect(res.body.length).toMatch(postObj.data.length); + expect(res.body.description).toMatch(postObj.data.description); + expect(res.body.segment).toMatch(postObj.data.segment); + expect(res.body.read).toContain(ApplicationRoles.ADMIN); + expect(res.body.read).toContain(ApplicationRoles.ADMIN_LNG); + expect(res.body.read).toContain(ApplicationRoles.PUBLIC); + return done(); + }); + }); + + test('Protectd get returns 404 with invalid id', async done => { + const roles = ['sysadmin']; + const invalidId = new ObjectId(); + app.get(endpoint, (req, res) => { + const params = test_util.buildParams(req.query); + const paramsWithValues = test_util.createSwaggerParams(params, roles, testUser); + return mapInfo.protectedGet(paramsWithValues, res, next); + }); + + request(app) + .get(endpoint) + .query(qs.stringify({ mapInfoId: invalidId.toString() })) + .expect(404) + .end((err, res) => { + if (err) { + console.log(err); + return done(err); + } + expect(res.body).toMatch('protectedGet - map info not found: '); + return done(); + }); + }); + + test('Protectd get returns 200 with valid id', async done => { + const roles = ['sysadmin']; + app.get(endpoint, (req, res) => { + const params = test_util.buildParams(req.query); + const paramsWithValues = test_util.createSwaggerParams(params, roles, testUser); + return mapInfo.protectedGet(paramsWithValues, res, next); + }); + + request(app) + .get(endpoint) + .query(qs.stringify({ mapInfoId: testObjectId.toString() })) + .expect(200) + .end((err, res) => { + if (err) { + console.log(err); + return done(err); + } + + expect(res.body._schemaName).toMatch('MapLayerInfo'); + expect(res.body.location).toMatch(testObj.location); + expect(res.body.length).toMatch(testObj.length); + expect(res.body.description).toMatch(testObj.description); + expect(res.body.segment).toMatch(testObj.segment); + expect(res.body.read).toContain(ApplicationRoles.ADMIN); + expect(res.body.read).toContain(ApplicationRoles.ADMIN_LNG); + expect(res.body.read).toContain(ApplicationRoles.PUBLIC); + return done(); + }); + }); + + test('Protectd put returns 200 and updates record values', async done => { + const roles = ['sysadmin']; + const updateObj = { + description: 'new description', + location: 'new location', + segment: 'new segment', + length: 'new length' + }; + + app.put(endpoint, (req, res) => { + let params = test_util.buildParams(req.query); + params = { ...params, ...test_util.buildParams({ mapInfo: req.body }) }; + const paramsWithValues = test_util.createSwaggerParams(params, roles, testUser); + return mapInfo.protectedPut(paramsWithValues, res, next); + }); + + request(app) + .put(endpoint) + .query(qs.stringify({ mapInfoId: testObjectId.toString() })) + .send(updateObj) + .expect(200) + .end((err, res) => { + if (err) { + console.log(err); + return done(err); + } + + expect(res.body.description).toMatch(updateObj.description); + expect(res.body.location).toMatch(updateObj.location); + expect(res.body.length).toMatch(updateObj.length); + // Segment value shouldn't change + expect(res.body.segment).toMatch(testObj.segment); + + return done(); + }); + }); + + test('Protectd delete returns 200', async done => { + const roles = ['sysadmin']; + + app.delete(endpoint, (req, res) => { + const params = test_util.buildParams(req.query); + const paramsWithValues = test_util.createSwaggerParams(params, roles, testUser); + return mapInfo.protectedDelete(paramsWithValues, res, next); + }); + + request(app) + .delete(endpoint) + .query(qs.stringify({ mapInfoId: testObjectId.toString() })) + .expect(200) + .end((err, res) => { + if (err) { + console.log(err); + return done(err); + } + + expect(res.body).toEqual({ n: 1, ok: 1 }); + + return done(); + }); + }); +});