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.
This commit is contained in:
@@ -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<object>} 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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user