feat(postgres): migrate all route files from SQLite to pg pool
- All 16 route files now import pool from ../db directly
- Removed db parameter from all factory functions
- All callbacks replaced with async/await pool.query()
- All ? placeholders converted to $1, $2... numbered params
- datetime('now') → NOW(), INSERT OR IGNORE → ON CONFLICT DO NOTHING
- LIKE → ILIKE for case-insensitive searches
- Error detection: err.code === '23505' for unique violations
- server.js no longer passes pool/db/requireAuth to route factories
- Only ivantiFindings.js still receives pool (pending task 8 rewrite)
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const pool = require('../db');
|
||||
const { requireAuth, requireGroup } = require('../middleware/auth');
|
||||
const logAudit = require('../helpers/auditLog');
|
||||
|
||||
function createKnowledgeBaseRouter(db, upload) {
|
||||
function createKnowledgeBaseRouter(upload) {
|
||||
const router = express.Router();
|
||||
|
||||
// Helper to sanitize filename
|
||||
@@ -39,20 +40,8 @@ function createKnowledgeBaseRouter(db, upload) {
|
||||
return ALLOWED_EXTENSIONS.has(ext);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/knowledge-base/upload
|
||||
* Upload a new knowledge base document.
|
||||
*
|
||||
* @body {string} title - Article title (required)
|
||||
* @body {string} [description] - Article description
|
||||
* @body {string} [category] - Article category (defaults to 'General')
|
||||
* @body {File} file - The document file to upload (multipart/form-data)
|
||||
*
|
||||
* @response 200 - { success: true, id: number, title: string, slug: string, category: string }
|
||||
* @response 400 - { error: string } - Missing title, no file, or invalid file type
|
||||
* @response 500 - { error: string } - Database or filesystem error
|
||||
*/
|
||||
router.post('/upload', requireAuth(db), requireGroup('Admin', 'Standard_User'), (req, res, next) => {
|
||||
// POST /api/knowledge-base/upload
|
||||
router.post('/upload', requireAuth(), requireGroup('Admin', 'Standard_User'), (req, res, next) => {
|
||||
upload.single('file')(req, res, (err) => {
|
||||
if (err) {
|
||||
console.error('[KB Upload] Multer error:', err);
|
||||
@@ -70,7 +59,6 @@ function createKnowledgeBaseRouter(db, upload) {
|
||||
const uploadedFile = req.file;
|
||||
const { title, description, category } = req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (!title || !title.trim()) {
|
||||
console.error('[KB Upload] Error: Title is missing');
|
||||
if (uploadedFile) fs.unlinkSync(uploadedFile.path);
|
||||
@@ -81,7 +69,6 @@ function createKnowledgeBaseRouter(db, upload) {
|
||||
return res.status(400).json({ error: 'No file uploaded' });
|
||||
}
|
||||
|
||||
// Validate file type
|
||||
if (!isValidFileType(uploadedFile.originalname)) {
|
||||
fs.unlinkSync(uploadedFile.path);
|
||||
return res.status(400).json({ error: 'File type not allowed' });
|
||||
@@ -96,172 +83,121 @@ function createKnowledgeBaseRouter(db, upload) {
|
||||
const filePath = path.join(kbDir, filename);
|
||||
|
||||
try {
|
||||
// Keep file in temp location until DB insert succeeds
|
||||
// Check if slug already exists
|
||||
db.get('SELECT id FROM knowledge_base WHERE slug = ?', [slug], (err, row) => {
|
||||
if (err) {
|
||||
fs.unlinkSync(uploadedFile.path);
|
||||
console.error('Error checking slug:', err);
|
||||
return res.status(500).json({ error: 'Database error' });
|
||||
const { rows: existingRows } = await pool.query(
|
||||
'SELECT id FROM knowledge_base WHERE slug = $1', [slug]
|
||||
);
|
||||
|
||||
const finalSlug = existingRows.length > 0 ? `${slug}-${timestamp}` : slug;
|
||||
|
||||
// Insert new knowledge base entry
|
||||
const { rows } = await pool.query(
|
||||
`INSERT INTO knowledge_base (
|
||||
title, slug, description, category, file_path, file_name,
|
||||
file_type, file_size, created_by
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING id`,
|
||||
[
|
||||
title.trim(),
|
||||
finalSlug,
|
||||
description || null,
|
||||
category || 'General',
|
||||
filePath,
|
||||
sanitizedName,
|
||||
uploadedFile.mimetype,
|
||||
uploadedFile.size,
|
||||
req.user.id
|
||||
]
|
||||
);
|
||||
|
||||
// DB insert succeeded — now move file to permanent location
|
||||
try {
|
||||
if (!fs.existsSync(kbDir)) {
|
||||
fs.mkdirSync(kbDir, { recursive: true });
|
||||
}
|
||||
fs.renameSync(uploadedFile.path, filePath);
|
||||
} catch (moveErr) {
|
||||
console.error('Error moving file to permanent location:', moveErr);
|
||||
}
|
||||
|
||||
// If slug exists, append timestamp to make it unique
|
||||
const finalSlug = row ? `${slug}-${timestamp}` : slug;
|
||||
logAudit({
|
||||
userId: req.user.id,
|
||||
username: req.user.username,
|
||||
action: 'CREATE_KB_ARTICLE',
|
||||
entityType: 'knowledge_base',
|
||||
entityId: String(rows[0].id),
|
||||
details: { title: title.trim(), filename: sanitizedName },
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
// Insert new knowledge base entry
|
||||
const insertSql = `
|
||||
INSERT INTO knowledge_base (
|
||||
title, slug, description, category, file_path, file_name,
|
||||
file_type, file_size, created_by
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
db.run(
|
||||
insertSql,
|
||||
[
|
||||
title.trim(),
|
||||
finalSlug,
|
||||
description || null,
|
||||
category || 'General',
|
||||
filePath,
|
||||
sanitizedName,
|
||||
uploadedFile.mimetype,
|
||||
uploadedFile.size,
|
||||
req.user.id
|
||||
],
|
||||
function (err) {
|
||||
if (err) {
|
||||
fs.unlinkSync(uploadedFile.path);
|
||||
console.error('Error inserting knowledge base entry:', err);
|
||||
return res.status(500).json({ error: 'Failed to save document metadata' });
|
||||
}
|
||||
|
||||
// DB insert succeeded — now move file to permanent location
|
||||
try {
|
||||
if (!fs.existsSync(kbDir)) {
|
||||
fs.mkdirSync(kbDir, { recursive: true });
|
||||
}
|
||||
fs.renameSync(uploadedFile.path, filePath);
|
||||
} catch (moveErr) {
|
||||
console.error('Error moving file to permanent location:', moveErr);
|
||||
// File is orphaned in temp but DB record exists — log and continue
|
||||
}
|
||||
|
||||
// Log audit entry
|
||||
logAudit(db, {
|
||||
userId: req.user.id,
|
||||
username: req.user.username,
|
||||
action: 'CREATE_KB_ARTICLE',
|
||||
entityType: 'knowledge_base',
|
||||
entityId: String(this.lastID),
|
||||
details: { title: title.trim(), filename: sanitizedName },
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
id: this.lastID,
|
||||
title: title.trim(),
|
||||
slug: finalSlug,
|
||||
category: category || 'General'
|
||||
});
|
||||
}
|
||||
);
|
||||
res.json({
|
||||
success: true,
|
||||
id: rows[0].id,
|
||||
title: title.trim(),
|
||||
slug: finalSlug,
|
||||
category: category || 'General'
|
||||
});
|
||||
} catch (error) {
|
||||
// Clean up temp file on error
|
||||
if (uploadedFile && fs.existsSync(uploadedFile.path)) fs.unlinkSync(uploadedFile.path);
|
||||
console.error('Error uploading knowledge base document:', error);
|
||||
res.status(500).json({ error: error.message || 'Failed to upload document' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/knowledge-base
|
||||
* List all knowledge base articles.
|
||||
*
|
||||
* @response 200 - Array of article objects: [{ id, title, slug, description, category, file_name, file_type, file_size, created_at, updated_at, created_by_username }]
|
||||
* @response 500 - { error: string }
|
||||
*/
|
||||
router.get('/', requireAuth(db), (req, res) => {
|
||||
const sql = `
|
||||
SELECT
|
||||
kb.id, kb.title, kb.slug, kb.description, kb.category,
|
||||
kb.file_name, kb.file_type, kb.file_size, kb.created_at, kb.updated_at,
|
||||
u.username as created_by_username
|
||||
FROM knowledge_base kb
|
||||
LEFT JOIN users u ON kb.created_by = u.id
|
||||
ORDER BY kb.created_at DESC
|
||||
`;
|
||||
|
||||
db.all(sql, [], (err, rows) => {
|
||||
if (err) {
|
||||
console.error('Error fetching knowledge base articles:', err);
|
||||
return res.status(500).json({ error: 'Failed to fetch articles' });
|
||||
}
|
||||
|
||||
// GET /api/knowledge-base
|
||||
router.get('/', requireAuth(), async (req, res) => {
|
||||
try {
|
||||
const { rows } = await pool.query(`
|
||||
SELECT
|
||||
kb.id, kb.title, kb.slug, kb.description, kb.category,
|
||||
kb.file_name, kb.file_type, kb.file_size, kb.created_at, kb.updated_at,
|
||||
u.username as created_by_username
|
||||
FROM knowledge_base kb
|
||||
LEFT JOIN users u ON kb.created_by = u.id
|
||||
ORDER BY kb.created_at DESC
|
||||
`);
|
||||
res.json(rows);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error fetching knowledge base articles:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch articles' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/knowledge-base/:id
|
||||
* Get a single article's details by ID.
|
||||
*
|
||||
* @param {string} id - Article ID (route parameter)
|
||||
*
|
||||
* @response 200 - { id, title, slug, description, category, file_name, file_type, file_size, created_at, updated_at, created_by_username }
|
||||
* @response 404 - { error: 'Article not found' }
|
||||
* @response 500 - { error: string }
|
||||
*/
|
||||
router.get('/:id', requireAuth(db), (req, res) => {
|
||||
// GET /api/knowledge-base/:id
|
||||
router.get('/:id', requireAuth(), async (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
const sql = `
|
||||
SELECT
|
||||
kb.id, kb.title, kb.slug, kb.description, kb.category,
|
||||
kb.file_name, kb.file_type, kb.file_size, kb.created_at, kb.updated_at,
|
||||
u.username as created_by_username
|
||||
FROM knowledge_base kb
|
||||
LEFT JOIN users u ON kb.created_by = u.id
|
||||
WHERE kb.id = ?
|
||||
`;
|
||||
try {
|
||||
const { rows } = await pool.query(`
|
||||
SELECT
|
||||
kb.id, kb.title, kb.slug, kb.description, kb.category,
|
||||
kb.file_name, kb.file_type, kb.file_size, kb.created_at, kb.updated_at,
|
||||
u.username as created_by_username
|
||||
FROM knowledge_base kb
|
||||
LEFT JOIN users u ON kb.created_by = u.id
|
||||
WHERE kb.id = $1
|
||||
`, [id]);
|
||||
|
||||
db.get(sql, [id], (err, row) => {
|
||||
if (err) {
|
||||
console.error('Error fetching article:', err);
|
||||
return res.status(500).json({ error: 'Failed to fetch article' });
|
||||
}
|
||||
|
||||
if (!row) {
|
||||
if (!rows[0]) {
|
||||
return res.status(404).json({ error: 'Article not found' });
|
||||
}
|
||||
|
||||
res.json(row);
|
||||
});
|
||||
res.json(rows[0]);
|
||||
} catch (err) {
|
||||
console.error('Error fetching article:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch article' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/knowledge-base/:id/content
|
||||
* Get document content for inline display. Returns the raw file with appropriate
|
||||
* Content-Type headers. Markdown and text files are served as text/plain.
|
||||
*
|
||||
* @param {string} id - Article ID (route parameter)
|
||||
*
|
||||
* @response 200 - Raw file content with Content-Type and Content-Disposition headers
|
||||
* @response 404 - { error: string } - Article or file not found
|
||||
* @response 500 - { error: string }
|
||||
*/
|
||||
router.get('/:id/content', requireAuth(db), (req, res) => {
|
||||
// GET /api/knowledge-base/:id/content
|
||||
router.get('/:id/content', requireAuth(), async (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
const sql = 'SELECT file_path, file_name, file_type FROM knowledge_base WHERE id = ?';
|
||||
|
||||
db.get(sql, [id], (err, row) => {
|
||||
if (err) {
|
||||
console.error('Error fetching document:', err);
|
||||
return res.status(500).json({ error: 'Failed to fetch document' });
|
||||
}
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
'SELECT file_path, file_name, file_type FROM knowledge_base WHERE id = $1', [id]
|
||||
);
|
||||
const row = rows[0];
|
||||
|
||||
if (!row) {
|
||||
return res.status(404).json({ error: 'Document not found' });
|
||||
@@ -271,8 +207,7 @@ function createKnowledgeBaseRouter(db, upload) {
|
||||
return res.status(404).json({ error: 'File not found on disk' });
|
||||
}
|
||||
|
||||
// Log audit entry
|
||||
logAudit(db, {
|
||||
logAudit({
|
||||
userId: req.user.id,
|
||||
username: req.user.username,
|
||||
action: 'VIEW_KB_ARTICLE',
|
||||
@@ -282,10 +217,7 @@ function createKnowledgeBaseRouter(db, upload) {
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
// Determine content type for inline display
|
||||
let contentType = row.file_type || 'application/octet-stream';
|
||||
|
||||
// For markdown files, send as plain text so frontend can parse it
|
||||
if (row.file_name.endsWith('.md')) {
|
||||
contentType = 'text/plain; charset=utf-8';
|
||||
} else if (row.file_name.endsWith('.txt')) {
|
||||
@@ -294,36 +226,26 @@ function createKnowledgeBaseRouter(db, upload) {
|
||||
|
||||
const safeFileName = row.file_name.replace(/["\r\n\\]/g, '');
|
||||
res.setHeader('Content-Type', contentType);
|
||||
// Use inline instead of attachment to allow browser to display
|
||||
res.setHeader('Content-Disposition', `inline; filename="${safeFileName}"`);
|
||||
// Allow iframe embedding from frontend origin
|
||||
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);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error fetching document:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch document' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/knowledge-base/:id/download
|
||||
* Download a knowledge base document as an attachment.
|
||||
*
|
||||
* @param {string} id - Article ID (route parameter)
|
||||
*
|
||||
* @response 200 - File download with Content-Disposition: attachment header
|
||||
* @response 404 - { error: string } - Article or file not found
|
||||
* @response 500 - { error: string }
|
||||
*/
|
||||
router.get('/:id/download', requireAuth(db), (req, res) => {
|
||||
// GET /api/knowledge-base/:id/download
|
||||
router.get('/:id/download', requireAuth(), async (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
const sql = 'SELECT file_path, file_name, file_type FROM knowledge_base WHERE id = ?';
|
||||
|
||||
db.get(sql, [id], (err, row) => {
|
||||
if (err) {
|
||||
console.error('Error fetching document:', err);
|
||||
return res.status(500).json({ error: 'Failed to fetch document' });
|
||||
}
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
'SELECT file_path, file_name, file_type FROM knowledge_base WHERE id = $1', [id]
|
||||
);
|
||||
const row = rows[0];
|
||||
|
||||
if (!row) {
|
||||
return res.status(404).json({ error: 'Document not found' });
|
||||
@@ -333,8 +255,7 @@ function createKnowledgeBaseRouter(db, upload) {
|
||||
return res.status(404).json({ error: 'File not found on disk' });
|
||||
}
|
||||
|
||||
// Log audit entry
|
||||
logAudit(db, {
|
||||
logAudit({
|
||||
userId: req.user.id,
|
||||
username: req.user.username,
|
||||
action: 'DOWNLOAD_KB_ARTICLE',
|
||||
@@ -348,31 +269,21 @@ function createKnowledgeBaseRouter(db, upload) {
|
||||
res.setHeader('Content-Type', row.file_type || 'application/octet-stream');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${safeDownloadName}"`);
|
||||
res.sendFile(row.file_path);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error fetching document:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch document' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/knowledge-base/:id
|
||||
* Delete a knowledge base article and its associated file.
|
||||
* Standard_User can only delete articles they created. Admin can delete any article.
|
||||
*
|
||||
* @param {string} id - Article ID (route parameter)
|
||||
*
|
||||
* @response 200 - { success: true }
|
||||
* @response 403 - { error: string } - Ownership check failed for Standard_User
|
||||
* @response 404 - { error: 'Article not found' }
|
||||
* @response 500 - { error: string }
|
||||
*/
|
||||
router.delete('/:id', requireAuth(db), requireGroup('Admin', 'Standard_User'), (req, res) => {
|
||||
// DELETE /api/knowledge-base/:id
|
||||
router.delete('/:id', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
const sql = 'SELECT file_path, title, created_by FROM knowledge_base WHERE id = ?';
|
||||
|
||||
db.get(sql, [id], (err, row) => {
|
||||
if (err) {
|
||||
console.error('Error fetching article for deletion:', err);
|
||||
return res.status(500).json({ error: 'Failed to fetch article' });
|
||||
}
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
'SELECT file_path, title, created_by FROM knowledge_base WHERE id = $1', [id]
|
||||
);
|
||||
const row = rows[0];
|
||||
|
||||
if (!row) {
|
||||
return res.status(404).json({ error: 'Article not found' });
|
||||
@@ -383,32 +294,28 @@ function createKnowledgeBaseRouter(db, upload) {
|
||||
return res.status(403).json({ error: 'You can only delete resources you created' });
|
||||
}
|
||||
|
||||
// Delete database record
|
||||
db.run('DELETE FROM knowledge_base WHERE id = ?', [id], (err) => {
|
||||
if (err) {
|
||||
console.error('Error deleting article:', err);
|
||||
return res.status(500).json({ error: 'Failed to delete article' });
|
||||
}
|
||||
await pool.query('DELETE FROM knowledge_base WHERE id = $1', [id]);
|
||||
|
||||
// Delete file
|
||||
if (fs.existsSync(row.file_path)) {
|
||||
fs.unlinkSync(row.file_path);
|
||||
}
|
||||
// Delete file
|
||||
if (fs.existsSync(row.file_path)) {
|
||||
fs.unlinkSync(row.file_path);
|
||||
}
|
||||
|
||||
// Log audit entry
|
||||
logAudit(db, {
|
||||
userId: req.user.id,
|
||||
username: req.user.username,
|
||||
action: 'DELETE_KB_ARTICLE',
|
||||
entityType: 'knowledge_base',
|
||||
entityId: String(id),
|
||||
details: { title: row.title },
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
logAudit({
|
||||
userId: req.user.id,
|
||||
username: req.user.username,
|
||||
action: 'DELETE_KB_ARTICLE',
|
||||
entityType: 'knowledge_base',
|
||||
entityId: String(id),
|
||||
details: { title: row.title },
|
||||
ipAddress: req.ip
|
||||
});
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error('Error deleting article:', err);
|
||||
res.status(500).json({ error: 'Failed to delete article' });
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
|
||||
Reference in New Issue
Block a user