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-06 11:44:17 -06:00
// GET /documents/search
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-06 11:44:17 -06:00
// POST / — Create FP workflow
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-06 11:44:17 -06:00
// GET /submissions
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-06 11:44:17 -06:00
// PUT /submissions/:id — Edit FP workflow fields
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-06 11:44:17 -06:00
// POST /submissions/:id/findings — Map additional findings to existing workflow
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-06 11:44:17 -06:00
// POST /submissions/:id/attachments — Upload additional attachments
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-11 14:29:50 -06:00
// PATCH /submissions/:id/dismiss — Dismiss a rejected submission
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-06 11:44:17 -06:00
// PATCH /submissions/:id/status — Update lifecycle status
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 ;