338 lines
10 KiB
JavaScript
338 lines
10 KiB
JavaScript
const express = require('express');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const { requireAuth, requireRole } = require('../middleware/auth');
|
|
const logAudit = require('../helpers/auditLog');
|
|
|
|
function createKnowledgeBaseRouter(db, upload) {
|
|
const router = express.Router();
|
|
|
|
// Helper to sanitize filename
|
|
function sanitizePathSegment(segment) {
|
|
if (!segment || typeof segment !== 'string') return '';
|
|
return segment
|
|
.replace(/\0/g, '')
|
|
.replace(/\.\./g, '')
|
|
.replace(/[\/\\]/g, '')
|
|
.trim();
|
|
}
|
|
|
|
// Helper to generate slug from title
|
|
function generateSlug(title) {
|
|
return title
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/^-+|-+$/g, '')
|
|
.substring(0, 200);
|
|
}
|
|
|
|
// Helper to validate file type
|
|
const ALLOWED_EXTENSIONS = new Set([
|
|
'.pdf', '.md', '.txt', '.doc', '.docx',
|
|
'.xls', '.xlsx', '.ppt', '.pptx',
|
|
'.html', '.htm', '.json', '.yaml', '.yml',
|
|
'.png', '.jpg', '.jpeg', '.gif'
|
|
]);
|
|
|
|
function isValidFileType(filename) {
|
|
const ext = path.extname(filename).toLowerCase();
|
|
return ALLOWED_EXTENSIONS.has(ext);
|
|
}
|
|
|
|
// POST /api/knowledge-base/upload - Upload new document
|
|
router.post('/upload', requireAuth(db), requireRole(db, 'editor', 'admin'), upload.single('file'), async (req, res) => {
|
|
const uploadedFile = req.file;
|
|
const { title, description, category } = req.body;
|
|
|
|
// Validate required fields
|
|
if (!title || !title.trim()) {
|
|
if (uploadedFile) fs.unlinkSync(uploadedFile.path);
|
|
return res.status(400).json({ error: 'Title is required' });
|
|
}
|
|
|
|
if (!uploadedFile) {
|
|
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' });
|
|
}
|
|
|
|
const timestamp = Date.now();
|
|
const sanitizedName = sanitizePathSegment(uploadedFile.originalname);
|
|
const slug = generateSlug(title);
|
|
const kbDir = path.join(__dirname, '..', 'uploads', 'knowledge_base');
|
|
|
|
// Create directory if it doesn't exist
|
|
if (!fs.existsSync(kbDir)) {
|
|
fs.mkdirSync(kbDir, { recursive: true });
|
|
}
|
|
|
|
const filename = `${timestamp}_${sanitizedName}`;
|
|
const filePath = path.join(kbDir, filename);
|
|
|
|
try {
|
|
// Move uploaded file to permanent location
|
|
fs.renameSync(uploadedFile.path, filePath);
|
|
|
|
// Check if slug already exists
|
|
db.get('SELECT id FROM knowledge_base WHERE slug = ?', [slug], (err, row) => {
|
|
if (err) {
|
|
fs.unlinkSync(filePath);
|
|
console.error('Error checking slug:', err);
|
|
return res.status(500).json({ error: 'Database error' });
|
|
}
|
|
|
|
// If slug exists, append timestamp to make it unique
|
|
const finalSlug = row ? `${slug}-${timestamp}` : slug;
|
|
|
|
// 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(filePath);
|
|
console.error('Error inserting knowledge base entry:', err);
|
|
return res.status(500).json({ error: 'Failed to save document metadata' });
|
|
}
|
|
|
|
// Log audit entry
|
|
logAudit(
|
|
db,
|
|
req.user.id,
|
|
req.user.username,
|
|
'CREATE_KB_ARTICLE',
|
|
'knowledge_base',
|
|
this.lastID,
|
|
JSON.stringify({ title: title.trim(), filename: sanitizedName }),
|
|
req.ip
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
id: this.lastID,
|
|
title: title.trim(),
|
|
slug: finalSlug,
|
|
category: category || 'General'
|
|
});
|
|
}
|
|
);
|
|
});
|
|
} catch (error) {
|
|
// Clean up file on error
|
|
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
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 articles
|
|
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' });
|
|
}
|
|
|
|
res.json(rows);
|
|
});
|
|
});
|
|
|
|
// GET /api/knowledge-base/:id - Get single article details
|
|
router.get('/:id', requireAuth(db), (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 = ?
|
|
`;
|
|
|
|
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) {
|
|
return res.status(404).json({ error: 'Article not found' });
|
|
}
|
|
|
|
res.json(row);
|
|
});
|
|
});
|
|
|
|
// GET /api/knowledge-base/:id/content - Get document content for display
|
|
router.get('/:id/content', requireAuth(db), (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' });
|
|
}
|
|
|
|
if (!row) {
|
|
return res.status(404).json({ error: 'Document not found' });
|
|
}
|
|
|
|
if (!fs.existsSync(row.file_path)) {
|
|
return res.status(404).json({ error: 'File not found on disk' });
|
|
}
|
|
|
|
// Log audit entry
|
|
logAudit(
|
|
db,
|
|
req.user.id,
|
|
req.user.username,
|
|
'VIEW_KB_ARTICLE',
|
|
'knowledge_base',
|
|
id,
|
|
JSON.stringify({ filename: row.file_name }),
|
|
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')) {
|
|
contentType = 'text/plain; charset=utf-8';
|
|
}
|
|
|
|
res.setHeader('Content-Type', contentType);
|
|
// Use inline instead of attachment to allow browser to display
|
|
res.setHeader('Content-Disposition', `inline; filename="${row.file_name}"`);
|
|
// Allow iframe embedding from frontend origin
|
|
res.removeHeader('X-Frame-Options');
|
|
res.setHeader('Content-Security-Policy', "frame-ancestors 'self' http://71.85.90.9:3000 http://localhost:3000");
|
|
res.sendFile(row.file_path);
|
|
});
|
|
});
|
|
|
|
// GET /api/knowledge-base/:id/download - Download document
|
|
router.get('/:id/download', requireAuth(db), (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' });
|
|
}
|
|
|
|
if (!row) {
|
|
return res.status(404).json({ error: 'Document not found' });
|
|
}
|
|
|
|
if (!fs.existsSync(row.file_path)) {
|
|
return res.status(404).json({ error: 'File not found on disk' });
|
|
}
|
|
|
|
// Log audit entry
|
|
logAudit(
|
|
db,
|
|
req.user.id,
|
|
req.user.username,
|
|
'DOWNLOAD_KB_ARTICLE',
|
|
'knowledge_base',
|
|
id,
|
|
JSON.stringify({ filename: row.file_name }),
|
|
req.ip
|
|
);
|
|
|
|
res.setHeader('Content-Type', row.file_type || 'application/octet-stream');
|
|
res.setHeader('Content-Disposition', `attachment; filename="${row.file_name}"`);
|
|
res.sendFile(row.file_path);
|
|
});
|
|
});
|
|
|
|
// DELETE /api/knowledge-base/:id - Delete article
|
|
router.delete('/:id', requireAuth(db), requireRole(db, 'editor', 'admin'), (req, res) => {
|
|
const { id } = req.params;
|
|
|
|
const sql = 'SELECT file_path, title 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' });
|
|
}
|
|
|
|
if (!row) {
|
|
return res.status(404).json({ error: 'Article not found' });
|
|
}
|
|
|
|
// 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' });
|
|
}
|
|
|
|
// Delete file
|
|
if (fs.existsSync(row.file_path)) {
|
|
fs.unlinkSync(row.file_path);
|
|
}
|
|
|
|
// Log audit entry
|
|
logAudit(
|
|
db,
|
|
req.user.id,
|
|
req.user.username,
|
|
'DELETE_KB_ARTICLE',
|
|
'knowledge_base',
|
|
id,
|
|
JSON.stringify({ title: row.title }),
|
|
req.ip
|
|
);
|
|
|
|
res.json({ success: true });
|
|
});
|
|
});
|
|
});
|
|
|
|
return router;
|
|
}
|
|
|
|
module.exports = createKnowledgeBaseRouter;
|