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'), (req, res, next) => { upload.single('file')(req, res, (err) => { if (err) { console.error('[KB Upload] Multer error:', err); return res.status(400).json({ error: err.message || 'File upload failed' }); } next(); }); }, async (req, res) => { console.log('[KB Upload] Request received:', { hasFile: !!req.file, body: req.body, contentType: req.headers['content-type'] }); 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); 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;