From 93efb70d1c7472f71a04cb02f4d5cc0fea55fd23 Mon Sep 17 00:00:00 2001 From: Jordan Ramos Date: Tue, 16 Jun 2026 13:23:20 -0600 Subject: [PATCH] Fix KB content/download failing for relative file paths res.sendFile requires an absolute path. Article #7 was stored with a relative path which caused the TypeError. Now both the content and download endpoints resolve relative paths against the backend directory before calling existsSync and sendFile. --- backend/routes/knowledgeBase.js | 112 +++++++++++++++++++++++++++++--- 1 file changed, 102 insertions(+), 10 deletions(-) diff --git a/backend/routes/knowledgeBase.js b/backend/routes/knowledgeBase.js index 87a5553..8ae8c4a 100644 --- a/backend/routes/knowledgeBase.js +++ b/backend/routes/knowledgeBase.js @@ -40,7 +40,22 @@ function createKnowledgeBaseRouter(upload) { return ALLOWED_EXTENSIONS.has(ext); } - // POST /api/knowledge-base/upload + /** + * POST /api/knowledge-base/upload + * + * Uploads a new knowledge base document. + * + * @body {FormData} file - The file to upload (max 10MB) + * @body {string} title - Document title (required) + * @body {string} [description] - Optional description + * @body {string} [category] - Category name (defaults to 'General') + * + * @returns {object} 200 - { success: true, id, title, slug, category } + * @returns {object} 400 - { error } if title missing, no file, or invalid file type + * @returns {object} 500 - { error } on server failure + * + * @requires Admin | Standard_User + */ router.post('/upload', requireAuth(), requireGroup('Admin', 'Standard_User'), (req, res, next) => { upload.single('file')(req, res, (err) => { if (err) { @@ -144,7 +159,18 @@ function createKnowledgeBaseRouter(upload) { } }); - // GET /api/knowledge-base + /** + * GET /api/knowledge-base + * + * Lists all knowledge base articles, ordered by creation date descending. + * + * @returns {Array} 200 - Array of articles with fields: + * id, title, slug, description, category, file_name, file_type, + * file_size, created_at, updated_at, created_by_username + * @returns {object} 500 - { error } on server failure + * + * @requires Authenticated user (any group) + */ router.get('/', requireAuth(), async (req, res) => { try { const { rows } = await pool.query(` @@ -163,7 +189,21 @@ function createKnowledgeBaseRouter(upload) { } }); - // GET /api/knowledge-base/:id + /** + * GET /api/knowledge-base/:id + * + * Retrieves metadata for a single knowledge base article. + * + * @param {number} id - Article ID (route param) + * + * @returns {object} 200 - Article metadata: id, title, slug, description, + * category, file_name, file_type, file_size, created_at, updated_at, + * created_by_username + * @returns {object} 404 - { error: 'Article not found' } + * @returns {object} 500 - { error } on server failure + * + * @requires Authenticated user (any group) + */ router.get('/:id', requireAuth(), async (req, res) => { const { id } = req.params; @@ -189,7 +229,21 @@ function createKnowledgeBaseRouter(upload) { } }); - // GET /api/knowledge-base/:id/content + /** + * GET /api/knowledge-base/:id/content + * + * Serves the raw file content inline for rendering in the browser. + * Sets Content-Security-Policy frame-ancestors for iframe embedding. + * Logs a VIEW_KB_ARTICLE audit event. + * + * @param {number} id - Article ID (route param) + * + * @returns {file} 200 - Raw file content with appropriate Content-Type + * @returns {object} 404 - { error } if article or file not found + * @returns {object} 500 - { error } on server failure + * + * @requires Authenticated user (any group) + */ router.get('/:id/content', requireAuth(), async (req, res) => { const { id } = req.params; @@ -203,7 +257,12 @@ function createKnowledgeBaseRouter(upload) { return res.status(404).json({ error: 'Document not found' }); } - if (!fs.existsSync(row.file_path)) { + // Resolve relative paths against the backend directory + const absoluteFilePath = path.isAbsolute(row.file_path) + ? row.file_path + : path.resolve(path.join(__dirname, '..'), row.file_path); + + if (!fs.existsSync(absoluteFilePath)) { return res.status(404).json({ error: 'File not found on disk' }); } @@ -230,14 +289,27 @@ function createKnowledgeBaseRouter(upload) { res.removeHeader('X-Frame-Options'); const corsOrigins = process.env.CORS_ORIGINS ? process.env.CORS_ORIGINS.split(',').join(' ') : 'http://localhost:3000'; res.setHeader('Content-Security-Policy', `frame-ancestors 'self' ${corsOrigins}`); - res.sendFile(row.file_path); + res.sendFile(absoluteFilePath); } catch (err) { console.error('Error fetching document:', err); res.status(500).json({ error: 'Failed to fetch document' }); } }); - // GET /api/knowledge-base/:id/download + /** + * GET /api/knowledge-base/:id/download + * + * Downloads the file as an attachment (Content-Disposition: attachment). + * Logs a DOWNLOAD_KB_ARTICLE audit event. + * + * @param {number} id - Article ID (route param) + * + * @returns {file} 200 - File content with attachment disposition + * @returns {object} 404 - { error } if article or file not found + * @returns {object} 500 - { error } on server failure + * + * @requires Authenticated user (any group) + */ router.get('/:id/download', requireAuth(), async (req, res) => { const { id } = req.params; @@ -251,7 +323,12 @@ function createKnowledgeBaseRouter(upload) { return res.status(404).json({ error: 'Document not found' }); } - if (!fs.existsSync(row.file_path)) { + // Resolve relative paths against the backend directory + const absoluteFilePath = path.isAbsolute(row.file_path) + ? row.file_path + : path.resolve(path.join(__dirname, '..'), row.file_path); + + if (!fs.existsSync(absoluteFilePath)) { return res.status(404).json({ error: 'File not found on disk' }); } @@ -268,14 +345,29 @@ function createKnowledgeBaseRouter(upload) { const safeDownloadName = row.file_name.replace(/["\r\n\\]/g, ''); res.setHeader('Content-Type', row.file_type || 'application/octet-stream'); res.setHeader('Content-Disposition', `attachment; filename="${safeDownloadName}"`); - res.sendFile(row.file_path); + res.sendFile(absoluteFilePath); } catch (err) { console.error('Error fetching document:', err); res.status(500).json({ error: 'Failed to fetch document' }); } }); - // DELETE /api/knowledge-base/:id + /** + * DELETE /api/knowledge-base/:id + * + * Deletes a knowledge base article and its associated file from disk. + * Standard_User can only delete articles they created; Admin can delete any. + * Logs a DELETE_KB_ARTICLE audit event. + * + * @param {number} id - Article ID (route param) + * + * @returns {object} 200 - { success: true } + * @returns {object} 403 - { error } if Standard_User tries to delete another user's article + * @returns {object} 404 - { error: 'Article not found' } + * @returns {object} 500 - { error } on server failure + * + * @requires Admin | Standard_User + */ router.delete('/:id', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => { const { id } = req.params;