2026-06-02 16:08:25 -06:00
// routes/archerTemplates.js
const express = require ( 'express' ) ;
const pool = require ( '../db' ) ;
const { requireAuth , requireGroup } = require ( '../middleware/auth' ) ;
const logAudit = require ( '../helpers/auditLog' ) ;
// Section fields and their max length
const SECTION _FIELDS = [
'environment_overview' ,
'segmentation' ,
'mitigating_controls' ,
'additional_info' ,
'charter_network_banner' ,
'data_classification' ,
'charter_network' ,
'additional_access_list'
] ;
const SECTION _MAX _LENGTH = 10000 ;
function createArcherTemplatesRouter ( ) {
const router = express . Router ( ) ;
Add page visibility by group with centralized matrix
Introduce a Page Visibility Matrix that controls which pages each user
group can access, enforced in both frontend and backend:
Frontend:
- Create frontend/src/config/pageVisibility.js with PAGE_VISIBILITY
matrix and canAccessPage() / getAccessiblePages() helpers
- NavDrawer: replace inline requiredGroups with canAccessPage() filter
- App.js: replace per-page isInGroup()/isAdmin() checks with generic
route guard in setCurrentPage; remove VALID_PAGES constant
- localStorage validation: verify persisted page is accessible on load
Backend (page-level access enforcement):
- jiraTickets.js: add router-level requireGroup('Admin','Standard_User')
- archerTemplates.js: add router-level requireGroup('Admin','Standard_User')
- VCL multi-vertical already had requireGroup('Admin','Leadership')
Visibility matrix:
- Home, Knowledge Base: all groups
- Triage, Compliance, Exports: Admin, Standard_User, Leadership
- CCP Metrics: Admin, Leadership
- Jira, Archer Templates: Admin, Standard_User
- Admin Panel: Admin only
- Read_Only sees only Home and Knowledge Base
2026-06-24 11:41:50 -06:00
// All Archer template routes require authentication and Admin or Standard_User group (page-level access)
router . use ( requireAuth ( ) ) ;
router . use ( requireGroup ( 'Admin' , 'Standard_User' ) ) ;
2026-06-02 16:08:25 -06:00
// --- Hierarchy endpoints (MUST be defined before /:id to avoid route conflicts) ---
/ * *
* GET / api / archer - templates / hierarchy / vendors
*
* Returns a sorted array of distinct vendor names across all templates .
*
* @ returns { string [ ] } 200 - Array of vendor names sorted alphabetically
* @ returns { object } 500 - { error : 'Internal server error' }
* /
router . get ( '/hierarchy/vendors' , requireAuth ( ) , async ( req , res ) => {
try {
const { rows } = await pool . query (
'SELECT DISTINCT vendor FROM archer_templates ORDER BY vendor ASC'
) ;
res . json ( rows . map ( r => r . vendor ) ) ;
} catch ( err ) {
console . error ( 'Error fetching template vendors:' , err ) ;
res . status ( 500 ) . json ( { error : 'Internal server error' } ) ;
}
} ) ;
/ * *
* GET / api / archer - templates / hierarchy / platforms
*
* Returns a sorted array of distinct platform names for a given vendor .
*
* @ query { string } vendor - ( required ) The vendor to filter platforms by
* @ returns { string [ ] } 200 - Array of platform names sorted alphabetically
* @ returns { object } 400 - { error : 'vendor query parameter is required' }
* @ returns { object } 500 - { error : 'Internal server error' }
* /
router . get ( '/hierarchy/platforms' , requireAuth ( ) , async ( req , res ) => {
const { vendor } = req . query ;
if ( ! vendor ) {
return res . status ( 400 ) . json ( { error : 'vendor query parameter is required' } ) ;
}
try {
const { rows } = await pool . query (
'SELECT DISTINCT platform FROM archer_templates WHERE LOWER(TRIM(vendor)) = LOWER(TRIM($1)) ORDER BY platform ASC' ,
[ vendor ]
) ;
res . json ( rows . map ( r => r . platform ) ) ;
} catch ( err ) {
console . error ( 'Error fetching template platforms:' , err ) ;
res . status ( 500 ) . json ( { error : 'Internal server error' } ) ;
}
} ) ;
/ * *
* GET / api / archer - templates / hierarchy / models
*
* Returns a sorted array of distinct model names for a given vendor and platform .
*
* @ query { string } vendor - ( required ) The vendor to filter by
* @ query { string } platform - ( required ) The platform to filter by
* @ returns { string [ ] } 200 - Array of model names sorted alphabetically
* @ returns { object } 400 - { error : 'Missing required query parameters: ...' }
* @ returns { object } 500 - { error : 'Internal server error' }
* /
router . get ( '/hierarchy/models' , requireAuth ( ) , async ( req , res ) => {
const { vendor , platform } = req . query ;
const missing = [ ] ;
if ( ! vendor ) missing . push ( 'vendor' ) ;
if ( ! platform ) missing . push ( 'platform' ) ;
if ( missing . length > 0 ) {
return res . status ( 400 ) . json ( { error : ` Missing required query parameters: ${ missing . join ( ', ' ) } ` } ) ;
}
try {
const { rows } = await pool . query (
'SELECT DISTINCT model FROM archer_templates WHERE LOWER(TRIM(vendor)) = LOWER(TRIM($1)) AND LOWER(TRIM(platform)) = LOWER(TRIM($2)) ORDER BY model ASC' ,
[ vendor , platform ]
) ;
res . json ( rows . map ( r => r . model ) ) ;
} catch ( err ) {
console . error ( 'Error fetching template models:' , err ) ;
res . status ( 500 ) . json ( { error : 'Internal server error' } ) ;
}
} ) ;
// --- Core CRUD endpoints ---
/ * *
* POST / api / archer - templates
*
* Creates a new Archer template with vendor / platform / model hierarchy and section content .
* Requires Admin or Standard _User group .
*
* @ body { string } vendor - ( required ) Vendor name , 1 - 100 chars after trim
* @ body { string } platform - ( required ) Platform name , 1 - 100 chars after trim
* @ body { string } model - ( required ) Model name , 1 - 100 chars after trim
* @ body { string } [ environment _overview ] - Section content , max 10 , 000 chars
* @ body { string } [ segmentation ] - Section content , max 10 , 000 chars
* @ body { string } [ mitigating _controls ] - Section content , max 10 , 000 chars
* @ body { string } [ additional _info ] - Section content , max 10 , 000 chars
* @ body { string } [ charter _network _banner ] - Section content , max 10 , 000 chars
* @ body { string } [ data _classification ] - Section content , max 10 , 000 chars
* @ body { string } [ charter _network ] - Section content , max 10 , 000 chars
* @ body { string } [ additional _access _list ] - Section content , max 10 , 000 chars
* @ returns { object } 201 - The created template record ( all columns )
* @ returns { object } 400 - { error : 'validation message' }
* @ returns { object } 409 - { error : 'A template with this vendor/platform/model combination already exists' }
* @ returns { object } 500 - { error : 'Internal server error' }
* /
router . post ( '/' , requireAuth ( ) , requireGroup ( 'Admin' , 'Standard_User' ) , async ( req , res ) => {
const { vendor , platform , model } = req . body ;
// Validate vendor, platform, model — required, 1-100 chars after trim, non-empty after trim
const errors = [ ] ;
for ( const [ field , value ] of [ [ 'vendor' , vendor ] , [ 'platform' , platform ] , [ 'model' , model ] ] ) {
if ( value === undefined || value === null || typeof value !== 'string' || value . trim ( ) . length === 0 ) {
errors . push ( ` ${ field } is required ` ) ;
} else if ( value . trim ( ) . length > 100 ) {
errors . push ( ` ${ field } must be 100 characters or fewer ` ) ;
}
}
if ( errors . length > 0 ) {
return res . status ( 400 ) . json ( { error : errors . join ( '; ' ) } ) ;
}
// Validate section fields — max 10,000 chars each, default to empty string
const sectionValues = { } ;
for ( const field of SECTION _FIELDS ) {
const val = req . body [ field ] ;
if ( val !== undefined && val !== null && typeof val === 'string' ) {
if ( val . length > SECTION _MAX _LENGTH ) {
return res . status ( 400 ) . json ( { error : ` ${ field } must be 10,000 characters or fewer ` } ) ;
}
sectionValues [ field ] = val ;
} else {
sectionValues [ field ] = '' ;
}
}
try {
const { rows } = await pool . query (
` INSERT INTO archer_templates (vendor, platform, model, environment_overview, segmentation, mitigating_controls, additional_info, charter_network_banner, data_classification, charter_network, additional_access_list, created_by)
VALUES ( $1 , $2 , $3 , $4 , $5 , $6 , $7 , $8 , $9 , $10 , $11 , $12 )
RETURNING * ` ,
[
vendor . trim ( ) ,
platform . trim ( ) ,
model . trim ( ) ,
sectionValues . environment _overview ,
sectionValues . segmentation ,
sectionValues . mitigating _controls ,
sectionValues . additional _info ,
sectionValues . charter _network _banner ,
sectionValues . data _classification ,
sectionValues . charter _network ,
sectionValues . additional _access _list ,
req . user . id
]
) ;
// Fire-and-forget audit log
logAudit ( {
userId : req . user . id ,
username : req . user . username ,
action : 'template_created' ,
entityType : 'archer_template' ,
entityId : String ( rows [ 0 ] . id ) ,
details : { vendor : vendor . trim ( ) , platform : platform . trim ( ) , model : model . trim ( ) } ,
ipAddress : req . ip
} ) ;
res . status ( 201 ) . json ( rows [ 0 ] ) ;
} catch ( err ) {
if ( err . code === '23505' ) {
return res . status ( 409 ) . json ( { error : 'A template with this vendor/platform/model combination already exists' } ) ;
}
console . error ( 'Error creating template:' , err ) ;
res . status ( 500 ) . json ( { error : 'Internal server error' } ) ;
}
} ) ;
/ * *
* GET / api / archer - templates
*
* Lists all templates with optional search and exact - match filters .
* Results are sorted by vendor , platform , model ( ascending ) .
*
* @ query { string } [ search ] - Substring search across vendor , platform , and model ( ILIKE )
* @ query { string } [ vendor ] - Exact - match filter on vendor ( case - insensitive )
* @ query { string } [ platform ] - Exact - match filter on platform ( case - insensitive )
* @ query { string } [ model ] - Exact - match filter on model ( case - insensitive )
* @ returns { object [ ] } 200 - Array of template records sorted by vendor / platform / model
* @ returns { object } 500 - { error : 'Internal server error' }
* /
router . get ( '/' , requireAuth ( ) , async ( req , res ) => {
const { search , vendor , platform , model } = req . query ;
let query = 'SELECT * FROM archer_templates WHERE 1=1' ;
const params = [ ] ;
let paramIndex = 1 ;
// Search — ILIKE substring match across vendor, platform, model
const trimmedSearch = search ? search . trim ( ) : '' ;
if ( trimmedSearch . length > 0 ) {
query += ` AND (vendor ILIKE $ ${ paramIndex } OR platform ILIKE $ ${ paramIndex } OR model ILIKE $ ${ paramIndex } ) ` ;
params . push ( ` % ${ trimmedSearch } % ` ) ;
paramIndex ++ ;
}
// Exact-match filters (case-insensitive via LOWER/TRIM)
if ( vendor ) {
query += ` AND LOWER(TRIM(vendor)) = LOWER(TRIM( $ ${ paramIndex } )) ` ;
params . push ( vendor ) ;
paramIndex ++ ;
}
if ( platform ) {
query += ` AND LOWER(TRIM(platform)) = LOWER(TRIM( $ ${ paramIndex } )) ` ;
params . push ( platform ) ;
paramIndex ++ ;
}
if ( model ) {
query += ` AND LOWER(TRIM(model)) = LOWER(TRIM( $ ${ paramIndex } )) ` ;
params . push ( model ) ;
paramIndex ++ ;
}
query += ' ORDER BY vendor ASC, platform ASC, model ASC' ;
try {
const { rows } = await pool . query ( query , params ) ;
res . json ( rows ) ;
} catch ( err ) {
console . error ( 'Error fetching templates:' , err ) ;
res . status ( 500 ) . json ( { error : 'Internal server error' } ) ;
}
} ) ;
/ * *
* POST / api / archer - templates / : id / clone
*
* Clones an existing template ' s section content into a new template with different
* vendor / platform / model hierarchy values . Requires Admin or Standard _User group .
*
* @ param { number } id - The ID of the source template to clone from
* @ body { string } vendor - ( required ) New vendor name , 1 - 100 chars after trim
* @ body { string } platform - ( required ) New platform name , 1 - 100 chars after trim
* @ body { string } model - ( required ) New model name , 1 - 100 chars after trim
* @ returns { object } 201 - The newly created cloned template record
* @ returns { object } 400 - { error : 'validation message' }
* @ returns { object } 404 - { error : 'Template not found' }
* @ returns { object } 409 - { error : 'A template with this vendor/platform/model combination already exists' }
* @ returns { object } 500 - { error : 'Internal server error' }
* /
router . post ( '/:id/clone' , requireAuth ( ) , requireGroup ( 'Admin' , 'Standard_User' ) , async ( req , res ) => {
const { id } = req . params ;
const { vendor , platform , model } = req . body ;
// Validate vendor, platform, model — required, 1-100 chars after trim, non-empty after trim
const errors = [ ] ;
for ( const [ field , value ] of [ [ 'vendor' , vendor ] , [ 'platform' , platform ] , [ 'model' , model ] ] ) {
if ( value === undefined || value === null || typeof value !== 'string' || value . trim ( ) . length === 0 ) {
errors . push ( ` ${ field } is required ` ) ;
} else if ( value . trim ( ) . length > 100 ) {
errors . push ( ` ${ field } must be 100 characters or fewer ` ) ;
}
}
if ( errors . length > 0 ) {
return res . status ( 400 ) . json ( { error : errors . join ( '; ' ) } ) ;
}
try {
// Verify source template exists
const { rows : sourceRows } = await pool . query ( 'SELECT * FROM archer_templates WHERE id = $1' , [ id ] ) ;
if ( sourceRows . length === 0 ) {
return res . status ( 404 ) . json ( { error : 'Template not found' } ) ;
}
const source = sourceRows [ 0 ] ;
// INSERT copying all 8 section fields from source with new hierarchy values
const { rows } = await pool . query (
` INSERT INTO archer_templates (vendor, platform, model, environment_overview, segmentation, mitigating_controls, additional_info, charter_network_banner, data_classification, charter_network, additional_access_list, created_by)
VALUES ( $1 , $2 , $3 , $4 , $5 , $6 , $7 , $8 , $9 , $10 , $11 , $12 )
RETURNING * ` ,
[
vendor . trim ( ) ,
platform . trim ( ) ,
model . trim ( ) ,
source . environment _overview ,
source . segmentation ,
source . mitigating _controls ,
source . additional _info ,
source . charter _network _banner ,
source . data _classification ,
source . charter _network ,
source . additional _access _list ,
req . user . id
]
) ;
// Fire-and-forget audit log
logAudit ( {
userId : req . user . id ,
username : req . user . username ,
action : 'template_cloned' ,
entityType : 'archer_template' ,
entityId : String ( rows [ 0 ] . id ) ,
details : { sourceId : Number ( id ) , newId : rows [ 0 ] . id , vendor : vendor . trim ( ) , platform : platform . trim ( ) , model : model . trim ( ) } ,
ipAddress : req . ip
} ) ;
res . status ( 201 ) . json ( rows [ 0 ] ) ;
} catch ( err ) {
if ( err . code === '23505' ) {
return res . status ( 409 ) . json ( { error : 'A template with this vendor/platform/model combination already exists' } ) ;
}
console . error ( 'Error cloning template:' , err ) ;
res . status ( 500 ) . json ( { error : 'Internal server error' } ) ;
}
} ) ;
/ * *
* GET / api / archer - templates / : id
*
* Fetches a single template by its ID .
*
* @ param { number } id - The template ID
* @ returns { object } 200 - The template record
* @ returns { object } 404 - { error : 'Template not found' }
* @ returns { object } 500 - { error : 'Internal server error' }
* /
router . get ( '/:id' , requireAuth ( ) , async ( req , res ) => {
const { id } = req . params ;
try {
const { rows } = await pool . query ( 'SELECT * FROM archer_templates WHERE id = $1' , [ id ] ) ;
if ( rows . length === 0 ) {
return res . status ( 404 ) . json ( { error : 'Template not found' } ) ;
}
res . json ( rows [ 0 ] ) ;
} catch ( err ) {
console . error ( 'Error fetching template:' , err ) ;
res . status ( 500 ) . json ( { error : 'Internal server error' } ) ;
}
} ) ;
/ * *
* PUT / api / archer - templates / : id
*
* Updates an existing template . Supports partial updates — only provided fields are changed .
* Always updates ` updated_at ` to NOW ( ) . Requires Admin or Standard _User group .
*
* @ param { number } id - The template ID to update
* @ body { string } [ vendor ] - New vendor name , 1 - 100 chars after trim
* @ body { string } [ platform ] - New platform name , 1 - 100 chars after trim
* @ body { string } [ model ] - New model name , 1 - 100 chars after trim
* @ body { string } [ environment _overview ] - Section content , max 10 , 000 chars
* @ body { string } [ segmentation ] - Section content , max 10 , 000 chars
* @ body { string } [ mitigating _controls ] - Section content , max 10 , 000 chars
* @ body { string } [ additional _info ] - Section content , max 10 , 000 chars
* @ body { string } [ charter _network _banner ] - Section content , max 10 , 000 chars
* @ body { string } [ data _classification ] - Section content , max 10 , 000 chars
* @ body { string } [ charter _network ] - Section content , max 10 , 000 chars
* @ body { string } [ additional _access _list ] - Section content , max 10 , 000 chars
* @ returns { object } 200 - The updated template record
* @ returns { object } 400 - { error : 'validation message' }
* @ returns { object } 404 - { error : 'Template not found' }
* @ returns { object } 409 - { error : 'A template with this vendor/platform/model combination already exists' }
* @ returns { object } 500 - { error : 'Internal server error' }
* /
router . put ( '/:id' , requireAuth ( ) , requireGroup ( 'Admin' , 'Standard_User' ) , async ( req , res ) => {
const { id } = req . params ;
try {
// Verify template exists
const { rows : existingRows } = await pool . query ( 'SELECT * FROM archer_templates WHERE id = $1' , [ id ] ) ;
if ( existingRows . length === 0 ) {
return res . status ( 404 ) . json ( { error : 'Template not found' } ) ;
}
const existing = existingRows [ 0 ] ;
// Validate provided hierarchy fields
const errors = [ ] ;
const updatedFields = { } ;
const changedFieldNames = [ ] ;
for ( const field of [ 'vendor' , 'platform' , 'model' ] ) {
const value = req . body [ field ] ;
if ( value !== undefined ) {
if ( value === null || typeof value !== 'string' || value . trim ( ) . length === 0 ) {
errors . push ( ` ${ field } is required ` ) ;
} else if ( value . trim ( ) . length > 100 ) {
errors . push ( ` ${ field } must be 100 characters or fewer ` ) ;
} else {
updatedFields [ field ] = value . trim ( ) ;
if ( value . trim ( ) !== existing [ field ] ) {
changedFieldNames . push ( field ) ;
}
}
}
}
// Validate provided section fields
for ( const field of SECTION _FIELDS ) {
const val = req . body [ field ] ;
if ( val !== undefined ) {
if ( val !== null && typeof val === 'string' ) {
if ( val . length > SECTION _MAX _LENGTH ) {
errors . push ( ` ${ field } must be 10,000 characters or fewer ` ) ;
} else {
updatedFields [ field ] = val ;
if ( val !== existing [ field ] ) {
changedFieldNames . push ( field ) ;
}
}
} else {
updatedFields [ field ] = '' ;
if ( '' !== existing [ field ] ) {
changedFieldNames . push ( field ) ;
}
}
}
}
if ( errors . length > 0 ) {
return res . status ( 400 ) . json ( { error : errors . join ( '; ' ) } ) ;
}
// Check uniqueness if vendor/platform/model changed (excluding self)
const newVendor = updatedFields . vendor || existing . vendor ;
const newPlatform = updatedFields . platform || existing . platform ;
const newModel = updatedFields . model || existing . model ;
if ( updatedFields . vendor !== undefined || updatedFields . platform !== undefined || updatedFields . model !== undefined ) {
const { rows : conflictRows } = await pool . query (
` SELECT id FROM archer_templates WHERE LOWER(TRIM(vendor)) = LOWER(TRIM( $ 1)) AND LOWER(TRIM(platform)) = LOWER(TRIM( $ 2)) AND LOWER(TRIM(model)) = LOWER(TRIM( $ 3)) AND id != $ 4 ` ,
[ newVendor , newPlatform , newModel , id ]
) ;
if ( conflictRows . length > 0 ) {
return res . status ( 409 ) . json ( { error : 'A template with this vendor/platform/model combination already exists' } ) ;
}
}
// Build dynamic UPDATE SET clause for only provided fields
const setClauses = [ ] ;
const params = [ ] ;
let paramIndex = 1 ;
for ( const [ field , value ] of Object . entries ( updatedFields ) ) {
setClauses . push ( ` ${ field } = $ ${ paramIndex } ` ) ;
params . push ( value ) ;
paramIndex ++ ;
}
// Always set updated_at = NOW()
setClauses . push ( ` updated_at = NOW() ` ) ;
// Execute update
params . push ( id ) ;
const { rows } = await pool . query (
` UPDATE archer_templates SET ${ setClauses . join ( ', ' ) } WHERE id = $ ${ paramIndex } RETURNING * ` ,
params
) ;
// Fire-and-forget audit log
logAudit ( {
userId : req . user . id ,
username : req . user . username ,
action : 'template_updated' ,
entityType : 'archer_template' ,
entityId : String ( id ) ,
details : { changedFields : changedFieldNames } ,
ipAddress : req . ip
} ) ;
res . json ( rows [ 0 ] ) ;
} catch ( err ) {
console . error ( 'Error updating template:' , err ) ;
res . status ( 500 ) . json ( { error : 'Internal server error' } ) ;
}
} ) ;
/ * *
* DELETE / api / archer - templates / : id
*
* Permanently deletes a template . Requires Admin or Standard _User group .
*
* @ param { number } id - The template ID to delete
* @ returns { object } 200 - { message : 'Template deleted successfully' }
* @ returns { object } 404 - { error : 'Template not found' }
* @ returns { object } 500 - { error : 'Internal server error' }
* /
router . delete ( '/:id' , requireAuth ( ) , requireGroup ( 'Admin' , 'Standard_User' ) , async ( req , res ) => {
const { id } = req . params ;
try {
// Verify template exists
const { rows : existingRows } = await pool . query ( 'SELECT * FROM archer_templates WHERE id = $1' , [ id ] ) ;
if ( existingRows . length === 0 ) {
return res . status ( 404 ) . json ( { error : 'Template not found' } ) ;
}
const existing = existingRows [ 0 ] ;
// Delete the template
await pool . query ( 'DELETE FROM archer_templates WHERE id = $1' , [ id ] ) ;
// Fire-and-forget audit log
logAudit ( {
userId : req . user . id ,
username : req . user . username ,
action : 'template_deleted' ,
entityType : 'archer_template' ,
entityId : String ( id ) ,
details : { vendor : existing . vendor , platform : existing . platform , model : existing . model } ,
ipAddress : req . ip
} ) ;
res . json ( { message : 'Template deleted successfully' } ) ;
} catch ( err ) {
console . error ( 'Error deleting template:' , err ) ;
res . status ( 500 ) . json ( { error : 'Internal server error' } ) ;
}
} ) ;
return router ;
}
module . exports = createArcherTemplatesRouter ;