2026-04-07 16:20:24 -06:00
// routes/ivantiFpWorkflow.js
const express = require ( 'express' ) ;
const multer = require ( 'multer' ) ;
const path = require ( 'path' ) ;
2026-04-15 15:27:21 -06:00
const fs = require ( 'fs' ) ;
2026-05-06 11:44:17 -06:00
const pool = require ( '../db' ) ;
const { requireAuth , requireGroup } = require ( '../middleware/auth' ) ;
2026-04-13 14:31:36 -06:00
const { ivantiFormPost , ivantiPost } = require ( '../helpers/ivantiApi' ) ;
2026-04-07 16:20:24 -06:00
const logAudit = require ( '../helpers/auditLog' ) ;
// ---------------------------------------------------------------------------
// Pure helpers (exported for testing)
// ---------------------------------------------------------------------------
const ALLOWED _EXTENSIONS = new Set ( [
'.pdf' , '.png' , '.jpg' , '.jpeg' , '.gif' ,
'.doc' , '.docx' , '.xlsx' , '.csv' , '.txt' , '.zip'
] ) ;
function isAllowedFileExtension ( filename ) {
if ( ! filename || typeof filename !== 'string' ) return false ;
const ext = path . extname ( filename ) . toLowerCase ( ) ;
return ALLOWED _EXTENSIONS . has ( ext ) ;
}
function validateFpWorkflowForm ( body ) {
const errors = { } ;
if ( ! body . name || typeof body . name !== 'string' || body . name . trim ( ) . length === 0 ) {
errors . name = 'Workflow name is required.' ;
} else if ( body . name . trim ( ) . length > 255 ) {
errors . name = 'Workflow name must be 255 characters or fewer.' ;
}
if ( ! body . reason || typeof body . reason !== 'string' || body . reason . trim ( ) . length === 0 ) {
errors . reason = 'Reason is required.' ;
}
if ( body . description !== undefined && body . description !== null && body . description !== '' ) {
2026-05-06 11:44:17 -06:00
if ( typeof body . description !== 'string' ) errors . description = 'Description must be a string.' ;
else if ( body . description . length > 2000 ) errors . description = 'Description must be 2000 characters or fewer.' ;
2026-04-07 16:20:24 -06:00
}
if ( ! body . expirationDate || typeof body . expirationDate !== 'string' || body . expirationDate . trim ( ) . length === 0 ) {
errors . expirationDate = 'Expiration date is required.' ;
} else {
const parsed = new Date ( body . expirationDate ) ;
if ( isNaN ( parsed . getTime ( ) ) ) {
errors . expirationDate = 'Expiration date must be a valid date.' ;
} else {
2026-05-06 11:44:17 -06:00
const today = new Date ( ) ; today . setHours ( 0 , 0 , 0 , 0 ) ;
const expDay = new Date ( parsed ) ; expDay . setHours ( 0 , 0 , 0 , 0 ) ;
if ( expDay <= today ) errors . expirationDate = 'Expiration date must be in the future.' ;
else { const maxDate = new Date ( today ) ; maxDate . setDate ( maxDate . getDate ( ) + 120 ) ; if ( expDay > maxDate ) errors . expirationDate = 'Expiration date cannot be more than 120 days from today.' ; }
2026-04-07 16:20:24 -06:00
}
}
return errors ;
}
2026-04-08 10:18:45 -06:00
function buildSubjectFilterRequest ( findingIds ) {
2026-05-06 11:44:17 -06:00
return JSON . stringify ( { subject : 'hostFinding' , filterRequest : { filters : [ { field : 'id' , exclusive : false , operator : 'IN' , value : findingIds . map ( id => String ( id ) ) . join ( ',' ) } ] } } ) ;
2026-04-08 10:18:45 -06:00
}
function buildIvantiFormFields ( formData , findingIds ) {
2026-05-06 11:44:17 -06:00
const scopeMap = { 'Authorized' : 'AUTHORIZED' , 'None' : 'NONE' , 'Automated' : 'AUTOMATED' } ;
2026-04-08 10:18:45 -06:00
return [
{ name : 'name' , value : formData . name } ,
{ name : 'reason' , value : formData . reason } ,
{ name : 'description' , value : formData . description || '' } ,
{ name : 'expirationDate' , value : formData . expirationDate } ,
{ name : 'overrideControl' , value : scopeMap [ formData . scopeOverride ] || 'AUTHORIZED' } ,
{ name : 'subjectFilterRequest' , value : buildSubjectFilterRequest ( findingIds ) } ,
{ name : 'isEmptyWorkflow' , value : findingIds . length === 0 ? 'true' : 'false' }
] ;
2026-04-07 16:20:24 -06:00
}
2026-05-06 11:44:17 -06:00
const LIFECYCLE _STATUSES = new Set ( [ 'submitted' , 'approved' , 'rejected' , 'rework' , 'resubmitted' ] ) ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
function validateLifecycleTransition ( currentStatus , newStatus ) {
2026-05-06 11:44:17 -06:00
if ( currentStatus === 'approved' ) return { valid : false , error : 'This submission is finalized and cannot be edited.' } ;
if ( ! LIFECYCLE _STATUSES . has ( newStatus ) ) return { valid : false , error : 'Invalid lifecycle status.' } ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
return { valid : true } ;
}
function mergeFindings ( existingJson , newIds ) {
const existing = JSON . parse ( existingJson || '[]' ) ;
const merged = [ ... new Set ( [ ... existing , ... newIds ] ) ] ;
return JSON . stringify ( merged ) ;
}
function buildSubmissionHistoryEntry ( changeType , details , userId , username ) {
2026-05-06 11:44:17 -06:00
return { user _id : userId , username : username , change _type : changeType , change _details _json : JSON . stringify ( details ) , created _at : new Date ( ) . toISOString ( ) } ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
}
2026-05-11 14:29:50 -06:00
function filterVisibleSubmissions ( submissions ) {
return submissions . filter ( s => s . lifecycle _status !== 'approved' && s . dismissed _at == null ) ;
}
function shouldShowDismissButton ( submission ) {
return submission . lifecycle _status === 'rejected' && submission . dismissed _at == null ;
}
2026-04-13 12:53:13 -06:00
// ---------------------------------------------------------------------------
2026-05-06 11:44:17 -06:00
// Resolve workflow batch UUID
2026-04-13 12:53:13 -06:00
// ---------------------------------------------------------------------------
2026-05-06 11:44:17 -06:00
async function resolveWorkflowBatchUuid ( submission , apiKey , clientId , skipTls ) {
2026-04-13 12:53:13 -06:00
if ( submission . ivanti _workflow _batch _uuid ) return submission . ivanti _workflow _batch _uuid ;
const searchUrl = ` /client/ ${ encodeURIComponent ( clientId ) } /workflowBatch/search ` ;
2026-04-13 13:16:09 -06:00
const workflowName = submission . workflow _name || '' ;
2026-05-06 11:44:17 -06:00
const searchBody = { filters : workflowName ? [ { field : 'name' , exclusive : false , operator : 'EXACT' , value : workflowName } ] : [ ] , projection : 'internal' , sort : [ { field : 'created' , direction : 'DESC' } ] , page : 0 , size : 10 } ;
2026-04-13 12:53:13 -06:00
2026-04-13 13:02:08 -06:00
let result ;
2026-05-06 11:44:17 -06:00
try { result = await ivantiPost ( searchUrl , searchBody , apiKey , skipTls ) ; } catch ( e ) { return null ; }
if ( result . status !== 200 ) return null ;
2026-04-13 12:53:13 -06:00
let uuid = null ;
try {
const data = JSON . parse ( result . body ) ;
2026-05-06 11:44:17 -06:00
let batches = data . _embedded ? . workflowBatches || data . _embedded ? . workflowBatch || data . content || data . data || ( Array . isArray ( data ) ? data : [ ] ) ;
const batchId = String ( submission . ivanti _workflow _batch _id ) ;
2026-04-13 13:16:09 -06:00
const batch = batches . find ( b => String ( b . id ) === batchId ) || batches [ 0 ] ;
2026-05-06 11:44:17 -06:00
if ( batch ) uuid = batch . uuid || batch . workflowBatchUuid || batch . batchUuid || batch . groupUuid || batch . group _uuid || batch . workflow _batch _uuid || batch . uid || null ;
} catch ( e ) { return null ; }
2026-04-13 12:53:13 -06:00
2026-04-13 13:02:08 -06:00
if ( uuid && submission . id ) {
2026-05-06 11:44:17 -06:00
pool . query ( ` UPDATE ivanti_fp_submissions SET ivanti_workflow_batch_uuid = $ 1 WHERE id = $ 2 ` , [ uuid , submission . id ] ) . catch ( ( ) => { } ) ;
2026-04-13 12:53:13 -06:00
}
return uuid ;
}
2026-04-07 16:20:24 -06:00
// ---------------------------------------------------------------------------
// Multer configuration
// ---------------------------------------------------------------------------
const uploadStorage = multer . memoryStorage ( ) ;
2026-05-06 11:44:17 -06:00
const fpUpload = multer ( { storage : uploadStorage , limits : { fileSize : 10 * 1024 * 1024 } , fileFilter : ( req , file , cb ) => { if ( isAllowedFileExtension ( file . originalname ) ) cb ( null , true ) ; else cb ( new Error ( ` File type not allowed: ${ path . extname ( file . originalname ) } ` ) ) ; } } ) . array ( 'attachments' , 10 ) ;
2026-04-07 16:20:24 -06:00
// ---------------------------------------------------------------------------
// Router factory
// ---------------------------------------------------------------------------
2026-05-06 11:44:17 -06:00
function createIvantiFpWorkflowRouter ( ) {
2026-04-07 16:20:24 -06:00
const router = express . Router ( ) ;
2026-05-13 16:46:49 -06:00
/ * *
* GET / api / ivanti / fp - workflow / documents / search
*
* Searches the documents library by name , CVE ID , or vendor .
* Returns up to 50 results ordered by upload date descending .
*
* @ query { string } q — Optional search term ; matches against name , cve _id , or vendor ( ILIKE )
* @ returns { Array < Object > } Array of document metadata objects
* - id { number }
* - cve _id { string }
* - vendor { string }
* - name { string }
* - type { string }
* - file _size { number }
* - mime _type { string }
* - uploaded _at { string }
* @ error 500 Database error
* /
2026-05-06 11:44:17 -06:00
router . get ( '/documents/search' , requireAuth ( ) , async ( req , res ) => {
2026-04-15 15:27:21 -06:00
const q = ( req . query . q || '' ) . trim ( ) ;
2026-05-06 11:44:17 -06:00
try {
let rows ;
if ( q ) {
const like = ` % ${ q } % ` ;
const result = await pool . query (
` SELECT id, cve_id, vendor, name, type, file_size, mime_type, uploaded_at FROM documents WHERE name ILIKE $ 1 OR cve_id ILIKE $ 2 OR vendor ILIKE $ 3 ORDER BY uploaded_at DESC LIMIT 50 ` ,
[ like , like , like ]
) ;
rows = result . rows ;
} else {
const result = await pool . query ( ` SELECT id, cve_id, vendor, name, type, file_size, mime_type, uploaded_at FROM documents ORDER BY uploaded_at DESC LIMIT 50 ` ) ;
rows = result . rows ;
2026-04-15 15:27:21 -06:00
}
res . json ( rows || [ ] ) ;
2026-05-06 11:44:17 -06:00
} catch ( err ) {
console . error ( 'Error searching documents:' , err ) ;
res . status ( 500 ) . json ( { error : 'Database error.' } ) ;
}
2026-04-15 15:27:21 -06:00
} ) ;
2026-05-13 16:46:49 -06:00
/ * *
* POST / api / ivanti / fp - workflow
*
* Creates a new False Positive workflow in Ivanti and records the submission locally .
* Validates queue items belong to the user , are FP type , and are pending .
* Uploads local files and library documents as attachments .
* Marks associated queue items as complete on success .
* Requires Admin or Standard _User group .
*
* @ body { Object } multipart / form - data
* - findingIds { string } JSON array of Ivanti finding IDs
* - queueItemIds { string } JSON array of todo queue item IDs
* - libraryDocIds { string } Optional JSON array of document library IDs to attach
* - name { string } Workflow name ( required , max 255 chars )
* - reason { string } Reason for false positive ( required )
* - description { string } Optional description ( max 2000 chars )
* - expirationDate { string } Required , must be future date within 120 days
* - scopeOverride { string } Optional : Authorized , None , or Automated
* - attachments { File [ ] } Optional uploaded files ( max 10 , each max 10 MB )
* @ returns { Object }
* - success { boolean }
* - workflowBatchId { number } Ivanti workflow batch ID
* - queueItemsUpdated { number } Count of queue items marked complete
* - status { string } "success"
* @ error 400 Invalid input , queue item validation failure
* @ error 403 Queue items belong to another user
* @ error 429 Ivanti API rate limit
* @ error 500 Ivanti API key not configured
* @ error 502 Ivanti API connection or response failure
* /
2026-05-06 11:44:17 -06:00
router . post ( '/' , requireAuth ( ) , requireGroup ( 'Admin' , 'Standard_User' ) , ( req , res ) => {
2026-04-07 16:20:24 -06:00
fpUpload ( req , res , ( multerErr ) => {
if ( multerErr ) {
2026-05-06 11:44:17 -06:00
if ( multerErr . code === 'LIMIT_FILE_SIZE' ) return res . status ( 400 ) . json ( { error : 'File exceeds the 10 MB size limit.' } ) ;
2026-04-07 16:20:24 -06:00
return res . status ( 400 ) . json ( { error : multerErr . message } ) ;
}
let findingIds , queueItemIds ;
2026-05-06 11:44:17 -06:00
try { findingIds = JSON . parse ( req . body . findingIds || '[]' ) ; queueItemIds = JSON . parse ( req . body . queueItemIds || '[]' ) ; }
catch ( e ) { return res . status ( 400 ) . json ( { error : 'findingIds and queueItemIds must be valid JSON arrays.' } ) ; }
2026-04-07 16:20:24 -06:00
2026-05-06 11:44:17 -06:00
if ( ! Array . isArray ( findingIds ) || findingIds . length === 0 ) return res . status ( 400 ) . json ( { error : 'At least one finding ID is required.' } ) ;
if ( ! Array . isArray ( queueItemIds ) || queueItemIds . length === 0 ) return res . status ( 400 ) . json ( { error : 'At least one queue item ID is required.' } ) ;
2026-04-07 16:20:24 -06:00
2026-04-15 15:27:21 -06:00
let libraryDocIds ;
2026-05-06 11:44:17 -06:00
try { libraryDocIds = JSON . parse ( req . body . libraryDocIds || '[]' ) ; } catch ( e ) { return res . status ( 400 ) . json ( { error : 'libraryDocIds must be a valid JSON array.' } ) ; }
if ( ! Array . isArray ( libraryDocIds ) ) return res . status ( 400 ) . json ( { error : 'libraryDocIds must be a valid JSON array.' } ) ;
for ( const id of libraryDocIds ) { if ( ! Number . isInteger ( id ) || id <= 0 ) return res . status ( 400 ) . json ( { error : ` Invalid library document ID: ${ id } . ` } ) ; }
2026-04-15 15:27:21 -06:00
2026-04-07 16:20:24 -06:00
const validationErrors = validateFpWorkflowForm ( req . body ) ;
2026-05-06 11:44:17 -06:00
if ( Object . keys ( validationErrors ) . length > 0 ) return res . status ( 400 ) . json ( { success : false , errors : validationErrors } ) ;
2026-04-07 16:20:24 -06:00
const files = req . files || [ ] ;
2026-05-06 11:44:17 -06:00
for ( const file of files ) { if ( ! isAllowedFileExtension ( file . originalname ) ) return res . status ( 400 ) . json ( { error : ` File type not allowed: ${ file . originalname } ` } ) ; }
2026-04-07 16:20:24 -06:00
2026-05-06 11:44:17 -06:00
( async ( ) => {
// Verify queue items
const { rows : queueRows } = await pool . query (
` SELECT id, workflow_type, status, user_id FROM ivanti_todo_queue WHERE id = ANY( $ 1) ` , [ queueItemIds ]
) ;
if ( ! queueRows || queueRows . length !== queueItemIds . length ) return res . status ( 400 ) . json ( { error : 'One or more queue items not found.' } ) ;
for ( const row of queueRows ) {
if ( row . user _id !== req . user . id ) return res . status ( 403 ) . json ( { error : 'You can only submit your own queue items.' } ) ;
if ( row . workflow _type !== 'FP' ) return res . status ( 400 ) . json ( { error : ` Queue item ${ row . id } is not an FP workflow type. ` } ) ;
if ( row . status !== 'pending' ) return res . status ( 400 ) . json ( { error : ` Queue item ${ row . id } is not in pending status. ` } ) ;
}
2026-04-07 16:20:24 -06:00
2026-05-06 11:44:17 -06:00
const apiKey = process . env . IVANTI _API _KEY ;
const clientId = process . env . IVANTI _CLIENT _ID || '1550' ;
const skipTls = process . env . IVANTI _SKIP _TLS === 'true' ;
if ( ! apiKey ) return res . status ( 500 ) . json ( { success : false , error : 'Ivanti API key is not configured.' , step : 'create_workflow' } ) ;
2026-04-07 16:20:24 -06:00
2026-05-06 11:44:17 -06:00
const formFields = buildIvantiFormFields ( req . body , findingIds ) ;
2026-04-07 16:20:24 -06:00
2026-05-06 11:44:17 -06:00
// Look up library documents
let libraryDocs = [ ] ;
const libraryAttachmentResults = [ ] ;
if ( libraryDocIds . length > 0 ) {
const { rows : docRows } = await pool . query ( ` SELECT id, name, file_path, file_size, mime_type FROM documents WHERE id = ANY( $ 1) ` , [ libraryDocIds ] ) ;
libraryDocs = docRows ;
const foundIds = new Set ( libraryDocs . map ( d => d . id ) ) ;
const missingIds = libraryDocIds . filter ( id => ! foundIds . has ( id ) ) ;
if ( missingIds . length > 0 ) return res . status ( 400 ) . json ( { error : ` Library document IDs not found: ${ missingIds . join ( ', ' ) } ` } ) ;
}
2026-04-15 15:27:21 -06:00
2026-05-06 11:44:17 -06:00
const libraryFormFiles = [ ] ;
for ( const doc of libraryDocs ) {
try { const buffer = fs . readFileSync ( doc . file _path ) ; libraryFormFiles . push ( { name : 'files' , buffer , filename : doc . name } ) ; libraryAttachmentResults . push ( { filename : doc . name , success : true , source : 'library' , documentId : doc . id } ) ; }
catch ( readErr ) { libraryAttachmentResults . push ( { success : false , error : 'File not found on disk' , source : 'library' , documentId : doc . id , filename : doc . name } ) ; }
}
2026-04-15 15:27:21 -06:00
2026-05-06 11:44:17 -06:00
const localFormFiles = files . map ( f => ( { name : 'files' , buffer : f . buffer , filename : f . originalname } ) ) ;
const formFiles = [ ... localFormFiles , ... libraryFormFiles ] ;
const createUrl = ` /client/ ${ encodeURIComponent ( clientId ) } /workflowBatch/falsePositive/request ` ;
2026-04-07 16:20:24 -06:00
2026-05-06 11:44:17 -06:00
let createResult ;
try { createResult = await ivantiFormPost ( createUrl , formFields , formFiles , apiKey , skipTls ) ; }
catch ( networkErr ) {
logAudit ( { userId : req . user . id , username : req . user . username , action : 'ivanti_fp_workflow_failed' , entityType : 'ivanti_workflow' , details : { error : networkErr . message , findingIds } , ipAddress : req . ip } ) ;
return res . status ( 502 ) . json ( { success : false , error : 'Failed to connect to Ivanti API.' , step : 'create_workflow' , details : networkErr . message } ) ;
}
2026-04-07 16:20:24 -06:00
2026-05-06 11:44:17 -06:00
if ( createResult . status !== 200 && createResult . status !== 201 && createResult . status !== 202 ) {
const errorMap = { 401 : 'Ivanti API key is invalid or missing.' , 419 : 'API key lacks workflow creation permissions.' , 429 : 'Ivanti API rate limit reached.' } ;
const errorMsg = errorMap [ createResult . status ] || ` Workflow creation failed: ${ createResult . status } ` ;
logAudit ( { userId : req . user . id , username : req . user . username , action : 'ivanti_fp_workflow_failed' , entityType : 'ivanti_workflow' , details : { error : errorMsg , status : createResult . status , findingIds } , ipAddress : req . ip } ) ;
return res . status ( createResult . status === 429 ? 429 : 502 ) . json ( { success : false , error : errorMsg , step : 'create_workflow' } ) ;
}
2026-04-07 16:20:24 -06:00
2026-05-06 11:44:17 -06:00
let workflowBatchId ;
try { const createData = JSON . parse ( createResult . body ) ; workflowBatchId = createData . id ; }
catch ( parseErr ) { return res . status ( 502 ) . json ( { success : false , error : 'Failed to parse Ivanti API response.' , step : 'create_workflow' } ) ; }
2026-04-07 16:20:24 -06:00
2026-05-06 11:44:17 -06:00
let workflowBatchUuid = null ;
try { workflowBatchUuid = await resolveWorkflowBatchUuid ( { id : null , ivanti _workflow _batch _id : workflowBatchId , ivanti _workflow _batch _uuid : null , workflow _name : req . body . name } , apiKey , clientId , skipTls ) ; } catch ( e ) { }
2026-04-07 16:20:24 -06:00
2026-05-06 11:44:17 -06:00
const localAttachmentResults = files . map ( f => ( { filename : f . originalname , success : true , source : 'local' } ) ) ;
const allAttachmentResults = [ ... localAttachmentResults , ... libraryAttachmentResults ] ;
const totalAttachmentCount = files . length + libraryDocIds . length ;
2026-04-07 16:20:24 -06:00
2026-05-06 11:44:17 -06:00
try {
await pool . query (
` INSERT INTO ivanti_fp_submissions (user_id, username, ivanti_workflow_batch_id, ivanti_workflow_batch_uuid, ivanti_generated_id, workflow_name, reason, description, expiration_date, scope_override, finding_ids_json, queue_item_ids_json, attachment_count, attachment_results_json, status, error_message)
VALUES ( $1 , $2 , $3 , $4 , $5 , $6 , $7 , $8 , $9 , $10 , $11 , $12 , $13 , $14 , $15 , $16 ) ` ,
[ req . user . id , req . user . username , workflowBatchId , workflowBatchUuid , null , req . body . name , req . body . reason , req . body . description || null , req . body . expirationDate , req . body . scopeOverride || 'Authorized' , JSON . stringify ( findingIds ) , JSON . stringify ( queueItemIds ) , totalAttachmentCount , JSON . stringify ( allAttachmentResults ) , 'success' , null ]
) ;
} catch ( dbErr ) { console . error ( 'Failed to insert submission record:' , dbErr ) ; }
2026-04-13 12:53:13 -06:00
2026-05-06 11:44:17 -06:00
logAudit ( { userId : req . user . id , username : req . user . username , action : 'ivanti_fp_workflow_created' , entityType : 'ivanti_workflow' , entityId : String ( workflowBatchId ) , details : { workflowName : req . body . name , findingIds , attachmentCount : totalAttachmentCount , status : 'success' } , ipAddress : req . ip } ) ;
2026-04-07 16:20:24 -06:00
2026-05-06 11:44:17 -06:00
let queueItemsUpdated = 0 ;
try {
const qResult = await pool . query ( ` UPDATE ivanti_todo_queue SET status='complete', updated_at=NOW() WHERE id = ANY( $ 1) AND user_id= $ 2 ` , [ queueItemIds , req . user . id ] ) ;
queueItemsUpdated = qResult . rowCount ;
} catch ( queueErr ) { console . error ( 'Failed to update queue items:' , queueErr ) ; }
2026-04-07 16:20:24 -06:00
2026-05-06 11:44:17 -06:00
res . json ( { success : true , workflowBatchId , queueItemsUpdated , status : 'success' } ) ;
} ) ( ) . catch ( ( unexpectedErr ) => { console . error ( 'Unexpected error in FP workflow submission:' , unexpectedErr ) ; res . status ( 500 ) . json ( { success : false , error : 'Internal server error.' } ) ; } ) ;
2026-04-07 16:20:24 -06:00
} ) ;
} ) ;
2026-05-13 16:46:49 -06:00
/ * *
* GET / api / ivanti / fp - workflow / submissions
*
* Returns all FP workflow submissions belonging to the authenticated user ,
* enriched with Ivanti workflow state ( rework notes , approval notes , current state ) .
* Automatically syncs lifecycle _status from Ivanti currentState when it differs .
* Includes submission history entries for each submission .
*
* @ query None
* @ returns { Array < Object > } Array of submission objects
* - id { number }
* - user _id { number }
* - username { string }
* - ivanti _workflow _batch _id { number }
* - ivanti _workflow _batch _uuid { string | null }
* - workflow _name { string }
* - reason { string }
* - description { string | null }
* - expiration _date { string }
* - scope _override { string }
* - finding _ids _json { string }
* - queue _item _ids _json { string }
* - attachment _count { number }
* - lifecycle _status { string } submitted | approved | rejected | rework | resubmitted
* - dismissed _at { string | null }
* - requeued _at { string | null }
* - history { Array < Object > } Submission history entries
* - ivanti _rework _note { string | null } Enriched from Ivanti API
* - ivanti _approval _note { string | null } Enriched from Ivanti API
* - ivanti _current _state { string | null } Enriched from Ivanti API
* @ error 500 Internal server error
* /
2026-05-06 11:44:17 -06:00
router . get ( '/submissions' , requireAuth ( ) , async ( req , res ) => {
try {
const { rows : submissions } = await pool . query ( ` SELECT * FROM ivanti_fp_submissions WHERE user_id = $ 1 ORDER BY created_at DESC ` , [ req . user . id ] ) ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
2026-05-06 11:44:17 -06:00
if ( submissions . length > 0 ) {
const submissionIds = submissions . map ( s => s . id ) ;
const { rows : historyRows } = await pool . query ( ` SELECT * FROM ivanti_fp_submission_history WHERE submission_id = ANY( $ 1) ORDER BY created_at ASC ` , [ submissionIds ] ) ;
const historyMap = { } ;
for ( const row of historyRows ) { if ( ! historyMap [ row . submission _id ] ) historyMap [ row . submission _id ] = [ ] ; historyMap [ row . submission _id ] . push ( row ) ; }
for ( const sub of submissions ) { sub . history = historyMap [ sub . id ] || [ ] ; }
2026-04-13 14:25:14 -06:00
2026-05-06 11:44:17 -06:00
const apiKey = process . env . IVANTI _API _KEY ;
const clientId = process . env . IVANTI _CLIENT _ID || '1550' ;
const skipTls = process . env . IVANTI _SKIP _TLS === 'true' ;
if ( apiKey ) {
try {
for ( const sub of submissions ) {
if ( ! sub . workflow _name ) continue ;
const searchUrl = ` /client/ ${ encodeURIComponent ( clientId ) } /workflowBatch/search ` ;
const searchBody = { filters : [ { field : 'name' , exclusive : false , operator : 'EXACT' , value : sub . workflow _name } ] , projection : 'internal' , sort : [ { field : 'created' , direction : 'DESC' } ] , page : 0 , size : 1 } ;
const result = await ivantiPost ( searchUrl , searchBody , apiKey , skipTls ) ;
if ( result . status === 200 ) {
const data = JSON . parse ( result . body ) ;
const batches = data . _embedded ? . workflowBatches || data . _embedded ? . workflowBatch || [ ] ;
const batch = batches [ 0 ] ;
if ( batch ) { sub . ivanti _rework _note = batch . reworkNote || null ; sub . ivanti _approval _note = batch . approvalNote || null ; sub . ivanti _current _state _notes = batch . currentStateUserNotes || null ; sub . ivanti _previous _state _notes = batch . previousStateUserNotes || null ; sub . ivanti _current _state = batch . currentState || null ; }
2026-04-13 14:25:14 -06:00
}
}
2026-05-06 11:44:17 -06:00
} catch ( e ) { console . error ( 'Error enriching submissions with Ivanti notes:' , e . message ) ; }
2026-05-13 14:36:05 -06:00
// Sync lifecycle_status from Ivanti currentState when it differs
const STATE _MAP = { 'APPROVED' : 'approved' , 'REJECTED' : 'rejected' , 'REWORK' : 'rework' } ;
for ( const sub of submissions ) {
if ( ! sub . ivanti _current _state ) continue ;
const mappedStatus = STATE _MAP [ sub . ivanti _current _state . toUpperCase ( ) ] ;
if ( mappedStatus && mappedStatus !== sub . lifecycle _status ) {
try {
await pool . query (
` UPDATE ivanti_fp_submissions SET lifecycle_status = $ 1, updated_at = NOW() WHERE id = $ 2 ` ,
[ mappedStatus , sub . id ]
) ;
sub . lifecycle _status = mappedStatus ;
} catch ( syncErr ) { console . error ( ` Failed to sync lifecycle_status for submission ${ sub . id } : ` , syncErr . message ) ; }
}
}
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
}
}
2026-05-06 11:44:17 -06:00
res . json ( submissions ) ;
} catch ( err ) { console . error ( 'Error fetching FP submissions:' , err ) ; res . status ( 500 ) . json ( { error : 'Internal server error.' } ) ; }
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
} ) ;
2026-05-13 16:46:49 -06:00
/ * *
* PUT / api / ivanti / fp - workflow / submissions / : id
*
* Edits the workflow fields of an existing FP submission . Proxies the update
* to the Ivanti API , then updates the local record . Transitions lifecycle _status
* to "resubmitted" if currently rejected or rework .
* Requires Admin or Standard _User group .
*
* @ param { string } id — Submission ID ( URL parameter )
* @ body { Object }
* - name { string } Workflow name ( required , max 255 chars )
* - reason { string } Reason ( required )
* - description { string } Optional ( max 2000 chars )
* - expirationDate { string } Required , future date within 120 days
* - scopeOverride { string } Optional : Authorized , None , or Automated
* @ returns { Object }
* - success { boolean }
* - submission { Object } The updated submission record
* @ error 400 Validation errors or submission is finalized
* @ error 403 Submission belongs to another user
* @ error 404 Submission not found
* @ error 429 Ivanti API rate limit
* @ error 500 Ivanti API key not configured or local DB failure
* @ error 502 Ivanti API connection or response failure
* /
2026-05-06 11:44:17 -06:00
router . put ( '/submissions/:id' , requireAuth ( ) , requireGroup ( 'Admin' , 'Standard_User' ) , ( req , res ) => {
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
( async ( ) => {
const submissionId = req . params . id ;
// 1. Fetch submission and verify ownership
2026-05-06 11:44:17 -06:00
const { rows : subRows } = await pool . query (
` SELECT * FROM ivanti_fp_submissions WHERE id = $ 1 ` , [ submissionId ]
) ;
const submission = subRows [ 0 ] ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
if ( ! submission ) {
return res . status ( 404 ) . json ( { error : 'Submission not found.' } ) ;
}
if ( submission . user _id !== req . user . id ) {
return res . status ( 403 ) . json ( { error : 'You can only edit your own submissions.' } ) ;
}
// 2. Lifecycle guard
if ( submission . lifecycle _status === 'approved' ) {
return res . status ( 400 ) . json ( { error : 'This submission is finalized and cannot be edited.' } ) ;
}
// 3. Validate body
const validationErrors = validateFpWorkflowForm ( req . body ) ;
if ( Object . keys ( validationErrors ) . length > 0 ) {
return res . status ( 400 ) . json ( { success : false , errors : validationErrors } ) ;
}
// 4. Proxy to Ivanti
const apiKey = process . env . IVANTI _API _KEY ;
const clientId = process . env . IVANTI _CLIENT _ID || '1550' ;
const skipTls = process . env . IVANTI _SKIP _TLS === 'true' ;
if ( ! apiKey ) {
return res . status ( 500 ) . json ( { success : false , error : 'Ivanti API key is not configured.' } ) ;
}
const scopeMap = { 'Authorized' : 'AUTHORIZED' , 'None' : 'NONE' , 'Automated' : 'AUTOMATED' } ;
const updateUrl = ` /client/ ${ encodeURIComponent ( clientId ) } /workflowBatch/falsePositive/update ` ;
const updateBody = {
id : submission . ivanti _workflow _batch _id ,
name : req . body . name ,
reason : req . body . reason ,
description : req . body . description || '' ,
expirationDate : req . body . expirationDate ,
overrideControl : scopeMap [ req . body . scopeOverride ] || 'AUTHORIZED'
} ;
let ivantiResult ;
try {
ivantiResult = await ivantiPost ( updateUrl , updateBody , apiKey , skipTls ) ;
} catch ( networkErr ) {
2026-05-06 11:44:17 -06:00
logAudit ( {
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
userId : req . user . id , username : req . user . username ,
action : 'ivanti_fp_submission_edit_failed' , entityType : 'ivanti_workflow' ,
details : { error : networkErr . message , submissionId } ,
ipAddress : req . ip
} ) ;
return res . status ( 502 ) . json ( { success : false , error : 'Failed to connect to Ivanti API.' , details : networkErr . message } ) ;
}
if ( ivantiResult . status !== 200 && ivantiResult . status !== 201 && ivantiResult . status !== 202 ) {
const errorMap = {
401 : 'Ivanti API key is invalid or missing. Contact your administrator.' ,
419 : 'API key lacks permissions for this operation.' ,
429 : 'Ivanti API rate limit reached. Please try again in a few minutes.'
} ;
const errorMsg = ivantiResult . status >= 500
? 'Ivanti API is temporarily unavailable. Please try again later.'
: ( errorMap [ ivantiResult . status ] || ` Operation failed: ${ ivantiResult . status } ` ) ;
return res . status ( ivantiResult . status === 429 ? 429 : 502 ) . json ( { success : false , error : errorMsg } ) ;
}
// 5. Determine new lifecycle_status
let newLifecycleStatus = submission . lifecycle _status ;
if ( submission . lifecycle _status === 'rejected' || submission . lifecycle _status === 'rework' ) {
newLifecycleStatus = 'resubmitted' ;
}
// 6. Update local record
try {
2026-05-06 11:44:17 -06:00
await pool . query (
` UPDATE ivanti_fp_submissions
SET workflow _name = $1 , reason = $2 , description = $3 , expiration _date = $4 , scope _override = $5 , lifecycle _status = $6 , updated _at = NOW ( )
WHERE id = $7 ` ,
[ req . body . name , req . body . reason , req . body . description || null , req . body . expirationDate , req . body . scopeOverride || 'Authorized' , newLifecycleStatus , submissionId ]
) ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
} catch ( dbErr ) {
console . error ( 'Failed to update submission record:' , dbErr ) ;
return res . status ( 500 ) . json ( { success : false , error : 'Failed to update local record.' } ) ;
}
// 7. Insert history row
const historyEntry = buildSubmissionHistoryEntry ( 'fields_updated' , {
changed : {
name : { from : submission . workflow _name , to : req . body . name } ,
reason : { from : submission . reason , to : req . body . reason } ,
description : { from : submission . description , to : req . body . description || '' } ,
expirationDate : { from : submission . expiration _date , to : req . body . expirationDate } ,
scopeOverride : { from : submission . scope _override , to : req . body . scopeOverride || 'Authorized' }
}
} , req . user . id , req . user . username ) ;
try {
2026-05-06 11:44:17 -06:00
await pool . query (
` INSERT INTO ivanti_fp_submission_history (submission_id, user_id, username, change_type, change_details_json, created_at)
VALUES ( $1 , $2 , $3 , $4 , $5 , NOW ( ) ) ` ,
[ submissionId , historyEntry . user _id , historyEntry . username , historyEntry . change _type , historyEntry . change _details _json ]
) ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
} catch ( histErr ) {
console . error ( 'Failed to insert history row:' , histErr ) ;
}
// 8. Audit log
2026-05-06 11:44:17 -06:00
logAudit ( {
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
userId : req . user . id , username : req . user . username ,
action : 'ivanti_fp_submission_edited' , entityType : 'ivanti_workflow' ,
entityId : String ( submission . ivanti _workflow _batch _id ) ,
details : { submissionId , workflowName : req . body . name } ,
ipAddress : req . ip
} ) ;
// 9. Return updated record
2026-05-06 11:44:17 -06:00
const { rows : updatedRows } = await pool . query (
` SELECT * FROM ivanti_fp_submissions WHERE id = $ 1 ` , [ submissionId ]
) ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
2026-05-06 11:44:17 -06:00
res . json ( { success : true , submission : updatedRows [ 0 ] } ) ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
} ) ( ) . catch ( ( unexpectedErr ) => {
console . error ( 'Unexpected error in PUT /submissions/:id:' , unexpectedErr ) ;
res . status ( 500 ) . json ( { success : false , error : 'Internal server error.' } ) ;
} ) ;
} ) ;
2026-05-13 16:46:49 -06:00
/ * *
* POST / api / ivanti / fp - workflow / submissions / : id / findings
*
* Maps additional findings to an existing FP workflow in Ivanti . Resolves the
* workflow batch UUID , then maps each finding individually . Successfully mapped
* findings are merged into the submission ' s finding _ids _json and their queue items
* are marked complete .
* Requires Admin or Standard _User group .
*
* @ param { string } id — Submission ID ( URL parameter )
* @ body { Object }
* - findingIds { Array < number | string > } Finding IDs to map ( at least one required )
* - queueItemIds { Array < number > } Corresponding queue item IDs ( at least one required )
* @ returns { Object }
* - success { boolean }
* - addedFindings { Array } Successfully mapped finding IDs
* - failedFindings { Array } Finding IDs that failed to map
* - queueItemsUpdated { number } Count of queue items marked complete
* @ error 400 Invalid input , queue item validation , or UUID resolution failure
* @ error 403 Submission or queue items belong to another user
* @ error 404 Submission not found
* @ error 500 Ivanti API key not configured
* @ error 502 All findings failed to map
* /
2026-05-06 11:44:17 -06:00
router . post ( '/submissions/:id/findings' , requireAuth ( ) , requireGroup ( 'Admin' , 'Standard_User' ) , ( req , res ) => {
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
( async ( ) => {
const submissionId = req . params . id ;
const { findingIds , queueItemIds } = req . body ;
if ( ! Array . isArray ( findingIds ) || findingIds . length === 0 ) {
return res . status ( 400 ) . json ( { error : 'At least one finding ID is required.' } ) ;
}
if ( ! Array . isArray ( queueItemIds ) || queueItemIds . length === 0 ) {
return res . status ( 400 ) . json ( { error : 'At least one queue item ID is required.' } ) ;
}
// 1. Fetch submission and verify ownership
2026-05-06 11:44:17 -06:00
const { rows : subRows } = await pool . query (
` SELECT * FROM ivanti_fp_submissions WHERE id = $ 1 ` , [ submissionId ]
) ;
const submission = subRows [ 0 ] ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
if ( ! submission ) {
return res . status ( 404 ) . json ( { error : 'Submission not found.' } ) ;
}
if ( submission . user _id !== req . user . id ) {
return res . status ( 403 ) . json ( { error : 'You can only edit your own submissions.' } ) ;
}
// 2. Lifecycle guard
if ( submission . lifecycle _status === 'approved' ) {
return res . status ( 400 ) . json ( { error : 'This submission is finalized and cannot be edited.' } ) ;
}
// 3. Verify queue items belong to user, are FP type, and pending
2026-05-06 11:44:17 -06:00
const { rows : queueRows } = await pool . query (
` SELECT id, workflow_type, status, user_id FROM ivanti_todo_queue WHERE id = ANY( $ 1) ` ,
[ queueItemIds ]
) ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
if ( queueRows . length !== queueItemIds . length ) {
return res . status ( 400 ) . json ( { error : 'One or more queue items not found.' } ) ;
}
for ( const row of queueRows ) {
if ( row . user _id !== req . user . id ) {
return res . status ( 403 ) . json ( { error : 'You can only submit your own queue items.' } ) ;
}
if ( row . workflow _type !== 'FP' ) {
return res . status ( 400 ) . json ( { error : ` Queue item ${ row . id } is not an FP workflow type. ` } ) ;
}
if ( row . status !== 'pending' ) {
return res . status ( 400 ) . json ( { error : ` Queue item ${ row . id } is not in pending status. ` } ) ;
}
}
// 4. Proxy to Ivanti map endpoint
const apiKey = process . env . IVANTI _API _KEY ;
const clientId = process . env . IVANTI _CLIENT _ID || '1550' ;
const skipTls = process . env . IVANTI _SKIP _TLS === 'true' ;
if ( ! apiKey ) {
return res . status ( 500 ) . json ( { success : false , error : 'Ivanti API key is not configured.' } ) ;
}
2026-05-06 11:44:17 -06:00
const mapUuid = await resolveWorkflowBatchUuid ( submission , apiKey , clientId , skipTls ) ;
2026-04-13 12:53:13 -06:00
if ( ! mapUuid ) {
return res . status ( 400 ) . json ( { success : false , error : 'Could not resolve workflow batch UUID. The workflow may not exist in Ivanti.' } ) ;
}
const mapUrl = ` /client/ ${ encodeURIComponent ( clientId ) } /workflowBatch/falsePositive/ ${ encodeURIComponent ( mapUuid ) } /map ` ;
2026-04-13 13:56:00 -06:00
2026-04-13 15:59:55 -06:00
const mappedIds = [ ] ;
const failedIds = [ ] ;
for ( const fid of findingIds ) {
const mapBody = {
subject : 'hostFinding' ,
filterRequest : {
2026-05-06 11:44:17 -06:00
filters : [ { field : 'id' , exclusive : false , operator : 'EXACT' , value : String ( fid ) } ]
2026-04-13 15:59:55 -06:00
}
} ;
try {
const result = await ivantiPost ( mapUrl , mapBody , apiKey , skipTls ) ;
if ( result . status === 200 || result . status === 201 || result . status === 202 ) {
mappedIds . push ( fid ) ;
} else {
failedIds . push ( { id : fid , status : result . status } ) ;
}
} catch ( err ) {
failedIds . push ( { id : fid , error : err . message } ) ;
}
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
}
2026-04-13 15:59:55 -06:00
if ( mappedIds . length === 0 ) {
return res . status ( 502 ) . json ( { success : false , error : 'Failed to map any findings to the workflow.' } ) ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
}
2026-04-13 15:59:55 -06:00
// 5. Merge only successfully mapped finding IDs
const mergedJson = mergeFindings ( submission . finding _ids _json , mappedIds ) ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
try {
2026-05-06 11:44:17 -06:00
await pool . query (
` UPDATE ivanti_fp_submissions SET finding_ids_json = $ 1, updated_at = NOW() WHERE id = $ 2 ` ,
[ mergedJson , submissionId ]
) ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
} catch ( dbErr ) {
console . error ( 'Failed to update finding_ids_json:' , dbErr ) ;
}
2026-04-13 15:59:55 -06:00
// 6. Mark only successfully mapped queue items as complete
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
let queueItemsUpdated = 0 ;
2026-04-13 15:59:55 -06:00
const mappedSet = new Set ( mappedIds . map ( String ) ) ;
const successQueueIds = queueItemIds . filter ( ( qid , idx ) => {
const queueItem = queueRows . find ( r => r . id === qid ) ;
return queueItem && mappedSet . has ( String ( findingIds [ idx ] ) ) ;
} ) ;
if ( successQueueIds . length > 0 ) {
try {
2026-05-06 11:44:17 -06:00
const qResult = await pool . query (
` UPDATE ivanti_todo_queue SET status='complete', updated_at=NOW() WHERE id = ANY( $ 1) AND user_id= $ 2 ` ,
[ successQueueIds , req . user . id ]
) ;
queueItemsUpdated = qResult . rowCount ;
2026-04-13 15:59:55 -06:00
} catch ( queueErr ) {
console . error ( 'Failed to update queue items:' , queueErr ) ;
}
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
}
// 7. Insert history row
const historyEntry = buildSubmissionHistoryEntry ( 'findings_added' , {
2026-04-13 15:59:55 -06:00
addedFindingIds : mappedIds ,
failedFindingIds : failedIds . map ( f => f . id || f ) ,
queueItemIds : successQueueIds
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
} , req . user . id , req . user . username ) ;
try {
2026-05-06 11:44:17 -06:00
await pool . query (
` INSERT INTO ivanti_fp_submission_history (submission_id, user_id, username, change_type, change_details_json, created_at)
VALUES ( $1 , $2 , $3 , $4 , $5 , NOW ( ) ) ` ,
[ submissionId , historyEntry . user _id , historyEntry . username , historyEntry . change _type , historyEntry . change _details _json ]
) ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
} catch ( histErr ) {
console . error ( 'Failed to insert history row:' , histErr ) ;
}
// 8. Audit log
2026-05-06 11:44:17 -06:00
logAudit ( {
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
userId : req . user . id , username : req . user . username ,
action : 'ivanti_fp_findings_added' , entityType : 'ivanti_workflow' ,
entityId : String ( submission . ivanti _workflow _batch _id ) ,
details : { submissionId , addedFindingIds : findingIds , queueItemsUpdated } ,
ipAddress : req . ip
} ) ;
2026-04-13 15:59:55 -06:00
res . json ( { success : true , addedFindings : mappedIds , failedFindings : failedIds , queueItemsUpdated } ) ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
} ) ( ) . catch ( ( unexpectedErr ) => {
console . error ( 'Unexpected error in POST /submissions/:id/findings:' , unexpectedErr ) ;
res . status ( 500 ) . json ( { success : false , error : 'Internal server error.' } ) ;
} ) ;
} ) ;
2026-05-13 16:46:49 -06:00
/ * *
* POST / api / ivanti / fp - workflow / submissions / : id / attachments
*
* Uploads additional attachments ( local files and / or library documents ) to an
* existing FP workflow in Ivanti . Updates the local submission ' s attachment count
* and results .
* Requires Admin or Standard _User group .
*
* @ param { string } id — Submission ID ( URL parameter )
* @ body { Object } multipart / form - data
* - attachments { File [ ] } Local files to upload ( max 10 , each max 10 MB )
* - libraryDocIds { string } Optional JSON array of document library IDs
* @ returns { Object }
* - success { boolean }
* - attachmentResults { Array < Object > } Per - file upload results with filename , success , source
* - status { string } "success" if all uploaded , "partial" if some failed
* @ error 400 No files provided , invalid file type , or submission is finalized
* @ error 403 Submission belongs to another user
* @ error 404 Submission not found
* @ error 500 Ivanti API key not configured
* /
2026-05-06 11:44:17 -06:00
router . post ( '/submissions/:id/attachments' , requireAuth ( ) , requireGroup ( 'Admin' , 'Standard_User' ) , ( req , res ) => {
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
fpUpload ( req , res , ( multerErr ) => {
if ( multerErr ) {
if ( multerErr . code === 'LIMIT_FILE_SIZE' ) {
return res . status ( 400 ) . json ( { error : 'File exceeds the 10 MB size limit.' } ) ;
}
return res . status ( 400 ) . json ( { error : multerErr . message } ) ;
}
const files = req . files || [ ] ;
2026-04-15 15:27:21 -06:00
let libraryDocIds ;
2026-05-06 11:44:17 -06:00
try { libraryDocIds = JSON . parse ( req . body . libraryDocIds || '[]' ) ; }
catch ( e ) { return res . status ( 400 ) . json ( { error : 'libraryDocIds must be a valid JSON array.' } ) ; }
if ( ! Array . isArray ( libraryDocIds ) ) return res . status ( 400 ) . json ( { error : 'libraryDocIds must be a valid JSON array.' } ) ;
for ( const id of libraryDocIds ) { if ( ! Number . isInteger ( id ) || id <= 0 ) return res . status ( 400 ) . json ( { error : ` Invalid library document ID: ${ id } . ` } ) ; }
2026-04-15 15:27:21 -06:00
if ( files . length === 0 && libraryDocIds . length === 0 ) {
return res . status ( 400 ) . json ( { error : 'At least one file or library document is required.' } ) ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
}
for ( const file of files ) {
if ( ! isAllowedFileExtension ( file . originalname ) ) {
2026-05-06 11:44:17 -06:00
return res . status ( 400 ) . json ( { error : ` File type not allowed: ${ file . originalname } ` } ) ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
}
}
( async ( ) => {
const submissionId = req . params . id ;
2026-05-06 11:44:17 -06:00
const { rows : subRows } = await pool . query (
` SELECT * FROM ivanti_fp_submissions WHERE id = $ 1 ` , [ submissionId ]
) ;
const submission = subRows [ 0 ] ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
2026-05-06 11:44:17 -06:00
if ( ! submission ) return res . status ( 404 ) . json ( { error : 'Submission not found.' } ) ;
if ( submission . user _id !== req . user . id ) return res . status ( 403 ) . json ( { error : 'You can only edit your own submissions.' } ) ;
if ( submission . lifecycle _status === 'approved' ) return res . status ( 400 ) . json ( { error : 'This submission is finalized and cannot be edited.' } ) ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
2026-05-06 11:44:17 -06:00
// Look up library documents
2026-04-15 15:27:21 -06:00
let libraryDocs = [ ] ;
if ( libraryDocIds . length > 0 ) {
2026-05-06 11:44:17 -06:00
const { rows : docRows } = await pool . query (
` SELECT id, name, file_path, file_size, mime_type FROM documents WHERE id = ANY( $ 1) ` , [ libraryDocIds ]
) ;
libraryDocs = docRows ;
2026-04-15 15:27:21 -06:00
const foundIds = new Set ( libraryDocs . map ( d => d . id ) ) ;
const missingIds = libraryDocIds . filter ( id => ! foundIds . has ( id ) ) ;
2026-05-06 11:44:17 -06:00
if ( missingIds . length > 0 ) return res . status ( 400 ) . json ( { error : ` Library document IDs not found: ${ missingIds . join ( ', ' ) } ` } ) ;
2026-04-15 15:27:21 -06:00
}
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
const apiKey = process . env . IVANTI _API _KEY ;
const clientId = process . env . IVANTI _CLIENT _ID || '1550' ;
const skipTls = process . env . IVANTI _SKIP _TLS === 'true' ;
2026-05-06 11:44:17 -06:00
if ( ! apiKey ) return res . status ( 500 ) . json ( { success : false , error : 'Ivanti API key is not configured.' } ) ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
2026-05-06 11:44:17 -06:00
const attachUuid = await resolveWorkflowBatchUuid ( submission , apiKey , clientId , skipTls ) ;
if ( ! attachUuid ) return res . status ( 400 ) . json ( { success : false , error : 'Could not resolve workflow batch UUID.' } ) ;
2026-04-13 12:53:13 -06:00
const attachUrl = ` /client/ ${ encodeURIComponent ( clientId ) } /workflowBatch/falsePositive/ ${ encodeURIComponent ( attachUuid ) } /attach ` ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
const attachmentResults = [ ] ;
2026-04-15 15:27:21 -06:00
// Upload local files
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
for ( const f of files ) {
try {
2026-04-13 14:07:13 -06:00
const formFiles = [ { name : 'file' , buffer : f . buffer , filename : f . originalname , contentType : f . mimetype || 'application/octet-stream' } ] ;
2026-04-13 14:05:05 -06:00
const result = await ivantiFormPost ( attachUrl , [ ] , formFiles , apiKey , skipTls ) ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
const success = result . status === 200 || result . status === 201 || result . status === 202 ;
2026-04-15 15:27:21 -06:00
attachmentResults . push ( { filename : f . originalname , success , source : 'local' , ... ( success ? { } : { error : ` Upload failed: ${ result . status } ` } ) } ) ;
} catch ( uploadErr ) {
attachmentResults . push ( { filename : f . originalname , success : false , source : 'local' , error : uploadErr . message } ) ;
}
}
// Upload library files
for ( const doc of libraryDocs ) {
let buffer ;
2026-05-06 11:44:17 -06:00
try { buffer = fs . readFileSync ( doc . file _path ) ; }
catch ( readErr ) { attachmentResults . push ( { success : false , error : 'File not found on disk' , source : 'library' , documentId : doc . id , filename : doc . name } ) ; continue ; }
2026-04-15 15:27:21 -06:00
try {
const formFiles = [ { name : 'file' , buffer , filename : doc . name , contentType : doc . mime _type || 'application/octet-stream' } ] ;
const result = await ivantiFormPost ( attachUrl , [ ] , formFiles , apiKey , skipTls ) ;
const success = result . status === 200 || result . status === 201 || result . status === 202 ;
attachmentResults . push ( { filename : doc . name , success , source : 'library' , documentId : doc . id , ... ( success ? { } : { error : ` Upload failed: ${ result . status } ` } ) } ) ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
} catch ( uploadErr ) {
2026-04-15 15:27:21 -06:00
attachmentResults . push ( { filename : doc . name , success : false , source : 'library' , documentId : doc . id , error : uploadErr . message } ) ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
}
}
2026-05-06 11:44:17 -06:00
// Update attachment_count and attachment_results_json
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
const existingResults = JSON . parse ( submission . attachment _results _json || '[]' ) ;
const allResults = [ ... existingResults , ... attachmentResults ] ;
const successCount = attachmentResults . filter ( r => r . success ) . length ;
const newAttachmentCount = ( submission . attachment _count || 0 ) + successCount ;
try {
2026-05-06 11:44:17 -06:00
await pool . query (
` UPDATE ivanti_fp_submissions SET attachment_count = $ 1, attachment_results_json = $ 2, updated_at = NOW() WHERE id = $ 3 ` ,
[ newAttachmentCount , JSON . stringify ( allResults ) , submissionId ]
) ;
} catch ( dbErr ) { console . error ( 'Failed to update attachment records:' , dbErr ) ; }
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
2026-05-06 11:44:17 -06:00
// Insert history row
const historyEntry = buildSubmissionHistoryEntry ( 'attachments_added' , { files : attachmentResults } , req . user . id , req . user . username ) ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
try {
2026-05-06 11:44:17 -06:00
await pool . query (
` INSERT INTO ivanti_fp_submission_history (submission_id, user_id, username, change_type, change_details_json, created_at)
VALUES ( $1 , $2 , $3 , $4 , $5 , NOW ( ) ) ` ,
[ submissionId , historyEntry . user _id , historyEntry . username , historyEntry . change _type , historyEntry . change _details _json ]
) ;
} catch ( histErr ) { console . error ( 'Failed to insert history row:' , histErr ) ; }
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
2026-05-06 11:44:17 -06:00
logAudit ( {
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
userId : req . user . id , username : req . user . username ,
action : 'ivanti_fp_attachments_added' , entityType : 'ivanti_workflow' ,
entityId : String ( submission . ivanti _workflow _batch _id ) ,
2026-04-15 15:27:21 -06:00
details : { submissionId , attachmentResults , libraryDocCount : libraryDocIds . length } ,
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
ipAddress : req . ip
} ) ;
const allSucceeded = attachmentResults . every ( r => r . success ) ;
res . json ( { success : true , attachmentResults , status : allSucceeded ? 'success' : 'partial' } ) ;
} ) ( ) . catch ( ( unexpectedErr ) => {
console . error ( 'Unexpected error in POST /submissions/:id/attachments:' , unexpectedErr ) ;
res . status ( 500 ) . json ( { success : false , error : 'Internal server error.' } ) ;
} ) ;
} ) ;
} ) ;
2026-05-13 16:46:49 -06:00
/ * *
* PATCH / api / ivanti / fp - workflow / submissions / : id / dismiss
*
* Dismisses a rejected FP submission by setting dismissed _at timestamp .
* Only rejected submissions can be dismissed .
* Requires Admin or Standard _User group .
*
* @ param { string } id — Submission ID ( URL parameter )
* @ body None
* @ returns { Object } { success : true }
* @ error 400 Submission is not in rejected status
* @ error 403 Submission belongs to another user
* @ error 404 Submission not found
* @ error 500 Internal server error
* /
2026-05-11 14:29:50 -06:00
router . patch ( '/submissions/:id/dismiss' , requireAuth ( ) , requireGroup ( 'Admin' , 'Standard_User' ) , ( req , res ) => {
( async ( ) => {
const submissionId = req . params . id ;
const { rows : subRows } = await pool . query (
` SELECT * FROM ivanti_fp_submissions WHERE id = $ 1 ` , [ submissionId ]
) ;
const submission = subRows [ 0 ] ;
if ( ! submission ) return res . status ( 404 ) . json ( { error : 'Submission not found.' } ) ;
if ( submission . user _id !== req . user . id ) return res . status ( 403 ) . json ( { error : 'You can only dismiss your own submissions.' } ) ;
if ( submission . lifecycle _status !== 'rejected' ) return res . status ( 400 ) . json ( { error : 'Only rejected submissions can be dismissed.' } ) ;
try {
await pool . query (
` UPDATE ivanti_fp_submissions SET dismissed_at = NOW() WHERE id = $ 1 ` ,
[ submissionId ]
) ;
} catch ( dbErr ) {
console . error ( 'Failed to set dismissed_at:' , dbErr ) ;
return res . status ( 500 ) . json ( { error : 'Internal server error.' } ) ;
}
logAudit ( {
userId : req . user . id , username : req . user . username ,
action : 'ivanti_fp_submission_dismissed' , entityType : 'ivanti_workflow' ,
entityId : String ( submission . ivanti _workflow _batch _id ) ,
details : { submissionId } ,
ipAddress : req . ip
} ) ;
res . json ( { success : true } ) ;
} ) ( ) . catch ( ( unexpectedErr ) => {
console . error ( 'Unexpected error in PATCH /submissions/:id/dismiss:' , unexpectedErr ) ;
res . status ( 500 ) . json ( { error : 'Internal server error.' } ) ;
} ) ;
} ) ;
2026-05-13 16:46:49 -06:00
/ * *
* POST / api / ivanti / fp - workflow / submissions / : id / requeue
*
* Re - queues findings from a rejected FP submission into the todo queue
* under a specified target workflow type . Creates new pending queue items
* for each finding referenced in the submission ' s queue _item _ids _json .
* Requires Admin or Standard _User group .
*
* @ param { string } id — Submission ID ( URL parameter )
* @ body { Object }
* - workflow _type { string } Required . One of : FP , Archer , CARD , GRANITE , DECOM
* - vendor { string } Required for FP , Archer , and DECOM workflows ; max 200 chars
* @ returns { Object }
* - success { boolean }
* - items { Array < Object > } Newly created queue items with parsed cves
* - count { number } Number of items created
* @ error 400 Invalid input , submission not rejected , or already re - queued
* @ error 403 Submission belongs to another user
* @ error 404 Submission not found
* @ error 500 Internal server error
* /
router . post ( '/submissions/:id/requeue' , requireAuth ( ) , requireGroup ( 'Admin' , 'Standard_User' ) , ( req , res ) => {
( async ( ) => {
const submissionId = req . params . id ;
const { workflow _type , vendor } = req . body ;
// Validate workflow_type
const VALID _REQUEUE _TYPES = [ 'FP' , 'Archer' , 'CARD' , 'GRANITE' , 'DECOM' ] ;
const INVENTORY _TYPES = [ 'CARD' , 'GRANITE' ] ;
if ( ! VALID _REQUEUE _TYPES . includes ( workflow _type ) ) {
return res . status ( 400 ) . json ( { error : 'workflow_type must be FP, Archer, CARD, GRANITE, or DECOM.' } ) ;
}
// Validate vendor for FP/Archer/DECOM
if ( ! INVENTORY _TYPES . includes ( workflow _type ) ) {
if ( ! vendor || typeof vendor !== 'string' || vendor . trim ( ) . length === 0 ) {
return res . status ( 400 ) . json ( { error : 'vendor is required for FP, Archer, and DECOM workflows.' } ) ;
}
if ( vendor . trim ( ) . length > 200 ) {
return res . status ( 400 ) . json ( { error : 'vendor must be 200 characters or fewer.' } ) ;
}
}
const vendorVal = INVENTORY _TYPES . includes ( workflow _type ) ? '' : vendor . trim ( ) ;
// Fetch submission
const { rows : subRows } = await pool . query (
` SELECT * FROM ivanti_fp_submissions WHERE id = $ 1 ` , [ submissionId ]
) ;
const submission = subRows [ 0 ] ;
if ( ! submission ) return res . status ( 404 ) . json ( { error : 'Submission not found.' } ) ;
if ( submission . user _id !== req . user . id ) return res . status ( 403 ) . json ( { error : 'You can only re-queue your own submissions.' } ) ;
if ( submission . lifecycle _status !== 'rejected' ) return res . status ( 400 ) . json ( { error : 'Only rejected submissions can be re-queued.' } ) ;
if ( submission . requeued _at ) return res . status ( 400 ) . json ( { error : 'Findings from this submission have already been re-queued.' } ) ;
// Parse original queue item IDs
let queueItemIds = [ ] ;
try {
queueItemIds = JSON . parse ( submission . queue _item _ids _json || '[]' ) ;
2026-05-13 16:57:57 -06:00
} catch ( e ) { /* ignore parse errors */ }
2026-05-13 16:46:49 -06:00
2026-05-13 16:57:57 -06:00
// Parse finding IDs (always available, even for submissions created outside dashboard)
let findingIds = [ ] ;
try {
findingIds = JSON . parse ( submission . finding _ids _json || '[]' ) ;
} catch ( e ) { /* ignore */ }
if ( ( ! Array . isArray ( queueItemIds ) || queueItemIds . length === 0 ) &&
( ! Array . isArray ( findingIds ) || findingIds . length === 0 ) ) {
return res . status ( 400 ) . json ( { error : 'No findings associated with this submission.' } ) ;
2026-05-13 16:46:49 -06:00
}
2026-05-13 16:57:57 -06:00
// Fetch original queue items to get finding data (if they still exist)
let findingsToQueue = [ ] ;
if ( queueItemIds . length > 0 ) {
const { rows : originalItems } = await pool . query (
` SELECT finding_id, finding_title, cves_json, ip_address, hostname FROM ivanti_todo_queue WHERE id = ANY( $ 1) ` ,
[ queueItemIds ]
) ;
findingsToQueue = originalItems ;
}
2026-05-13 16:46:49 -06:00
2026-05-13 16:57:57 -06:00
// Fallback: if original queue items were deleted or never existed,
// use finding_ids_json to look up finding data from ivanti_findings
if ( findingsToQueue . length === 0 && findingIds . length > 0 ) {
const { rows : findings } = await pool . query (
2026-05-15 17:41:38 -06:00
` SELECT id AS finding_id, title AS finding_title, cves, ip_address, host_name AS hostname FROM ivanti_findings WHERE id = ANY( $ 1) ` ,
2026-05-13 16:57:57 -06:00
[ findingIds . map ( String ) ]
) ;
2026-05-15 17:41:38 -06:00
findingsToQueue = findings . map ( f => ( {
... f ,
cves _json : Array . isArray ( f . cves ) ? JSON . stringify ( f . cves ) : null ,
} ) ) ;
2026-05-13 16:57:57 -06:00
// Last resort: create items with just the finding IDs (minimal data)
if ( findingsToQueue . length === 0 ) {
findingsToQueue = findingIds . map ( id => ( {
finding _id : String ( id ) ,
finding _title : null ,
cves _json : null ,
ip _address : null ,
hostname : null ,
} ) ) ;
}
2026-05-13 16:46:49 -06:00
}
// INSERT new pending queue items for each finding
const newItems = [ ] ;
2026-05-13 16:57:57 -06:00
for ( const item of findingsToQueue ) {
2026-05-13 16:46:49 -06:00
const { rows : inserted } = await pool . query (
` INSERT INTO ivanti_todo_queue
( user _id , finding _id , finding _title , cves _json , ip _address , hostname , vendor , workflow _type )
VALUES ( $1 , $2 , $3 , $4 , $5 , $6 , $7 , $8 )
RETURNING * ` ,
[ req . user . id , item . finding _id , item . finding _title , item . cves _json , item . ip _address , item . hostname , vendorVal , workflow _type ]
) ;
newItems . push ( inserted [ 0 ] ) ;
}
// UPDATE submission to mark as requeued
await pool . query (
` UPDATE ivanti_fp_submissions SET requeued_at = NOW() WHERE id = $ 1 ` ,
[ submissionId ]
) ;
// Audit log (fire-and-forget)
2026-05-13 16:57:57 -06:00
const auditFindingIds = findingsToQueue . map ( i => i . finding _id ) . filter ( Boolean ) ;
2026-05-13 16:46:49 -06:00
logAudit ( {
userId : req . user . id , username : req . user . username ,
action : 'fp_submission_requeued' , entityType : 'ivanti_fp_submissions' ,
entityId : String ( submission . id ) ,
2026-05-13 16:57:57 -06:00
details : { target _workflow _type : workflow _type , items _created : newItems . length , finding _ids : auditFindingIds } ,
2026-05-13 16:46:49 -06:00
ipAddress : req . ip
} ) ;
// Return items with parsed cves
const itemsWithCves = newItems . map ( i => ( {
... i ,
cves : i . cves _json ? JSON . parse ( i . cves _json ) : [ ]
} ) ) ;
res . status ( 201 ) . json ( { success : true , items : itemsWithCves , count : newItems . length } ) ;
} ) ( ) . catch ( ( unexpectedErr ) => {
console . error ( 'Unexpected error in POST /submissions/:id/requeue:' , unexpectedErr ) ;
res . status ( 500 ) . json ( { success : false , error : 'Internal server error.' } ) ;
} ) ;
} ) ;
/ * *
* PATCH / api / ivanti / fp - workflow / submissions / : id / status
*
* Manually updates the lifecycle status of an FP submission .
* Validates the transition is allowed ( e . g . , approved submissions cannot be changed ) .
* Requires Admin or Standard _User group .
*
* @ param { string } id — Submission ID ( URL parameter )
* @ body { Object }
* - lifecycle _status { string } New status . One of : submitted , approved , rejected , rework , resubmitted
* @ returns { Object }
* - success { boolean }
* - previousStatus { string }
* - newStatus { string }
* @ error 400 Invalid transition or invalid status value
* @ error 403 Submission belongs to another user
* @ error 404 Submission not found
* @ error 500 Internal server error
* /
2026-05-06 11:44:17 -06:00
router . patch ( '/submissions/:id/status' , requireAuth ( ) , requireGroup ( 'Admin' , 'Standard_User' ) , ( req , res ) => {
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
( async ( ) => {
const submissionId = req . params . id ;
const newStatus = req . body . lifecycle _status ;
2026-05-06 11:44:17 -06:00
const { rows : subRows } = await pool . query (
` SELECT * FROM ivanti_fp_submissions WHERE id = $ 1 ` , [ submissionId ]
) ;
const submission = subRows [ 0 ] ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
2026-05-06 11:44:17 -06:00
if ( ! submission ) return res . status ( 404 ) . json ( { error : 'Submission not found.' } ) ;
if ( submission . user _id !== req . user . id ) return res . status ( 403 ) . json ( { error : 'You can only edit your own submissions.' } ) ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
const transition = validateLifecycleTransition ( submission . lifecycle _status , newStatus ) ;
2026-05-06 11:44:17 -06:00
if ( ! transition . valid ) return res . status ( 400 ) . json ( { error : transition . error } ) ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
const previousStatus = submission . lifecycle _status ;
try {
2026-05-06 11:44:17 -06:00
await pool . query (
` UPDATE ivanti_fp_submissions SET lifecycle_status = $ 1, updated_at = NOW() WHERE id = $ 2 ` ,
[ newStatus , submissionId ]
) ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
} catch ( dbErr ) {
console . error ( 'Failed to update lifecycle status:' , dbErr ) ;
return res . status ( 500 ) . json ( { success : false , error : 'Failed to update status.' } ) ;
}
2026-05-06 11:44:17 -06:00
const historyEntry = buildSubmissionHistoryEntry ( 'status_changed' , { from : previousStatus , to : newStatus } , req . user . id , req . user . username ) ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
try {
2026-05-06 11:44:17 -06:00
await pool . query (
` INSERT INTO ivanti_fp_submission_history (submission_id, user_id, username, change_type, change_details_json, created_at)
VALUES ( $1 , $2 , $3 , $4 , $5 , NOW ( ) ) ` ,
[ submissionId , historyEntry . user _id , historyEntry . username , historyEntry . change _type , historyEntry . change _details _json ]
) ;
} catch ( histErr ) { console . error ( 'Failed to insert history row:' , histErr ) ; }
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
2026-05-06 11:44:17 -06:00
logAudit ( {
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
userId : req . user . id , username : req . user . username ,
action : 'ivanti_fp_status_changed' , entityType : 'ivanti_workflow' ,
entityId : String ( submission . ivanti _workflow _batch _id ) ,
details : { submissionId , from : previousStatus , to : newStatus } ,
ipAddress : req . ip
} ) ;
res . json ( { success : true , previousStatus , newStatus } ) ;
} ) ( ) . catch ( ( unexpectedErr ) => {
console . error ( 'Unexpected error in PATCH /submissions/:id/status:' , unexpectedErr ) ;
res . status ( 500 ) . json ( { success : false , error : 'Internal server error.' } ) ;
} ) ;
} ) ;
2026-04-07 16:20:24 -06:00
return router ;
}
module . exports = createIvantiFpWorkflowRouter ;
module . exports . validateFpWorkflowForm = validateFpWorkflowForm ;
2026-04-08 10:18:45 -06:00
module . exports . buildIvantiFormFields = buildIvantiFormFields ;
module . exports . buildSubjectFilterRequest = buildSubjectFilterRequest ;
2026-04-07 16:20:24 -06:00
module . exports . isAllowedFileExtension = isAllowedFileExtension ;
feat: add FP submission editing with lifecycle tracking, clickable workflow badges, and edit modal
- Add migration for lifecycle_status, batch UUID, updated_at columns and submission history table
- Add backend endpoints: GET/PUT/POST/PATCH for viewing, editing, adding findings/attachments, and status changes
- Add pure helpers: validateLifecycleTransition, mergeFindings, buildSubmissionHistoryEntry
- Add FpEditModal with tabbed UI (Details, Findings, Attachments, History)
- Make workflow badges clickable for Reworked/Rejected/Expired states with pencil icon
- Add submissions list section to QueuePanel with lifecycle status badges
- Wire state and data flow in ReportingPage for submissions fetch and edit callbacks
2026-04-13 12:27:56 -06:00
module . exports . validateLifecycleTransition = validateLifecycleTransition ;
module . exports . mergeFindings = mergeFindings ;
module . exports . buildSubmissionHistoryEntry = buildSubmissionHistoryEntry ;
2026-05-11 14:29:50 -06:00
module . exports . filterVisibleSubmissions = filterVisibleSubmissions ;
module . exports . shouldShowDismissButton = shouldShowDismissButton ;