From b072e79452296a082b648d4f205046ff3923a9d7 Mon Sep 17 00:00:00 2001 From: Daniel Truong Date: Tue, 13 Oct 2026 17:59:36 +0000 Subject: [PATCH 1/2] chore(api): remove write routes with no caller These routes have no caller in eagle-admin or eagle-public and had no prod traffic in 30 days: - topic and vc POST, PUT, DELETE - POST /task and the api/tasks controller dir - POST /jobs and DELETE /jobs/{jobId} - POST /materialized-views/refresh (the CronJob runs worker/matview-worker.js directly) - inspection POST x3 - staff CAC routes x5 - DELETE /recentActivity (bulk form, always answered 400) - GET /audit (no controller, so it was never routed) GET routes for topic, vc, jobs and inspection items stay, as do the public CAC sign-up and remove routes. POST /login/token stays because the eagle-admin login screen calls it. BREAKING CHANGE: the routes listed above now answer 404. --- api/controllers/cac.js | 140 --- api/controllers/inspection.js | 292 ----- api/controllers/job.js | 110 +- api/controllers/materialized-view.js | 36 +- api/controllers/recentActivity.js | 15 +- api/controllers/topic.js | 66 -- api/controllers/vc.js | 56 - api/materialized_views/runLock.js | 16 +- api/swagger/swagger.yaml | 1006 +----------------- api/tasks/import-task.js | 51 - app.js | 6 +- test/controllers/cac.test.js | 153 --- test/controllers/demi-push-callsites.test.js | 67 +- test/controllers/recentActivity.test.js | 11 - test/helpers/anonymousRouting.test.js | 9 +- test/smoke/protected-projects.test.js | 11 - test/smoke/write-auth-gates.test.js | 59 - test/support/demiPushHarness.js | 15 +- 18 files changed, 69 insertions(+), 2050 deletions(-) delete mode 100644 api/tasks/import-task.js diff --git a/api/controllers/cac.js b/api/controllers/cac.js index c13ea133..6cf65590 100644 --- a/api/controllers/cac.js +++ b/api/controllers/cac.js @@ -8,60 +8,6 @@ exports.protectedOptions = function (args, res) { res.status(200).send(); }; -exports.protectedPublishCAC = async function (args, res) { - var projId = args.swagger.params.projId.value; - defaultLog.info('protectedPublishCAC for project:', projId); - if (!mongoose.Types.ObjectId.isValid(projId)) { - return Actions.sendResponse(res, 400, {}); - } - var Project = mongoose.model('Project'); - try { - var project = await Project.findOne({ _id: projId }); - if (project) { - var published = await Project.updateOne( - { _id: new mongoose.Types.ObjectId(projId) }, - { $set: { 'projectCACPublished': true } } - ); - Utils.recordAction('Publish', 'CAC', args.swagger.params.auth_payload.preferred_username, projId); - defaultLog.info('Published CAC for project:', projId); - return Actions.sendResponse(res, 200, published); - } else { - defaultLog.info('Project not found:', projId); - return Actions.sendResponse(res, 404, {}); - } - } catch (e) { - defaultLog.error(`Error publishing CAC for project: ${projId}: ${e.message}`); - return Actions.sendResponse(res, 400, e); - } -}; - -exports.protectedUnPublishCAC = async function (args, res) { - var projId = args.swagger.params.projId.value; - defaultLog.info('protectedUnPublishCAC for project:', projId); - if (!mongoose.Types.ObjectId.isValid(projId)) { - return Actions.sendResponse(res, 400, {}); - } - var Project = mongoose.model('Project'); - try { - var project = await Project.findOne({ _id: projId }); - if (project) { - var updated = await Project.updateOne( - { _id: new mongoose.Types.ObjectId(projId) }, - { $set: { 'projectCACPublished': false } } - ); - Utils.recordAction('Unpublish', 'CAC', args.swagger.params.auth_payload.preferred_username, projId); - defaultLog.info('Unpublished CAC for project:', projId); - return Actions.sendResponse(res, 200, updated); - } else { - defaultLog.info('Project not found:', projId); - return Actions.sendResponse(res, 404, {}); - } - } catch (e) { - defaultLog.error(`Error unpublishing CAC for project: ${projId}: ${e.message}`); - return Actions.sendResponse(res, 400, e); - } -}; - exports.publicCACSignUp = async function (args, res) { const CACUser = mongoose.model('CACUser'); const Project = mongoose.model('Project'); @@ -150,89 +96,3 @@ exports.publicCACRemoveMember = async function (args, res) { return Actions.sendResponse(res, 400, e); } }; - -exports.protectedCACRemoveMember = async function (args, res) { - const projId = args.swagger.params.projId.value; - defaultLog.info('protectedCACRemoveMember for project:', projId); - if (!mongoose.Types.ObjectId.isValid(projId)) { - return Actions.sendResponse(res, 400, {}); - } - const member = args.swagger.params.member.value; - - const Project = mongoose.model('Project'); - const CACUser = mongoose.model('CACUser'); - try { - var projectData = await Project.updateOne( - { _id: new mongoose.Types.ObjectId(projId) }, - { $pull: { cacMembers: { $in: [new mongoose.Types.ObjectId(member._id)] } } } - ); - await CACUser.deleteOne({ _id: new mongoose.Types.ObjectId(member._id) }); - - Utils.recordAction('Delete', 'CACMemberFromProject', args.swagger.params.auth_payload.preferred_username, member._id); - defaultLog.info('Removed CAC member:', member._id, 'from project:', projId); - return Actions.sendResponse(res, 200, projectData); - } catch (e) { - defaultLog.error(`Error removing CAC member: ${member._id} from project: ${projId}: ${e.message}`); - return Actions.sendResponse(res, 400, e); - } -}; - -exports.protectedCreateCAC = async function (args, res) { - const projId = args.swagger.params.projId.value; - defaultLog.info('protectedCreateCAC for project:', projId); - if (!mongoose.Types.ObjectId.isValid(projId)) { - return Actions.sendResponse(res, 400, {}); - } - const cacData = args.swagger.params.data.value; - const Project = mongoose.model('Project'); - - try { - let data = await Project.updateOne( - { _id: new mongoose.Types.ObjectId(projId) }, - { projectCAC: true, cacEmail: cacData.cacEmail, projectCACPublished: false } - ); - if (data.modifiedCount === 0) { - defaultLog.info('No project found to update for CAC creation:', projId); - return Actions.sendResponse(res, 400, {}); - } - Utils.recordAction('Post', 'Add Project CAC', args.swagger.params.auth_payload.preferred_username, projId); - defaultLog.info('Created CAC for project:', projId); - return Actions.sendResponse(res, 201, data); - } catch (e) { - defaultLog.error(`Error creating CAC for project: ${projId}: ${e.message}`); - return Actions.sendResponse(res, 400, e); - } -}; - -exports.protectedCACDelete = async function (args, res) { - const projId = args.swagger.params.projId.value; - defaultLog.info('protectedCACDelete for project:', projId); - if (!mongoose.Types.ObjectId.isValid(projId)) { - return Actions.sendResponse(res, 400, {}); - } - const Project = mongoose.model('Project'); - const CACUser = mongoose.model('CACUser'); - try { - const proj = await Project.findOne({ _id: new mongoose.Types.ObjectId(projId) }); - if (!proj) { - defaultLog.info('Project not found for CAC delete:', projId); - return Actions.sendResponse(res, 404, {}); - } - await CACUser.deleteMany({ _id: { $in: proj.cacMembers } }); - - const data = await Project.updateOne( - { _id: new mongoose.Types.ObjectId(projId) }, - { projectCAC: false, cacMembers: [] } - ); - if (data.modifiedCount === 0) { - defaultLog.info('No project found to update for CAC delete:', projId); - return Actions.sendResponse(res, 400, {}); - } - Utils.recordAction('Post', 'Remove Project CAC', args.swagger.params.auth_payload.preferred_username, projId); - defaultLog.info('Deleted CAC for project:', projId); - return Actions.sendResponse(res, 200, data); - } catch (e) { - defaultLog.error(`Error deleting CAC for project: ${projId}: ${e.message}`); - return Actions.sendResponse(res, 400, e); - } -}; diff --git a/api/controllers/inspection.js b/api/controllers/inspection.js index 2685c27d..04406229 100644 --- a/api/controllers/inspection.js +++ b/api/controllers/inspection.js @@ -1,304 +1,12 @@ var defaultLog = require('winston').loggers.get('default'); -var mongoose = require('mongoose'); var Actions = require('../helpers/actions'); var Utils = require('../helpers/utils'); -var mime = require('mime-types'); -var uploadDir = process.env.UPLOAD_DIRECTORY || './uploads/'; var MinioController = require('../helpers/minio'); -var demiPush = require('../helpers/demiPush'); exports.protectedOptions = function (args, res) { res.status(200).send(); }; -// Create a new inspection -exports.protectedPostInspection = async function (args, res) { - var obj = args.swagger.params.inspection.value; - - defaultLog.info('Incoming new object:', obj); - - var Inspection = mongoose.model('Inspection'); - - var inspection = new Inspection(obj); - - inspection.startDate = obj.startDate; - inspection.endDate = obj.endDate; - - if (obj.project) { - inspection.project = new mongoose.Types.ObjectId(obj.project); - } else { - inspection.customInspectionName = obj.customInspectionName; - } - // Define security tag defaults - inspection.read = ['sysadmin', 'inspector']; - inspection.write = ['sysadmin', 'inspector']; - inspection.delete = ['sysadmin', 'inspector']; - inspection._createdBy = args.swagger.params.auth_payload.preferred_username; - inspection._createdDate = Date.now(); - - // This is an overwrite in case of error from mobile app, - // otherwise this will create a new record. - try { - let elementDocument = await Inspection.findOne({_schemaName: 'Inspection', inspectionId: inspection.inspectionId}); - if (elementDocument) { - // We alrady had this - send it back to them. - return Actions.sendResponse(res, 200, elementDocument); - } else { - inspection.save() - .then(function (doc) { - Utils.recordAction('Post', 'Inspection', args.swagger.params.auth_payload.preferred_username, doc._id); - const push = demiPush.inspection(doc); - doc.status = 'Uploading'; - return Actions.sendMirrored(res, 200, doc, push); - }) - .catch(function (err) { - defaultLog.error('Error saving inspection:', err); - return Actions.sendResponse(res, 400, err); - }); - } - } catch (err) { - defaultLog.error('Error saving inspection:', err); - return Actions.sendResponse(res, 400, err); - } -}; - -// Create a new inspection element -exports.protectedPostElement = async function (args, res) { - var obj = args.swagger.params.inspection.value; - var inspId = args.swagger.params.inspId.value; - - defaultLog.info('Incoming new object:', obj); - if (obj && obj.elementId && !mongoose.Types.ObjectId.isValid(obj.elementId)) { - return Actions.sendResponse(res, 400, { }); - } - - var InspectionElement = mongoose.model('InspectionElement'); - - var inspectionElement = new InspectionElement(obj); - - // Define security tag defaults - inspectionElement.read = ['sysadmin', 'inspector']; - inspectionElement.write = ['sysadmin', 'inspector']; - inspectionElement.delete = ['sysadmin', 'inspector']; - inspectionElement._createdBy = args.swagger.params.auth_payload.preferred_username; - inspectionElement._createdDate = Date.now(); - - // If this exists already, don't bother to insert.. just return 200 OK - // Based on elementId (as per mobile uuid); We don't want to modify it in case there - // are other elements that exist in the elements array - var Inspection = mongoose.model('Inspection'); - let elementDocument = await Inspection.findOne({_schemaName: 'InspectionElement', elementId: inspectionElement.elementId}); - if (elementDocument) { - // We alrady had this - send it back to them. - return Actions.sendResponse(res, 200, elementDocument); - } else { - // Couldn't find that id, let it pass through. - let theDoc = null; - inspectionElement.save() - .then(function (doc) { - theDoc = doc; - var Inspection = mongoose.model('Inspection'); - return Inspection.updateOne( - { _id: new mongoose.Types.ObjectId(inspId) }, - { - $push: { - elements: doc._id - } - } - ); - }) - .then(function (updated) { - Utils.recordAction('Post', 'InspectionElement', args.swagger.params.auth_payload.preferred_username, theDoc._id); - return Actions.sendMirrored(res, 200, theDoc, [ - demiPush.inspectionElement(theDoc), - demiPush.pushIfMatched(demiPush.inspection, updated, inspId) - ]); - }) - .catch(function (err) { - defaultLog.error('Error saving inspection element:', err); - return Actions.sendResponse(res, 400, err); - }); - } -}; - -// Create element item -exports.protectedPostElementItem = async function (args, res) { - var upfile = args.swagger.params.upfile.value; - var guid = args.swagger.params.itemId.value; - var project = null; - if (args.swagger.params.project.value) { - project = args.swagger.params.project.value; - } - var elementId = args.swagger.params.elementId.value; - var type = args.swagger.params.type.value; - var timestamp = args.swagger.params.timestamp.value; - var caption = args.swagger.params.caption.value; - var text = args.swagger.params.text.value; - var geo = args.swagger.params.geo.value; - - if (args.swagger.params.elementId && args.swagger.params.elementId.value && !mongoose.Types.ObjectId.isValid(args.swagger.params.elementId.value)) { - return Actions.sendResponse(res, 400, { }); - } - if (args.swagger.params.project && args.swagger.params.project.value && !mongoose.Types.ObjectId.isValid(args.swagger.params.project.value)) { - return Actions.sendResponse(res, 400, { }); - } - if (args.swagger.params.itemId && args.swagger.params.itemId.value && !mongoose.Types.ObjectId.isValid(args.swagger.params.itemId.value)) { - return Actions.sendResponse(res, 400, { }); - } - - var ext, tempFilePath = null; - if (upfile) { - ext = mime.extension(args.swagger.params.upfile.value.mimetype); - tempFilePath = uploadDir + guid + '.' + ext; - - var fs = require('fs'); - fs.writeFileSync(tempFilePath, args.swagger.params.upfile.value.buffer); - defaultLog.info('Wrote inspection item file: %s', tempFilePath); - - defaultLog.debug('putDocument bucket=%s project=%s filename=%s path=%s', - MinioController.BUCKETS.DOCUMENTS_BUCKET, project, upfile.originalname, tempFilePath); - - MinioController.putDocument(MinioController.BUCKETS.DOCUMENTS_BUCKET, - project, - upfile.originalname, - tempFilePath) - .then(async function (minioFile) { - defaultLog.debug('putDocument success: %s', minioFile && minioFile.path); - - // remove file from temp folder - fs.unlinkSync(tempFilePath); - - defaultLog.debug('putDocument: temp file unlinked'); - - var InspectionItem = mongoose.model('InspectionItem'); - var doc = new InspectionItem(); - - // Set the mobile itemId - doc.itemId = guid; - - // Define security tag defaults - if (project) { - doc.project = new mongoose.Types.ObjectId(project); - } - doc._addedBy = args.swagger.params.auth_payload.preferred_username; - doc._createdDate = new Date(); - doc.read = ['sysadmin', 'inspector']; - doc.write = ['sysadmin', 'inspector']; - doc.delete = ['sysadmin', 'inspector']; - - doc.type = type; - doc.caption = caption; - doc.timestamp = timestamp; - doc.geo = JSON.parse(geo); - - doc.internalURL = minioFile.path; - doc.internalExt = minioFile.extension; - doc.internalSize = upfile.size; - doc.internalMime = upfile.mimetype; - - let itemDocument = await InspectionItem.findOne({_schemaName: 'InspectionItem', itemId: doc.itemId}); - if (itemDocument) { - // We already had this - send it back to them. - return Actions.sendResponse(res, 200, itemDocument); - } else { - var savedDocument = null; - doc.save() - .then(function (d) { - defaultLog.info('Saved new document object:', d._id); - Utils.recordAction('Post', 'InspectionItem', args.swagger.params.auth_payload.preferred_username, d._id); - savedDocument = d; - return; - }).then(function () { - // Push this into the inspection elements' items' array for things. - var InspectionElement = mongoose.model('InspectionElement'); - return InspectionElement.updateOne( - { _id: new mongoose.Types.ObjectId(elementId) }, - { - $push: { - items: doc - } - } - ); - }).then(function (theInspection) { - defaultLog.debug('Updated InspectionElement after item push: %j', theInspection); - return [ - demiPush.inspectionItem(savedDocument), - demiPush.pushIfMatched(demiPush.inspectionElement, theInspection, elementId) - ]; - }) - // Two-argument then: a failed push answers 200 with mirrored: false and flags the row for the sweep, so it must not fall into the undo path below. - .then(pushes => Actions.sendMirrored(res, 200, savedDocument, pushes), function (error) { - defaultLog.error('Error saving InspectionItem:', error); - // the model failed to be created - delete the document from minio so the database and minio remain in sync. - MinioController.deleteDocument(MinioController.BUCKETS.DOCUMENTS_BUCKET, doc.project, doc.internalURL); - return Actions.sendResponse(res, 400, error); - }); - } - }); - } else { - // Just a text element. - var InspectionItem = mongoose.model('InspectionItem'); - var doc = new InspectionItem(); - - doc.itemId = guid; - - // Define security tag defaults - if (project) { - doc.project = new mongoose.Types.ObjectId(project); - } - doc._addedBy = args.swagger.params.auth_payload.preferred_username; - doc._createdDate = new Date(); - doc.read = ['sysadmin', 'inspector']; - doc.write = ['sysadmin', 'inspector']; - doc.delete = ['sysadmin', 'inspector']; - - doc.text = JSON.parse(text); - doc.markModified('text'); - doc.type = type; - doc.geo = JSON.parse(geo); - doc.markModified('geo'); - - let itemDocument = await InspectionItem.findOne({_schemaName: 'InspectionItem', itemId: doc.itemId}); - if (itemDocument) { - // We alrady had this - send it back to them. - return Actions.sendResponse(res, 200, itemDocument); - } else { - var savedDocument = null; - doc.save() - .then(function (d) { - defaultLog.info('Saved new document object:', d._id); - Utils.recordAction('Post', 'InspectionItem', args.swagger.params.auth_payload.preferred_username, d._id); - savedDocument = d; - return; - }).then(function () { - // Push this into the inspection elements' array for things. - var InspectionElement = mongoose.model('InspectionElement'); - return InspectionElement.updateOne( - { _id: new mongoose.Types.ObjectId(elementId) }, - { - $push: { - items: doc - } - } - ); - }).then(function (theInspection) { - defaultLog.debug('Updated InspectionElement after text item push: %j', theInspection); - return [ - demiPush.inspectionItem(savedDocument), - demiPush.pushIfMatched(demiPush.inspectionElement, theInspection, elementId) - ]; - }) - // Two-argument then: a failed push answers 200 with mirrored: false and flags the row for the sweep, so it must not fall into the undo path below. - .then(pushes => Actions.sendMirrored(res, 200, savedDocument, pushes), function (error) { - defaultLog.error('Error saving InspectionItem (text):', error); - // the model failed to be created - delete the document from minio so the database and minio remain in sync. - MinioController.deleteDocument(MinioController.BUCKETS.DOCUMENTS_BUCKET, doc.project, doc.internalURL); - return Actions.sendResponse(res, 400, error); - }); - } - } -}; - exports.protectedElementItemGet = function (args, res) { var self = this; self.scopes = args.swagger.params.auth_payload.realm_access.roles; diff --git a/api/controllers/job.js b/api/controllers/job.js index e65dc999..17ee9041 100644 --- a/api/controllers/job.js +++ b/api/controllers/job.js @@ -3,40 +3,19 @@ /** * job.js — Async Jobs Controller * - * Role-gated API for queuing and monitoring background jobs. - * Authorization is per job type (see JOB_PERMISSIONS below). - * Sysadmin may read/cancel any job regardless of owner. + * Read-only API over background jobs already in the Agenda queue. + * Sysadmin may read any job regardless of owner; others only their own. * * Flow: - * POST /api/jobs – queue job, return 202 { jobId } * GET /api/jobs – sysadmin: all jobs; others: own jobs only * GET /api/jobs/:jobId – owner or sysadmin * GET /api/jobs/:jobId/download – owner or sysadmin - * DELETE /api/jobs/:jobId – owner or sysadmin - * - * Adding a new job type: - * 1. Define the handler in jobQueue.js (agenda.define) - * 2. Add an entry to JOB_PERMISSIONS below - * 3. Add the body/validation logic to createJob */ const { ObjectId } = require('mongodb'); const defaultLog = require('winston').loggers.get('default'); const { getAgenda } = require('../helpers/jobQueue'); -// ── Permissions ────────────────────────────────────────────────────────────── - -/** - * Maps each job type to the Keycloak roles allowed to CREATE that job. - * Sysadmin can always READ/CANCEL any job regardless of this map. - * - * To add a new type: - * 'batch-document-download': ['sysadmin', 'public'] - */ -const JOB_PERMISSIONS = { - 'project-doc-export': ['sysadmin'], -}; - function hasRole(authPayload, role) { return Array.isArray(authPayload?.realm_access?.roles) && authPayload.realm_access.roles.includes(role); @@ -46,12 +25,6 @@ function hasSysadmin(authPayload) { return hasRole(authPayload, 'sysadmin'); } -function canCreateJob(authPayload, type) { - const allowedRoles = JOB_PERMISSIONS[type]; - if (!allowedRoles) return false; - return allowedRoles.some(role => hasRole(authPayload, role)); -} - exports.protectedOptions = function (args, res) { res.status(200).send(); @@ -102,52 +75,6 @@ function userId(authPayload) { // ── Endpoints ──────────────────────────────────────────────────────────────── -/** - * POST /api/jobs - * - * Body: { type: 'project-doc-export', projectId: string, includeAll?: boolean } - * Response 202: { jobId: string } - */ -exports.createJob = async function (args, res) { - const authPayload = args.swagger.params.auth_payload; - const body = args.swagger.params.body?.value || {}; - const { type, projectId, includeAll } = body; - - if (!JOB_PERMISSIONS[type]) { - return res.status(400).json({ message: `Unknown job type: ${type}. Allowed: ${Object.keys(JOB_PERMISSIONS).join(', ')}` }); - } - - if (!canCreateJob(authPayload, type)) { - return res.status(403).json({ message: `Insufficient permissions to create job type: ${type}` }); - } - - if (!projectId || typeof projectId !== 'string') { - return res.status(400).json({ message: 'projectId is required.' }); - } - - if (!ObjectId.isValid(projectId)) { - return res.status(400).json({ message: 'projectId must be a valid MongoDB ObjectId.' }); - } - - const requestedBy = userId(authPayload); - - try { - const agenda = getAgenda(); - const job = await agenda.now(type, { - projectId, - includeAll: !!includeAll, - requestedBy, - requestedAt: new Date().toISOString(), - }); - - defaultLog.info(`[job] queued ${type} ${job.attrs._id} for ${requestedBy}`); - return res.status(202).json({ jobId: job.attrs._id }); - } catch (err) { - defaultLog.error(`[job] createJob error: ${err.message}`); - return res.status(500).json({ message: 'Failed to queue job.' }); - } -}; - /** * GET /api/jobs * @@ -269,36 +196,3 @@ exports.downloadJobResult = async function (args, res) { } }; -/** - * DELETE /api/jobs/:jobId - * - * Cancels a queued job or removes a finished/failed job record. - */ -exports.cancelJob = async function (args, res) { - const authPayload = args.swagger.params.auth_payload; - const jobId = args.swagger.params.jobId.value; - const requestedBy = userId(authPayload); - - let jobObjId; - try { - jobObjId = new ObjectId(jobId); - } catch { - return res.status(400).json({ message: 'Invalid jobId.' }); - } - - try { - const agenda = getAgenda(); - const jobs = await agenda.jobs({ _id: jobObjId }); - - // Owner always sees their job; sysadmin sees any job. - if (!jobs.length || (jobs[0].attrs.data?.requestedBy !== requestedBy && !hasSysadmin(authPayload))) { - return res.status(404).json({ message: 'Job not found.' }); - } - - await agenda.cancel({ _id: jobObjId }); - return res.status(204).send(); - } catch (err) { - defaultLog.error(`[job] cancelJob error: ${err.message}`); - return res.status(500).json({ message: 'Failed to cancel job.' }); - } -}; diff --git a/api/controllers/materialized-view.js b/api/controllers/materialized-view.js index eeeb3299..5b953f10 100644 --- a/api/controllers/materialized-view.js +++ b/api/controllers/materialized-view.js @@ -1,46 +1,12 @@ 'use strict'; -const defaultLog = require('winston').loggers.get('default'); const Actions = require('../helpers/actions'); -const { updateHotViews, updateColdViews, updateAllMaterializedViews } = require('../materialized_views/updateViews'); -const { isLocked, getLockInfo, forceRelease } = require('../materialized_views/runLock'); +const { getLockInfo } = require('../materialized_views/runLock'); exports.protectedOptions = function (args, res) { res.status(200).send(); }; -exports.protectedRefresh = async function (args, res) { - const body = args.swagger.params.body && args.swagger.params.body.value; - const subset = (body && body.subset) || 'all'; - const force = !!(body && body.force); - - if (!['hot', 'cold', 'all'].includes(subset)) { - return Actions.sendResponse(res, 400, { error: 'subset must be hot, cold, or all' }); - } - - const lockId = subset === 'all' ? 'mat_view_all' : `mat_view_${subset}`; - - if (force) { - await forceRelease(lockId, defaultLog); - } else { - const locked = await isLocked(lockId); - if (locked) { - return Actions.sendResponse(res, 409, { error: 'Refresh already in progress', lockId }); - } - } - - switch (subset) { - case 'hot': updateHotViews(defaultLog); break; - case 'cold': updateColdViews(defaultLog); break; - default: updateAllMaterializedViews(defaultLog); break; - } - - const user = args.swagger.params.auth_payload && args.swagger.params.auth_payload.preferred_username; - defaultLog.info(`[mat-view] manual refresh triggered: subset=${subset}, user=${user}`); - - return Actions.sendResponse(res, 202, { message: 'Refresh started', subset }); -}; - exports.protectedGetStatus = async function (args, res) { const [hot, cold, all] = await Promise.all([ getLockInfo('mat_view_hot'), diff --git a/api/controllers/recentActivity.js b/api/controllers/recentActivity.js index e0f0dd6a..7bbc4fd5 100644 --- a/api/controllers/recentActivity.js +++ b/api/controllers/recentActivity.js @@ -109,20 +109,11 @@ exports.publicGet = async function (args, res) { }; exports.protectedDelete = async function (args, res) { - defaultLog.info('Deleting a RecentActivity(s)'); + defaultLog.info('Deleting a RecentActivity'); defaultLog.info('args.swagger.params:', args.swagger.operation['x-security-scopes']); - var RecentActivity = mongoose.model('RecentActivity'); - var query = {}; - // Build match query if on recentActivityId route - if (args.swagger.params.recentActivityId) { - query = Utils.buildQuery('_id', args.swagger.params.recentActivityId.value, { _schemaName: 'RecentActivity' }); - } - - if (!query._id) { - // Don't allow unilateral delete. - return Actions.sendResponse(res, 400, 'Can\'t delete entire collection.'); - } + const RecentActivity = mongoose.model('RecentActivity'); + const query = Utils.buildQuery('_id', args.swagger.params.recentActivityId.value, { _schemaName: 'RecentActivity' }); // Archive, never delete: a published Update keeps its public URL answerable ("no longer available"). let rec; diff --git a/api/controllers/topic.js b/api/controllers/topic.js index e90cb829..d1548810 100644 --- a/api/controllers/topic.js +++ b/api/controllers/topic.js @@ -1,5 +1,3 @@ -var defaultLog = require('winston').loggers.get('default'); -var mongoose = require('mongoose'); var Actions = require('../helpers/actions'); var Utils = require('../helpers/utils'); const ALLOWED_FIELDS = [ @@ -14,26 +12,6 @@ exports.protectedOptions = function (args, res) { res.status(200).send(); }; -// Create a new topic -exports.protectedPost = async function (args, res) { - var obj = args.swagger.params.topic.value; - - defaultLog.info('Incoming new object:', obj); - - var Topic = mongoose.model('Topic'); - var topic = new Topic(obj); - topic._schemaName = 'Topic'; - topic.read = ['project-system-admin']; - - // Change this to use guid instead of idir/user - topic._addedBy = args.swagger.params.auth_payload.preferred_username; - - // Define security tag defaults - var theTopic = await topic.save(); - Utils.recordAction('Post', 'Topic', args.swagger.params.auth_payload.preferred_username, theTopic._id); - return Actions.sendResponse(res, 200, theTopic); -}; - exports.protectedGet = async function (args, res) { var skip = null, limit = null, sort = {}, query = {}; @@ -66,47 +44,3 @@ exports.protectedGet = async function (args, res) { Utils.recordAction('Get', 'Topic', args.swagger.params.auth_payload.preferred_username, args.swagger.params.topicId && args.swagger.params.topicId.value ? args.swagger.params.topicId.value : null); return Actions.sendResponse(res, 200, data); }; - -exports.protectedPut = async function (args, res) { - var objId; - try { - objId = Utils.getValidObjectId(args.swagger.params.topicId); - } catch (e) { - return Actions.sendResponse(res, 400, {}); - } - defaultLog.info('ObjectID:', objId); - var obj = args.swagger.params.cp.value; - - // Strip security tags - these will not be updated on this route. - delete obj.tags; - - defaultLog.info('Incoming updated object:', obj); - - var topic = require('mongoose').model('Topic'); - - // Change this to use guid instead of idir/user - topic._updatedBy = args.swagger.params.auth_payload.preferred_username; - - var data = await topic.findOneAndUpdate({ _id: objId }, obj, { upsert: false, returnDocument: 'after' }).exec(); - Utils.recordAction('Put', 'Topic', args.swagger.params.auth_payload.preferred_username, objId); - return Actions.sendResponse(res, 200, data); -}; - -exports.protectedDelete = async function (args, res) { - var objId; - try { - objId = Utils.getValidObjectId(args.swagger.params.topicId); - } catch (e) { - return Actions.sendResponse(res, 400, {}); - } - defaultLog.info('Delete Topic:', objId); - - var topic = require('mongoose').model('Topic'); - - // Change this to use guid instead of idir/user - topic._deletedBy = args.swagger.params.auth_payload.preferred_username; - - var data = await topic.deleteOne({ _id: objId }); - Utils.recordAction('Delete', 'Topic', args.swagger.params.auth_payload.preferred_username, objId); - return Actions.sendResponse(res, 200, data); -}; diff --git a/api/controllers/vc.js b/api/controllers/vc.js index 8de865a7..2043992a 100644 --- a/api/controllers/vc.js +++ b/api/controllers/vc.js @@ -1,5 +1,3 @@ -var defaultLog = require('winston').loggers.get('default'); -var mongoose = require('mongoose'); var Actions = require('../helpers/actions'); var Utils = require('../helpers/utils'); const ALLOWED_FIELDS = [ @@ -18,25 +16,6 @@ exports.protectedOptions = function (args, res) { res.status(200).send(); }; -// Create a new vc -exports.protectedPost = async function (args, res) { - var obj = args.swagger.params.vc.value; - - defaultLog.info('Incoming new object:', obj); - - var Vc = mongoose.model('Vc'); - var vc = new Vc(obj); - vc._schemaName = 'Vc'; - vc.read = ['public', 'project-system-admin', 'staff']; - vc.write = ['project-system-admin', 'staff']; - vc.delete = ['project-system-admin', 'staff']; - - // Define security tag defaults - var theVc = await vc.save(); - Utils.recordAction('Post', 'Vc', args.swagger.params.auth_payload.preferred_username, theVc._id); - return Actions.sendResponse(res, 200, theVc); -}; - exports.protectedGet = async function (args, res) { var skip = null, limit = null, sort = {}; var query = {}; @@ -79,38 +58,3 @@ exports.protectedGet = async function (args, res) { Utils.recordAction('Get', 'Vc', args.swagger.params.auth_payload.preferred_username, args.swagger.params.vcId && args.swagger.params.vcId.value ? args.swagger.params.vcId.value : null); return Actions.sendResponse(res, 200, data); }; - -exports.protectedPut = async function (args, res) { - var objId; - try { - objId = Utils.getValidObjectId(args.swagger.params.vcId); - } catch (e) { - return Actions.sendResponse(res, 400, {}); - } - defaultLog.info('ObjectID:', objId); - var obj = args.swagger.params.cp.value; - - // Strip security tags - these will not be updated on this route. - delete obj.tags; - - defaultLog.info('Incoming updated object:', obj); - - var valuedComponent = require('mongoose').model('Vc'); - var data = await valuedComponent.findOneAndUpdate({ _id: objId }, obj, { upsert: false, returnDocument: 'after' }).exec(); - Utils.recordAction('Put', 'Vc', args.swagger.params.auth_payload.preferred_username, objId); - return Actions.sendResponse(res, 200, data); -}; - -exports.protectedDelete = async function (args, res) { - var objId; - try { - objId = Utils.getValidObjectId(args.swagger.params.vcId); - } catch (e) { - return Actions.sendResponse(res, 400, {}); - } - defaultLog.info('Delete Vc:', objId); - var commentperiod = require('mongoose').model('Vc'); - var data = await commentperiod.deleteOne({ _id: objId }); - Utils.recordAction('Delete', 'Vc', args.swagger.params.auth_payload.preferred_username, objId); - return Actions.sendResponse(res, 200, data); -}; diff --git a/api/materialized_views/runLock.js b/api/materialized_views/runLock.js index 580a3727..c2ab7adf 100644 --- a/api/materialized_views/runLock.js +++ b/api/materialized_views/runLock.js @@ -68,11 +68,6 @@ async function releaseLock(lockId, defaultLog, stats) { } } -async function isLocked(lockId) { - const lock = await col().findOne({ _id: lockId }); - return !!(lock && lock.running && lock.startedAt > staleThreshold()); -} - async function getLockInfo(lockId) { const lock = await col().findOne({ _id: lockId }); if (!lock) return { running: false }; @@ -85,13 +80,4 @@ async function getLockInfo(lockId) { }; } -async function forceRelease(lockId, defaultLog) { - try { - await col().updateOne({ _id: lockId }, { $set: { running: false } }); - defaultLog.warn(`[mat-view] lock '${lockId}' force-released`); - } catch (err) { - defaultLog.error(`[mat-view] failed to force-release lock '${lockId}': ${err.message}`); - } -} - -module.exports = { acquireLock, releaseLock, isLocked, getLockInfo, forceRelease }; +module.exports = { acquireLock, releaseLock, getLockInfo }; diff --git a/api/swagger/swagger.yaml b/api/swagger/swagger.yaml index d390a00f..66ceceac 100644 --- a/api/swagger/swagger.yaml +++ b/api/swagger/swagger.yaml @@ -215,16 +215,6 @@ definitions: type: string example: "[['public']]" ## TODO: Add more fields here. - InspectionElementItemObject: - type: object - properties: - _id: - type: string - example: "54759eb3c090d83494e2d804" - tags: - type: string - example: "[['public']]" - ## TODO: Add more fields here. GroupObject: type: object properties: @@ -604,12 +594,6 @@ definitions: - +documentFileSize - -documentFileSize - AuditObject: - type: object - properties: - keywords: - type: string - example: "Project Name" AuditSortByFields: type: string description: "Optional fields to sort by (prefixed with + or - for order)" @@ -738,12 +722,6 @@ definitions: example: "John Doe wants to sign up for the CAC" ### Topic Definitions - TopicId: - type: object - properties: - comment: - type: string - example: "A cool user" TopicObject: type: object properties: @@ -803,12 +781,6 @@ definitions: - roles ### Vc Definitions - VcId: - type: object - properties: - comment: - type: string - example: "A cool user" VcObject: type: object properties: @@ -858,28 +830,6 @@ definitions: type: string ### Task Definitions - TaskObject: - type: object - example: 'A task object' - required: [taskType] - properties: - params: - type: object - example: '{"taskType": "updateMaterializedView"}' - taskType: - type: string - description: 'The type of task' - example: 'updateMaterializedView' - enum: &taskTypes - - updateMaterializedView - materializedViewSubset: - type: string - description: 'Name of subset to update' - example: 'default' - enum: &materializedViewSubset - - default - - hot - - cold ### Path Definitions paths: /login/token: @@ -1761,104 +1711,6 @@ paths: schema: $ref: "#/definitions/Error" - /project/{projId}/cac/publish: - x-swagger-router-controller: cac - options: - tags: - - project - summary: "Pre-flight request" - operationId: protectedOptions - description: "Options on Project Route" - parameters: - - name: projId - in: path - description: "ID of project to get" - required: true - type: string - responses: - "200": - description: "Success" - schema: - $ref: "#/definitions/ProjObject" - "403": - description: "Access Denied" - schema: - $ref: "#/definitions/Error" - put: - tags: - - cac - summary: "Publish a CAC on a project" - operationId: protectedPublishCAC - description: "Sets the value true role to the project projectCACPublished field" - security: - - Bearer: [] - x-security-scopes: - - staff - - sysadmin - parameters: - - name: projId - in: path - description: "ID of project to update" - required: true - type: string - responses: - "200": - description: "Success" - schema: - $ref: "#/definitions/DocObject" - "403": - description: "Access Denied" - schema: - $ref: "#/definitions/Error" - /project/{projId}/cac/unpublish: - x-swagger-router-controller: cac - options: - tags: - - project - summary: "Pre-flight request" - operationId: protectedOptions - description: "Options on Project Route" - parameters: - - name: projId - in: path - description: "ID of project to get" - required: true - type: string - responses: - "200": - description: "Success" - schema: - $ref: "#/definitions/ProjObject" - "403": - description: "Access Denied" - schema: - $ref: "#/definitions/Error" - put: - tags: - - cac - summary: "Unpublish a CAC on a project" - operationId: protectedUnPublishCAC - description: "Sets the value of projectCACPublished to false" - security: - - Bearer: [] - x-security-scopes: - - staff - - sysadmin - parameters: - - name: projId - in: path - description: "ID of project to update" - required: true - type: string - responses: - "200": - description: "Success" - schema: - $ref: "#/definitions/DocObject" - "403": - description: "Access Denied" - schema: - $ref: "#/definitions/Error" /project/{projId}/pin/{pinId}: x-swagger-router-controller: pins options: @@ -2907,117 +2759,6 @@ paths: description: "Access Denied" schema: $ref: "#/definitions/Error" - /project/{projId}/cac: - x-swagger-router-controller: cac - options: - tags: - - project - summary: "Pre-flight request" - operationId: protectedOptions - description: "Options on Project Route" - parameters: - - name: projId - in: path - description: "ID of project to put request to" - required: true - type: string - responses: - "200": - description: "Success" - schema: - $ref: "#/definitions/CACObject" - "403": - description: "Access Denied" - schema: - $ref: "#/definitions/Error" - put: - tags: - - project - summary: "Remove member from CAC on a project" - operationId: protectedCACRemoveMember - description: "Remove member from CAC on a project." - security: - - Bearer: [] - x-security-scopes: - - staff - - sysadmin - parameters: - - name: projId - in: path - description: "ID of project to update" - required: true - type: string - - name: member - in: body - required: true - schema: - $ref: "#/definitions/CACObject" - responses: - "200": - description: "Success" - schema: - $ref: "#/definitions/CACObject" - "403": - description: "Access Denied" - schema: - $ref: "#/definitions/Error" - delete: - tags: - - project - summary: "Remove CAC from project" - operationId: protectedCACDelete - description: "Remove CAC from project." - security: - - Bearer: [] - x-security-scopes: - - staff - - sysadmin - parameters: - - name: projId - in: path - description: "ID of project to update" - required: true - type: string - responses: - "200": - description: "Success" - schema: - $ref: "#/definitions/CACObject" - "403": - description: "Access Denied" - schema: - $ref: "#/definitions/Error" - post: - tags: - - project - summary: "Add CAC to a project" - operationId: protectedCreateCAC - description: "Add CAC to a project." - security: - - Bearer: [] - x-security-scopes: - - staff - - sysadmin - parameters: - - name: projId - in: path - description: "ID of project to update" - required: true - type: string - - name: data - in: body - required: true - schema: - $ref: "#/definitions/Project" - responses: - "200": - description: "Success" - schema: - $ref: "#/definitions/Project" - "403": - description: "Access Denied" - schema: - $ref: "#/definitions/Error" /Public/project/{projId}/FeaturedDocuments: x-swagger-router-controller: project options: @@ -3158,7 +2899,8 @@ paths: schema: $ref: "#/definitions/Error" ### Inspections - /inspection: + + /inspection/{inspId}/{elementId}/{itemId}: x-swagger-router-controller: inspection options: tags: @@ -3166,6 +2908,22 @@ paths: summary: "Pre-flight request" operationId: protectedOptions description: "Options on Inspection Route" + parameters: + - name: inspId + in: path + description: "ID of inspection" + required: true + type: string + - name: elementId + in: path + description: "ID of Element" + required: true + type: string + - name: itemId + in: path + description: "ID of Item" + required: true + type: string responses: "200": description: "Success" @@ -3175,236 +2933,21 @@ paths: description: "Access Denied" schema: $ref: "#/definitions/Error" - post: + get: tags: - inspection - summary: "Adds a new Inspection" - operationId: protectedPostInspection - description: "Adds a new Inspection." + summary: "Get Item related to an Inspection Element" + operationId: protectedElementItemGet + description: "Get Item related to an Inspection Element." security: - Bearer: [] x-security-scopes: - staff - sysadmin parameters: - - name: inspection - in: body - required: true - schema: - $ref: "#/definitions/InspectionObject" - responses: - "200": - description: "Success" - schema: - $ref: "#/definitions/InspectionObject" - "403": - description: "Access Denied" - schema: - $ref: "#/definitions/Error" - - /inspection/{inspId}/element: - x-swagger-router-controller: inspection - options: - tags: - - inspection - summary: "Pre-flight request" - operationId: protectedOptions - description: "Options on Inspection Route" - parameters: - - name: inspId - in: path - description: "ID of inspection to bind this element to" - required: true - type: string - responses: - "200": - description: "Success" - schema: - $ref: "#/definitions/ProjObject" - "403": - description: "Access Denied" - schema: - $ref: "#/definitions/Error" - post: - tags: - - inspection - summary: "Adds a new Element to an Inspection" - operationId: protectedPostElement - description: "Adds a new Element to an inspection." - security: - - Bearer: [] - x-security-scopes: - - staff - - sysadmin - parameters: - - name: inspId - in: path - description: "ID of inspection to bind this element to" - required: true - type: string - - name: inspection - in: body - required: true - schema: - $ref: "#/definitions/InspectionObject" - responses: - "200": - description: "Success" - schema: - $ref: "#/definitions/InspectionObject" - "403": - description: "Access Denied" - schema: - $ref: "#/definitions/Error" - /inspection/{inspId}/{elementId}/item: - x-swagger-router-controller: inspection - options: - tags: - - inspection - summary: "Pre-flight request" - operationId: protectedOptions - description: "Options on Inspection Route" - parameters: - - name: inspId - in: path - type: string - required: true - description: "Inspection this Item relates to." - - name: elementId - in: path - type: string - required: true - description: "Inspection Element this Item relates to." - responses: - "200": - description: "Success" - schema: - $ref: "#/definitions/InspectionElementItemObject" - "403": - description: "Access Denied" - schema: - $ref: "#/definitions/Error" - post: - tags: - - inspection - summary: "Add Inspection item to an existing element" - operationId: protectedPostElementItem - description: "Add Inspection item to an existing element" - security: - - Bearer: [] - x-security-scopes: - - staff - - sysadmin - consumes: - - multipart/form-data - parameters: - - name: inspId - in: path - type: string - required: true - description: "Inspection Element this Item relates to." - - name: elementId - in: path - type: string - required: true - description: "Inspection Element this Element Item relates to." - - name: project - in: formData - type: string - required: false - description: "Project this Inspection Element Item relates to." - - name: upfile - in: formData - description: "The file to upload." - required: false - type: file - - name: type - in: formData - required: false - type: string - description: "Type" - - name: itemId - in: formData - required: true - type: string - description: "Unique ID of the item in the mobile device" - - name: caption - in: formData - required: false - type: string - description: "Caption" - - name: timestamp - in: formData - required: false - type: string - description: "Timestamp" - - name: text - in: formData - required: false - type: string - description: "Text Object" - - name: geo - in: formData - required: true - type: string - description: "Geo Object" - responses: - "200": - description: "Success" - schema: - $ref: "#/definitions/InspectionElementItemObject" - "403": - description: "Access Denied" - schema: - $ref: "#/definitions/Error" - /inspection/{inspId}/{elementId}/{itemId}: - x-swagger-router-controller: inspection - options: - tags: - - inspection - summary: "Pre-flight request" - operationId: protectedOptions - description: "Options on Inspection Route" - parameters: - - name: inspId - in: path - description: "ID of inspection" - required: true - type: string - - name: elementId - in: path - description: "ID of Element" - required: true - type: string - - name: itemId - in: path - description: "ID of Item" - required: true - type: string - responses: - "200": - description: "Success" - schema: - $ref: "#/definitions/ProjObject" - "403": - description: "Access Denied" - schema: - $ref: "#/definitions/Error" - get: - tags: - - inspection - summary: "Get Item related to an Inspection Element" - operationId: protectedElementItemGet - description: "Get Item related to an Inspection Element." - security: - - Bearer: [] - x-security-scopes: - - staff - - sysadmin - parameters: - - name: inspId - in: path - description: "ID of inspection" + - name: inspId + in: path + description: "ID of inspection" required: true type: string - name: elementId @@ -3585,32 +3128,6 @@ paths: description: "Access Denied" schema: $ref: "#/definitions/Error" - delete: - tags: - - recentActivity - summary: "Refused: archive one recentActivity by id instead" - operationId: protectedDelete - description: "Always 400. Archive a single Update through DELETE /recentActivity/{recentActivityId}." - security: - - Bearer: [] - x-security-scopes: - - project-system-admin - - sysadmin - parameters: - - name: applicationID - in: query - type: string - required: true - description: "Delete activities corresponding to an applicationID." - responses: - "200": - description: "Success" - schema: - $ref: "#/definitions/RecentActivityObject" - "403": - description: "Access Denied" - schema: - $ref: "#/definitions/Error" /recentActivity/{recentActivityId}: x-swagger-router-controller: recentActivity options: @@ -6706,201 +6223,85 @@ paths: schema: $ref: "#/definitions/Error" -### Audit - /audit: - x-swagger-router-controller: audit +### Topic Routes + /topic: + x-swagger-router-controller: topic options: tags: - - audit + - topic summary: "Pre-flight request" operationId: protectedOptions - description: "Options on audit Route" + description: "Options on Topic Route" responses: "200": description: "Success" schema: - $ref: "#/definitions/AuditObject" + $ref: "#/definitions/TopicObject" "403": description: "Access Denied" schema: $ref: "#/definitions/Error" get: tags: - - audit - summary: "Get a list of things related to these keywords" + - topic + summary: "Get a list of topics" operationId: protectedGet - description: "Retreiving items relating to the given keywords." + description: "Authenticated access to retrieving topics." security: - Bearer: [] x-security-scopes: - project-system-admin - - sysadmin parameters: - - in: query - name: keywords - description: "Full Text Audit based on keywords" - type: string - required: false - collectionFormat: pipes - items: - type: string - - in: query - name: fields - description: "Audit fields to return." - required: false - type: array - collectionFormat: pipes - items: - type: string - enum: *auditFields - - in: query - name: action - description: "Dataset to search on" - type: string - required: false - - in: query - name: timestamp - description: "Timestamp filtering" - type: string - required: false - in: query name: pageNum - description: "Page number to skip to." type: number required: false + description: "Page number (100 results each)." - in: query name: pageSize - description: "# Records to limit" type: number required: false + description: "Number of results per page (if 'pageNum' is specified)." - in: query - name: projectLegislation - description: "Legislation year of the project" - type: string + name: fields + description: "Topic fields to return." required: false + type: array + collectionFormat: pipes + items: + type: string + enum: *topicFields - in: query name: sortBy - description: "Fields to sort by." + description: "Topic fields to sort by." required: false type: array items: type: string - enum: *auditSortByFields + enum: *topicSortByFields responses: "200": description: "Success" schema: - $ref: "#/definitions/AuditObject" + $ref: "#/definitions/TopicObject" "403": description: "Access Denied" schema: $ref: "#/definitions/Error" - -### Topic Routes - /topic: + /topic/{topicId}: x-swagger-router-controller: topic options: tags: - topic summary: "Pre-flight request" operationId: protectedOptions - description: "Options on Topic Route" - responses: - "200": - description: "Success" - schema: - $ref: "#/definitions/TopicObject" - "403": - description: "Access Denied" - schema: - $ref: "#/definitions/Error" - post: - tags: - - topic - summary: "Create a new topic" - operationId: protectedPost - description: "Create a new topic object." - security: - - Bearer: [] - x-security-scopes: - - staff - - sysadmin - parameters: - - name: topic - in: body - required: true - schema: - $ref: "#/definitions/TopicId" - responses: - "200": - description: "Success" - schema: - $ref: "#/definitions/TopicObject" - "403": - description: "Access Denied" - schema: - $ref: "#/definitions/Error" - get: - tags: - - topic - summary: "Get a list of topics" - operationId: protectedGet - description: "Authenticated access to retrieving topics." - security: - - Bearer: [] - x-security-scopes: - - project-system-admin - parameters: - - in: query - name: pageNum - type: number - required: false - description: "Page number (100 results each)." - - in: query - name: pageSize - type: number - required: false - description: "Number of results per page (if 'pageNum' is specified)." - - in: query - name: fields - description: "Topic fields to return." - required: false - type: array - collectionFormat: pipes - items: - type: string - enum: *topicFields - - in: query - name: sortBy - description: "Topic fields to sort by." - required: false - type: array - items: - type: string - enum: *topicSortByFields - responses: - "200": - description: "Success" - schema: - $ref: "#/definitions/TopicObject" - "403": - description: "Access Denied" - schema: - $ref: "#/definitions/Error" - /topic/{topicId}: - x-swagger-router-controller: topic - options: - tags: - - topic - summary: "Pre-flight request" - operationId: protectedOptions - description: "Options on topic GET Route" - parameters: - - name: topicId - in: path - description: "ID of topic to get" - required: true - type: string + description: "Options on topic GET Route" + parameters: + - name: topicId + in: path + description: "ID of topic to get" + required: true + type: string responses: "200": description: "Success" @@ -6944,63 +6345,6 @@ paths: description: "Access Denied" schema: $ref: "#/definitions/Error" - put: - tags: - - topic - summary: "Update/Upload a topic" - operationId: protectedPut - description: "Update a topic object." - security: - - Bearer: [] - x-security-scopes: - - project-system-admin - consumes: - - application/json - parameters: - - name: topicId - in: path - description: "ID of topic to update" - required: true - type: string - - name: cp - in: body - required: true - schema: - $ref: "#/definitions/TopicObject" - responses: - "200": - description: "Success" - schema: - $ref: "#/definitions/TopicObject" - "403": - description: "Access Denied" - schema: - $ref: "#/definitions/Error" - delete: - tags: - - topic - summary: "Delete an topic" - operationId: protectedDelete - description: "Delete an topic object." - security: - - Bearer: [] - x-security-scopes: - - project-system-admin - parameters: - - name: topicId - in: path - description: "ID of topic to delete" - required: true - type: string - responses: - "200": - description: "Success" - schema: - $ref: "#/definitions/TopicObject" - "403": - description: "Access Denied" - schema: - $ref: "#/definitions/Error" ### Vc Routes /vc: @@ -7020,31 +6364,6 @@ paths: description: "Access Denied" schema: $ref: "#/definitions/Error" - post: - tags: - - vc - summary: "Create a new vc" - operationId: protectedPost - description: "Create a new vc object." - security: - - Bearer: [] - x-security-scopes: - - project-system-admin - parameters: - - name: vc - in: body - required: true - schema: - $ref: "#/definitions/VcId" - responses: - "200": - description: "Success" - schema: - $ref: "#/definitions/VcObject" - "403": - description: "Access Denied" - schema: - $ref: "#/definitions/Error" get: tags: - vc @@ -7154,112 +6473,7 @@ paths: description: "Access Denied" schema: $ref: "#/definitions/Error" - put: - tags: - - vc - summary: "Update/Upload a vc" - operationId: protectedPut - description: "Update a vc object." - security: - - Bearer: [] - x-security-scopes: - - project-system-admin - consumes: - - application/json - parameters: - - name: vcId - in: path - description: "ID of vc to update" - required: true - type: string - - name: cp - in: body - required: true - schema: - $ref: "#/definitions/VcObject" - responses: - "200": - description: "Success" - schema: - $ref: "#/definitions/VcObject" - "403": - description: "Access Denied" - schema: - $ref: "#/definitions/Error" - delete: - tags: - - vc - summary: "Delete an vc" - operationId: protectedDelete - description: "Delete an vc object." - security: - - Bearer: [] - x-security-scopes: - - project-system-admin - parameters: - - name: vcId - in: path - description: "ID of vc to delete" - required: true - type: string - responses: - "200": - description: "Success" - schema: - $ref: "#/definitions/VcObject" - "403": - description: "Access Denied" - schema: - $ref: "#/definitions/Error" ### Task Routes - /materialized-views/refresh: - x-swagger-router-controller: materialized-view - options: - tags: - - materialized-views - summary: 'Pre-flight request' - operationId: protectedOptions - description: 'Options on materialized-views refresh route' - responses: - '200': - description: 'Success' - post: - tags: - - materialized-views - summary: 'Trigger a materialized view refresh' - operationId: protectedRefresh - description: 'Authenticated access to manually trigger a mat view refresh run' - security: - - Bearer: [] - x-security-scopes: - - sysadmin - parameters: - - name: body - in: body - required: false - schema: - type: object - properties: - subset: - type: string - enum: - - hot - - cold - - all - default: all - force: - type: boolean - default: false - description: 'Force-release any existing lock before starting (use to recover from stale locks)' - responses: - '202': - description: 'Refresh started' - '409': - description: 'Refresh already in progress' - '403': - description: 'Access Denied' - schema: - $ref: '#/definitions/Error' /materialized-views/status: x-swagger-router-controller: materialized-view options: @@ -7287,46 +6501,6 @@ paths: description: 'Access Denied' schema: $ref: '#/definitions/Error' - /task: - x-swagger-router-controller: import-task - options: - tags: - - task - summary: 'Pre-flight request' - operationId: protectedOptions - description: 'Options on authenticated task route' - responses: - '200': - description: 'Success' - '403': - description: 'Access Denied' - schema: - $ref: '#/definitions/Error' - post: - tags: - - task - summary: 'Create a new Task' - operationId: protectedCreateTask - description: 'Authenticated access to create a Task Worker' - security: - - Bearer: [] - x-security-scopes: - - sysadmin - - project-system-admin - parameters: - - name: task - in: body - description: 'Flag to import records' - required: true - schema: - $ref: '#/definitions/TaskObject' - responses: - '200': - description: 'Success' - '403': - description: 'Access Denied' - schema: - $ref: '#/definitions/Error' /reports: x-swagger-router-controller: report options: @@ -7385,50 +6559,6 @@ paths: responses: "200": description: "Success" - post: - tags: - - job - summary: "Queue a background job" - operationId: createJob - description: "Queue a background job. Sysadmin only. Returns 202 with jobId for polling." - security: - - Bearer: [] - x-security-scopes: - - sysadmin - parameters: - - name: body - in: body - required: true - schema: - type: object - required: [type, projectId] - properties: - type: - type: string - enum: [project-doc-export] - description: "Job type" - projectId: - type: string - description: "MongoDB ObjectId of the EPIC project" - includeAll: - type: boolean - description: "Include non-Published documents (default: false)" - responses: - "202": - description: "Job queued" - schema: - type: object - properties: - jobId: - type: string - "400": - description: "Bad Request" - schema: - $ref: "#/definitions/Error" - "403": - description: "Access Denied" - schema: - $ref: "#/definitions/Error" get: tags: - job @@ -7480,7 +6610,7 @@ paths: in: path required: true type: string - description: "Job ID returned by POST /api/jobs" + description: "Job ID" responses: "200": description: "Job status" @@ -7494,32 +6624,6 @@ paths: description: "Access Denied" schema: $ref: "#/definitions/Error" - delete: - tags: - - job - summary: "Cancel or delete a job" - operationId: cancelJob - description: "Cancels a queued job or removes a completed/failed job record." - security: - - Bearer: [] - x-security-scopes: - - sysadmin - parameters: - - name: jobId - in: path - required: true - type: string - responses: - "204": - description: "Deleted" - "404": - description: "Not found" - schema: - $ref: "#/definitions/Error" - "403": - description: "Access Denied" - schema: - $ref: "#/definitions/Error" /jobs/{jobId}/download: x-swagger-router-controller: job options: diff --git a/api/tasks/import-task.js b/api/tasks/import-task.js deleted file mode 100644 index 77669b0d..00000000 --- a/api/tasks/import-task.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; - -const defaultLog = require('winston').loggers.get('default'); -const Actions = require('../helpers/actions'); - -const { updateAllMaterializedViews, updateHotViews, updateColdViews } = require('../materialized_views/updateViews'); - -exports.protectedOptions = async function (args, res) { - res.status(200).send(); -}; - -/*Required fields for type of task: -updateMaterializedView: -{ - taskType: updateMaterializedView -}*/ -exports.protectedCreateTask = async function (args, res) { - // validate request parameters - if (!args.swagger.params.task || !args.swagger.params.task.value) { - defaultLog.error('protectedCreateTask - missing required request body'); - return Actions.sendResponse(res, 400, 'protectedCreateTask - missing required request body'); - } - if (!args.swagger.params.task.value.taskType) { - defaultLog.error('protectedCreateTask - missing required taskType'); - return Actions.sendResponse(res, 400, 'protectedCreateTask - missing required taskType'); - } - - switch (args.swagger.params.task.value.taskType) { - case 'updateMaterializedView': - switch (args.swagger.params.task.value.materializedViewSubset) { - case 'default': - updateAllMaterializedViews(defaultLog); - break; - case 'hot': - updateHotViews(defaultLog); - break; - case 'cold': - updateColdViews(defaultLog); - break; - default: - defaultLog.error(`protectedCreateTask - unknown materialized view subset`); - return Actions.sendResponse(res, 400, `protectedCreateTask - unknown materialized view subset`); - } - break; - default: - defaultLog.error('protectedCreateTask - unknown taskType'); - return Actions.sendResponse(res, 400, 'protectedCreateTask - unknown taskType'); - } - // send response immediately as the tasks will run in the background - return Actions.sendResponse(res, 200); -}; diff --git a/app.js b/app.js index c7bf0b65..ca1682b1 100644 --- a/app.js +++ b/app.js @@ -173,11 +173,7 @@ app.use(projectRedirectMiddleware()); // Auto-wired API router — reads swagger.yaml and registers one Express route per // operation, populates req.swagger.params, enforces Bearer auth, and calls the // matching controller function. -var controllerDirs = [ - path.join(__dirname, 'api/controllers'), - path.join(__dirname, 'api/tasks') -]; -app.use('/api', createRouter(swaggerSpec, controllerDirs)); +app.use('/api', createRouter(swaggerSpec, [path.join(__dirname, 'api/controllers')])); app.use(require('./api/middleware/errorHandler')); diff --git a/test/controllers/cac.test.js b/test/controllers/cac.test.js index f4cc6c1c..82eb9bd2 100644 --- a/test/controllers/cac.test.js +++ b/test/controllers/cac.test.js @@ -42,7 +42,6 @@ describe('CAC Controller', () => { MockCACUser.findOne = sinon.stub(); MockCACUser.deleteOne = sinon.stub().resolves(); - MockCACUser.deleteMany = sinon.stub().resolves(); return MockCACUser; } @@ -69,7 +68,6 @@ describe('CAC Controller', () => { res = { status: sinon.stub().returnsThis(), json: sinon.stub() }; projectModel = { - findOne: sinon.stub(), updateOne: sinon.stub(), findOneAndUpdate: sinon.stub() }; @@ -100,71 +98,6 @@ describe('CAC Controller', () => { }); }); - // --------------------------------------------------------------------------- - describe('protectedPublishCAC', () => { - it('returns 400 for invalid projId', async () => { - await cac.protectedPublishCAC(makeArgs({ projId: { value: 'bad' } }), res); - expect(res.status.calledWith(400)).to.be.true; - }); - - it('returns 404 when project is not found', async () => { - projectModel.findOne.resolves(null); - await cac.protectedPublishCAC(makeArgs(), res); - expect(res.status.calledWith(404)).to.be.true; - }); - - it('returns 200 and records Publish action on success', async () => { - projectModel.findOne.resolves({ _id: VALID_PROJ_ID }); - projectModel.updateOne.resolves({ modifiedCount: 1 }); - await cac.protectedPublishCAC(makeArgs(), res); - expect(res.status.calledWith(200)).to.be.true; - expect(Utils.recordAction.calledWith('Publish', 'CAC', 'testuser', VALID_PROJ_ID)).to.be.true; - }); - - it('returns 400 and logs error when DB throws (regression: catch had no log)', async () => { - projectModel.findOne.rejects(new Error('timeout')); - await cac.protectedPublishCAC(makeArgs(), res); - expect(res.status.calledWith(400)).to.be.true; - expect(defaultLog.error.called).to.be.true; - }); - }); - - // --------------------------------------------------------------------------- - describe('protectedUnPublishCAC', () => { - it('returns 400 for invalid projId', async () => { - await cac.protectedUnPublishCAC(makeArgs({ projId: { value: 'bad' } }), res); - expect(res.status.calledWith(400)).to.be.true; - }); - - it('returns 404 when project is not found', async () => { - projectModel.findOne.resolves(null); - await cac.protectedUnPublishCAC(makeArgs(), res); - expect(res.status.calledWith(404)).to.be.true; - }); - - it('records Unpublish — NOT Publish — action (regression guard)', async () => { - projectModel.findOne.resolves({ _id: VALID_PROJ_ID }); - projectModel.updateOne.resolves({ modifiedCount: 1 }); - await cac.protectedUnPublishCAC(makeArgs(), res); - expect(Utils.recordAction.calledWith('Unpublish', 'CAC')).to.be.true; - expect(Utils.recordAction.calledWith('Publish', 'CAC')).to.be.false; - }); - - it('returns 200 on success', async () => { - projectModel.findOne.resolves({ _id: VALID_PROJ_ID }); - projectModel.updateOne.resolves({ modifiedCount: 1 }); - await cac.protectedUnPublishCAC(makeArgs(), res); - expect(res.status.calledWith(200)).to.be.true; - }); - - it('returns 400 and logs error when DB throws (regression: catch had no log)', async () => { - projectModel.findOne.rejects(new Error('network error')); - await cac.protectedUnPublishCAC(makeArgs(), res); - expect(res.status.calledWith(400)).to.be.true; - expect(defaultLog.error.called).to.be.true; - }); - }); - // --------------------------------------------------------------------------- describe('publicCACSignUp', () => { it('returns 400 for invalid projId', async () => { @@ -254,90 +187,4 @@ describe('CAC Controller', () => { expect(defaultLog.error.called).to.be.true; }); }); - - // --------------------------------------------------------------------------- - describe('protectedCACRemoveMember', () => { - it('returns 400 for invalid projId', async () => { - await cac.protectedCACRemoveMember(makeArgs({ projId: { value: 'bad' } }), res); - expect(res.status.calledWith(400)).to.be.true; - }); - - it('returns 200 and records Delete action on success', async () => { - projectModel.updateOne.resolves({ modifiedCount: 1 }); - await cac.protectedCACRemoveMember(makeArgs(), res); - expect(res.status.calledWith(200)).to.be.true; - expect(Utils.recordAction.calledWith('Delete', 'CACMemberFromProject', 'testuser')).to.be.true; - }); - - it('returns 400 and logs error when DB throws', async () => { - projectModel.updateOne.rejects(new Error('write error')); - await cac.protectedCACRemoveMember(makeArgs(), res); - expect(res.status.calledWith(400)).to.be.true; - expect(defaultLog.error.called).to.be.true; - }); - }); - - // --------------------------------------------------------------------------- - describe('protectedCreateCAC', () => { - it('returns 400 for invalid projId', async () => { - await cac.protectedCreateCAC(makeArgs({ projId: { value: 'bad' } }), res); - expect(res.status.calledWith(400)).to.be.true; - }); - - it('returns 400 when no matching project was updated', async () => { - projectModel.updateOne.resolves({ modifiedCount: 0 }); - await cac.protectedCreateCAC(makeArgs(), res); - expect(res.status.calledWith(400)).to.be.true; - }); - - it('returns 201 and records Post action on success', async () => { - projectModel.updateOne.resolves({ modifiedCount: 1 }); - await cac.protectedCreateCAC(makeArgs(), res); - expect(res.status.calledWith(201)).to.be.true; - expect(Utils.recordAction.calledWith('Post', 'Add Project CAC', 'testuser', VALID_PROJ_ID)).to.be.true; - }); - - it('returns 400 and logs error when DB throws (regression: was "Couldn\'t find that object!")', async () => { - projectModel.updateOne.rejects(new Error('write conflict')); - await cac.protectedCreateCAC(makeArgs(), res); - expect(res.status.calledWith(400)).to.be.true; - expect(defaultLog.error.called).to.be.true; - }); - }); - - // --------------------------------------------------------------------------- - describe('protectedCACDelete', () => { - it('returns 400 for invalid projId', async () => { - await cac.protectedCACDelete(makeArgs({ projId: { value: 'bad' } }), res); - expect(res.status.calledWith(400)).to.be.true; - }); - - it('returns 404 when project is not found', async () => { - projectModel.findOne.resolves(null); - await cac.protectedCACDelete(makeArgs(), res); - expect(res.status.calledWith(404)).to.be.true; - }); - - it('deletes all CAC users before updating project', async () => { - projectModel.findOne.resolves({ _id: VALID_PROJ_ID, cacMembers: [VALID_MEMBER_ID] }); - projectModel.updateOne.resolves({ modifiedCount: 1 }); - await cac.protectedCACDelete(makeArgs(), res); - expect(cacUserModel.deleteMany.calledOnce).to.be.true; - }); - - it('returns 200 and records Remove action on success', async () => { - projectModel.findOne.resolves({ _id: VALID_PROJ_ID, cacMembers: [VALID_MEMBER_ID] }); - projectModel.updateOne.resolves({ modifiedCount: 1 }); - await cac.protectedCACDelete(makeArgs(), res); - expect(res.status.calledWith(200)).to.be.true; - expect(Utils.recordAction.calledWith('Post', 'Remove Project CAC', 'testuser', VALID_PROJ_ID)).to.be.true; - }); - - it('returns 400 and logs error when DB throws (regression: was "Couldn\'t find that object!")', async () => { - projectModel.findOne.rejects(new Error('network error')); - await cac.protectedCACDelete(makeArgs(), res); - expect(res.status.calledWith(400)).to.be.true; - expect(defaultLog.error.called).to.be.true; - }); - }); }); diff --git a/test/controllers/demi-push-callsites.test.js b/test/controllers/demi-push-callsites.test.js index ac574145..59cf4508 100644 --- a/test/controllers/demi-push-callsites.test.js +++ b/test/controllers/demi-push-callsites.test.js @@ -22,11 +22,10 @@ const organizationController = require('../../api/controllers/organization'); const projectNotificationController = require('../../api/controllers/projectNotification'); const userController = require('../../api/controllers/user'); const projectGroupController = require('../../api/controllers/projectGroup'); -const inspectionController = require('../../api/controllers/inspection'); const { - OID, upfile, stubModel, setEnv, - docArgs, projArgs, pinArgs, raArgs, cpArgs, commentArgs, orgArgs, pnArgs, userArgs, groupArgs, inspArgs, itemArgs + OID, stubModel, setEnv, + docArgs, projArgs, pinArgs, raArgs, cpArgs, commentArgs, orgArgs, pnArgs, userArgs, groupArgs } = require('../support/demiPushHarness'); describe('DEMI push call sites', () => { @@ -261,9 +260,8 @@ describe('DEMI push call sites', () => { // Opt-in kinds. Each stub resolves true: the handler answers once its push lands. describe('opt-in kinds', () => { - const NEW_KINDS = ['user', 'group', 'inspection', 'inspectionElement', 'inspectionItem', 'usersOfOrganization']; + const NEW_KINDS = ['user', 'group', 'usersOfOrganization']; const byId = { _id: OID }; - let responded; // Group handlers push onto the ACL arrays a mongoose document starts with. function withAcl(M) { @@ -276,16 +274,7 @@ describe('DEMI push call sites', () => { beforeEach(() => { NEW_KINDS.forEach(kind => sinon.stub(demiPush, kind).resolves(true)); models.Group = withAcl(model('Group')); - ['Inspection', 'InspectionElement', 'InspectionItem'].forEach(name => { - models[name] = model(name); - // The mobile app's id is new, so every post saves rather than handing back a stored row. - models[name].findOne = sinon.stub().resolves(null); - models[name].updateOne = sinon.stub().resolves({ matchedCount: 1 }); - }); - models.InspectionItem.prototype.markModified = () => {}; models.Group.updateOne = sinon.stub().resolves({ matchedCount: 1 }); - // Inspection handlers answer from a promise chain they do not return. - responded = new Promise(resolve => res.json.callsFake(resolve)); }); [ @@ -349,56 +338,6 @@ describe('DEMI push call sites', () => { }); }); - it('inspection.protectedPostInspection answers 200 once its push of the saved inspection lands', async () => { - await inspectionController.protectedPostInspection(inspArgs(), res); - await responded; - - expect(res.status.args, `expected 200, got ${JSON.stringify(res.status.args)}`).to.deep.equal([[200]]); - expect(demiPush.inspection.calledOnceWithExactly(saved)).to.be.true; - }); - - it('inspection.protectedPostElement pushes the saved element and its inspection by id', async () => { - await inspectionController.protectedPostElement(inspArgs(), res); - await responded; - - expect(res.status.args, `expected 200, got ${JSON.stringify(res.status.args)}`).to.deep.equal([[200]]); - expect(demiPush.inspectionElement.calledOnceWithExactly(saved)).to.be.true; - expect(demiPush.inspection.calledOnceWithExactly(byId)).to.be.true; - }); - - it('inspection.protectedPostElement does not push the inspection when no inspection matched', async () => { - models.Inspection.updateOne.resolves({ matchedCount: 0 }); - - await inspectionController.protectedPostElement(inspArgs(), res); - await responded; - - expect(res.status.args, `expected 200, got ${JSON.stringify(res.status.args)}`).to.deep.equal([[200]]); - expect(demiPush.inspectionElement.calledOnceWithExactly(saved)).to.be.true; - expect(demiPush.inspection.called).to.be.false; - }); - - it('inspection.protectedPostElementItem does not push the element when no element matched', async () => { - models.InspectionElement.updateOne.resolves({ matchedCount: 0 }); - - await inspectionController.protectedPostElementItem(itemArgs(null), res); - await responded; - - expect(res.status.args, `expected 200, got ${JSON.stringify(res.status.args)}`).to.deep.equal([[200]]); - expect(demiPush.inspectionItem.calledOnceWithExactly(saved)).to.be.true; - expect(demiPush.inspectionElement.called).to.be.false; - }); - - [['text', null], ['file', upfile]].forEach(([label, file]) => { - it(`inspection.protectedPostElementItem (${label}) pushes the saved item and its element by id`, async () => { - await inspectionController.protectedPostElementItem(itemArgs(file), res); - await responded; - - expect(res.status.args, `expected 200, got ${JSON.stringify(res.status.args)}`).to.deep.equal([[200]]); - expect(demiPush.inspectionItem.calledOnceWithExactly(saved)).to.be.true; - expect(demiPush.inspectionElement.calledOnceWithExactly(byId)).to.be.true; - }); - }); - describe('with DEMI configured', () => { let restoreEnv; diff --git a/test/controllers/recentActivity.test.js b/test/controllers/recentActivity.test.js index e3ccad35..498028b4 100644 --- a/test/controllers/recentActivity.test.js +++ b/test/controllers/recentActivity.test.js @@ -219,17 +219,6 @@ describe('RecentActivity Controller - DEMI mirror', () => { expect(pushStub.called).to.be.false; expect(res.status.calledWith(404)).to.be.true; }); - - it('refuses to delete the whole collection', async () => { - const args = deleteArgs(); - delete args.swagger.params.recentActivityId; - - await recentActivity.protectedDelete(args, res); - - expect(model.findOneAndUpdate.called).to.be.false; - expect(pushStub.called).to.be.false; - expect(res.status.calledWith(400)).to.be.true; - }); }); describe('field checks', () => { diff --git a/test/helpers/anonymousRouting.test.js b/test/helpers/anonymousRouting.test.js index 1d01c094..0e044608 100644 --- a/test/helpers/anonymousRouting.test.js +++ b/test/helpers/anonymousRouting.test.js @@ -17,8 +17,6 @@ const YAML = require('js-yaml'); const createRouter = require('../../api/middleware/swagger-router'); -// /audit is deliberately absent: its controller is not loaded, so the router never registers the -// path and it answers 404 rather than 403. const CLOSED = [ '/vc', '/topic', @@ -36,10 +34,7 @@ describe('anonymous routing through the real router', () => { fs.readFileSync(path.join(__dirname, '../../api/swagger/swagger.yaml'), 'utf8') ); app = express(); - app.use('/api', createRouter(spec, [ - path.join(__dirname, '../../api/controllers'), - path.join(__dirname, '../../api/tasks') - ])); + app.use('/api', createRouter(spec, [path.join(__dirname, '../../api/controllers')])); }); CLOSED.forEach((route) => { @@ -50,7 +45,7 @@ describe('anonymous routing through the real router', () => { }); it('refuses a write without a token', async () => { - const res = await request(app).post('/api/vc').send({}); + const res = await request(app).post('/api/project').send({}); expect(res.status).to.equal(403); }); }); diff --git a/test/smoke/protected-projects.test.js b/test/smoke/protected-projects.test.js index e23aef1f..6bff38d1 100644 --- a/test/smoke/protected-projects.test.js +++ b/test/smoke/protected-projects.test.js @@ -45,17 +45,6 @@ describe('PROTECTED /api/project (requires token)', () => { const res = await authGet('/search').query({ dataset: 'Project', pageNum: 0, pageSize: 5 }).expect(200); expect(res.body).to.be.an('array'); }); - - it('GET /audit — audit log (sysadmin)', async function () { - if (!hasToken()) return this.skip(); - const res = await authGet('/audit').query({ pageNum: 0, pageSize: 5 }); - // 404 = audit controller not deployed; 500 = not yet implemented; 403 = insufficient scope; 200 = success - expect(res.status).to.be.oneOf([200, 403, 404, 500]); - if (res.status === 200) { - expect(res.body).to.be.an('array'); - } - }); - }); // No credential needed, and none required to reach them: these assert what an diff --git a/test/smoke/write-auth-gates.test.js b/test/smoke/write-auth-gates.test.js index 98f35a95..87ddcfa2 100644 --- a/test/smoke/write-auth-gates.test.js +++ b/test/smoke/write-auth-gates.test.js @@ -24,8 +24,6 @@ const unauth = (method, path, body) => } }); - - describe('WRITE ENDPOINT AUTH GATES (no token — all expect 401 or 403)', () => { describe('Project write endpoints', () => { it('POST /project — requires auth', async () => { @@ -55,21 +53,6 @@ describe('WRITE ENDPOINT AUTH GATES (no token — all expect 401 or 403)', () => it('PUT /project/:projId/pin/unpublish — requires auth', async () => { await unauth('put', '/project/000000000000000000000000/pin/unpublish'); }); - it('POST /project/:projId/cac — requires auth', async () => { - await unauth('post', '/project/000000000000000000000000/cac'); - }); - it('PUT /project/:projId/cac — requires auth', async () => { - await unauth('put', '/project/000000000000000000000000/cac'); - }); - it('DELETE /project/:projId/cac — requires auth', async () => { - await unauth('delete', '/project/000000000000000000000000/cac'); - }); - it('PUT /project/:projId/cac/publish — requires auth', async () => { - await unauth('put', '/project/000000000000000000000000/cac/publish'); - }); - it('PUT /project/:projId/cac/unpublish — requires auth', async () => { - await unauth('put', '/project/000000000000000000000000/cac/unpublish'); - }); it('POST /project/:projId/group — requires auth', async () => { await unauth('post', '/project/000000000000000000000000/group'); }); @@ -163,9 +146,6 @@ describe('WRITE ENDPOINT AUTH GATES (no token — all expect 401 or 403)', () => it('DELETE /recentActivity/:id — requires auth', async () => { await unauth('delete', '/recentActivity/000000000000000000000000'); }); - it('DELETE /recentActivity — requires auth (applicationID param)', async () => { - await unauth('delete', '/recentActivity').query({ applicationID: '000000000000000000000000' }); - }); }); describe('User write endpoints', () => { @@ -177,30 +157,6 @@ describe('WRITE ENDPOINT AUTH GATES (no token — all expect 401 or 403)', () => }); }); - describe('Topic write endpoints', () => { - it('POST /topic — requires auth', async () => { - await unauth('post', '/topic'); - }); - it('PUT /topic/:topicId — requires auth', async () => { - await unauth('put', '/topic/000000000000000000000000'); - }); - it('DELETE /topic/:topicId — requires auth', async () => { - await unauth('delete', '/topic/000000000000000000000000'); - }); - }); - - describe('Valued component write endpoints', () => { - it('POST /vc — requires auth', async () => { - await unauth('post', '/vc'); - }); - it('PUT /vc/:vcId — requires auth', async () => { - await unauth('put', '/vc/000000000000000000000000'); - }); - it('DELETE /vc/:vcId — requires auth', async () => { - await unauth('delete', '/vc/000000000000000000000000'); - }); - }); - describe('Project notification write endpoints', () => { it('POST /projectNotification — requires auth', async () => { await unauth('post', '/projectNotification'); @@ -210,21 +166,6 @@ describe('WRITE ENDPOINT AUTH GATES (no token — all expect 401 or 403)', () => }); }); - describe('Task write endpoints', () => { - it('POST /task — requires auth', async () => { - await unauth('post', '/task'); - }); - }); - - describe('Inspection write endpoints', () => { - it('POST /inspection — requires auth', async () => { - await unauth('post', '/inspection'); - }); - it('POST /inspection/:id/element — requires auth', async () => { - await unauth('post', '/inspection/000000000000000000000000/element'); - }); - }); - describe('V2 Project write endpoints', () => { it('POST /v2/Projects — requires auth', async () => { await unauth('post', '/v2/Projects'); diff --git a/test/support/demiPushHarness.js b/test/support/demiPushHarness.js index 173cfb4f..bcdc535d 100644 --- a/test/support/demiPushHarness.js +++ b/test/support/demiPushHarness.js @@ -86,19 +86,6 @@ const groupArgs = () => ({ } }); -const inspArgs = () => ({ - swagger: { params: { inspId: p(OID), inspection: p({ inspectionId: 'mobile-1', project: OID }), auth_payload: auth } } -}); - -const itemArgs = file => ({ - swagger: { - params: { - upfile: p(file), itemId: p(OID), project: p(OID), elementId: p(OID), type: p('photo'), - timestamp: p(null), caption: p('c'), text: p('"note"'), geo: p('[1,2]'), auth_payload: auth - } - } -}); - /** * A mongoose model stand-in. `saved()` is read per call, so a spec can swap the write result in its * beforeEach and every save and findOneAndUpdate hands that back. @@ -150,5 +137,5 @@ function setEnv(vars) { module.exports = { OID, p, auth, upfile, stubModel, setEnv, - docArgs, projArgs, pinArgs, raArgs, cpArgs, commentArgs, orgArgs, pnArgs, userArgs, groupArgs, inspArgs, itemArgs + docArgs, projArgs, pinArgs, raArgs, cpArgs, commentArgs, orgArgs, pnArgs, userArgs, groupArgs }; From 026943ec577c595a3d8e4c91b667e3f6399430ed Mon Sep 17 00:00:00 2001 From: Daniel Truong Date: Tue, 13 Oct 2026 18:11:27 +0000 Subject: [PATCH 2/2] chore: note jobs feature removal in TODO --- TODO.md | 1 + 1 file changed, 1 insertion(+) diff --git a/TODO.md b/TODO.md index 233ba052..09334dc0 100644 --- a/TODO.md +++ b/TODO.md @@ -28,3 +28,4 @@ - 2026-10-01 scripts/demi-repush.js:62: `--since` misses projects changed by PUT. - 2026-10-01 api/materialized_views/reports/documentTaggingProgressByProject.js:156: report views still read the root `$name` (also projectStatsFull.js:19, projectsWithCompletelyTaggedDocs.js:12 and 150, whoPublishedUnpublishedAllUsers.js:112). - 2026-09-25: no test covers the `pushClient.logUnsent()` call in `app.js` `shutdown()`. Needs an `app.js` change first: `exports.shutdown` at `app.js:246` is unreachable because `module.exports = app` replaces the exports object. +- 2026-10-13 eagle-api: remove the jobs feature: Agenda queue (api/helpers/jobQueue.js, startJobQueue app.js:194), GET /jobs routes, project-doc-export handler, job.js CSV code. Nothing queues a job since #945; the queue polls Mongo every 5 s per pod. Delete wiki Async-Jobs.md too