const express = require('express'); const argon2 = require('argon2'); const { initDb, getAllArticles, getArticle, createArticle, updateArticle, deleteArticle, searchArticles, createUser, getUserByUsername, getUserByEmail, getUserById, getAllUsers, updateUserRole, deleteUser, getOwnedDrafts, getAllCategories, getAllTags, getArticleCategories, getArticleTags, getOrCreateCategory, getOrCreateTag, setArticleCategories, setArticleTags } = require('./db'); const { generateToken, authenticateToken, authorizeRoles } = require('./auth'); const app = express(); const cors = require('cors'); const msal = require('@azure/msal-node'); const entraConfig = require('./entraConfig'); const jwt = require('jsonwebtoken'); const multer = require('multer'); const path = require('path'); const fs = require('fs'); const PORT = 9000; app.use(cors()) app.use(express.json()); app.use(express.static(path.join(__dirname, 'frontend/dist'))); const upload = multer({ dest: './temp_uploads/', limits: { filesize: 100 * 1024 * 1024 } }); if (!fs.existsSync('./temp_uploads')) { fs.mkdirSync('./temp_uploads'); } if (!fs.existsSync('./media')) { fs.mkdirSync('./media'); } initDb().then(() => { app.get('/', (req, res) => { res.json({ message: 'Server is running' }); }); app.get('/api/articles', authenticateToken, (req, res) => { try { const articles = getAllArticles(); articles.forEach(article => { article.categories = getArticleCategories(article.id); article.tags = getArticleTags(article.id); }); res.json(articles); } catch (error) { console.error('Error fetching articles:', error); res.status(500).json({ error: 'Failed to fetch articles', 'details': String(error) }); } }); app.get('/api/articles/drafts', authenticateToken, authorizeRoles('Admin', 'Editor'), (req, res) => { try { const drafts = getOwnedDrafts(req.user.display_name); res.json(drafts); } catch (error) { console.error('Error fetching owned drafts', error); res.status(500).json({ error: 'Failed to fetch owned drafts', details: String(error) }); } }); app.get('/api/articles/:ka_number', authenticateToken, (req, res) => { try { const article = getArticle(req.params.ka_number); if (!article) { return res.status(404).json({ error: 'Article not found' }); } article.categories = getArticleCategories(article.id); article.tags = getArticleTags(article.id); res.json(article); } catch (error) { console.error('Error fetching article:', error); res.status(500).json({ error: 'Failed to fetch article', 'details': String(error) }); } }); app.post('/api/articles', authenticateToken, authorizeRoles('Admin', 'Editor'), (req, res) => { try { const { title, content, status } = req.body; if (!title && status === 'published') { return res.status(400).json({ error: 'Title is required' }); } const article = createArticle(title, content, req.user.display_name, status); res.status(201).json(article); } catch (error) { console.error('Error creating article:', error); res.status(500).json({ error: 'Failed to create article', 'details': String(error) }); } }); app.put('/api/articles/:ka_number', authenticateToken, authorizeRoles('Admin', 'Editor'), (req, res) => { try { const { title, content, status } = req.body; if (status && !['draft', 'published'].includes(status)) { return res.status(400).json({ error: 'Invalid publishing status' }); } if (status === 'published' && !title) { return res.status(400).json({ error: 'Title is required to publish article' }); } const article = updateArticle( req.params.ka_number, title, content, req.user.display_name, status || 'published' ); if (!article) { return res.status(404).json({ error: 'Article not found' }); } res.json(article); } catch (error) { console.error('Error updating article:', error); return res.status(500).json({ error: 'Error while updating article', 'details': String(error) }); } }); app.delete('/api/articles/:ka_number', authenticateToken, authorizeRoles('Admin', 'Editor'), (req, res) => { try { deleteArticle(req.params.ka_number); return res.status(200).json({ 'message': 'Successfully deleted article' }); } catch (error) { console.error('Error when deleting article:', error); return res.status(500).json({ error: 'Failed to delete article', 'details': String(error) }); } }); app.get('/api/search', authenticateToken, (req, res) => { try { const query = req.query.q; if (!query) { return res.status(400).json({ error: 'Search query is required' }); } const results = searchArticles(query); results.forEach(article => { article.categories = getArticleCategories(article.id); article.tags = getArticleTags(article.id); }); res.json(results); } catch (error) { console.error('Error when searching articles:', error); return res.status(500).json({ error: 'Failed to search articles', 'details': String(error) }); } }); app.get('/api/search/advanced', authenticateToken, (req, res) => { try { const { query, categories, tags } = req.query; let articles = getAllArticles(); articles.forEach(article => { article.categories = getArticleCategories(article.id); article.tags = getArticleTags(article.id); }); if (query) { const searchTerm = query.toLowerCase(); articles = articles.filter(article => article.title.toLowerCase().includes(searchTerm) || article.content.toLowerCase().includes(searchTerm) || article.ka_number.toLowerCase().includes(searchTerm) ); } if (categories) { const categoryList = categories.split(',').map(c => c.trim().toLowerCase()); articles = articles.filter(article => article.categories.some(cat => categoryList.includes(cat.toLowerCase())) ); } if (tags) { const tagList = tags.split(',').map(t => t.trim().toLowerCase()); articles = articles.filter(article => article.tags.some(tag => tagList.includes(tag.toLowerCase())) ); } res.json(articles); } catch (error) { console.error('Error searching articles:', error); res.status(500).json({ error: 'Failed to search articles', details: String(error) }); } }); app.post('/api/auth/register', async (req, res) => { try { const { username, email, password, display_name } = req.body; if (!username || !email || !password || !display_name) { return res.status(400).json({ error: 'Username, email, password, and display name are required' }); } const cur_user_username = getUserByUsername(username); if (cur_user_username) { return res.status(400).json({ error: 'Username already exists' }); } const cur_user_email = getUserByEmail(email); if (cur_user_email) { return res.status(400).json({ error: 'email address already in use' }); } const pass_hash = await argon2.hash(password); const newUser = createUser(username, email, pass_hash, display_name); const token = generateToken(newUser); return res.status(201).json({ user: { id: newUser.id, username: newUser.username, email: newUser.email, display_name: newUser.display_name, auth_provider: newUser.auth_provider, role: newUser.role, created_at: newUser.created_at }, token }); } catch (error) { console.error('Error when registering new user: ', error); return res.status(500).json({ error: 'Failed to create user', 'details': String(error) }); } }); app.post('/api/auth/login', async (req, res) => { try { const { username, email, password } = req.body; if (!password || (!username && !email)) { return res.status(400).json({ error: 'Username/email & password are required' }); } let user; if (!username) { user = getUserByEmail(email); } else { user = getUserByUsername(username); } if (!user || !await argon2.verify(user.pass_hash, password)) { return res.status(401).json({ error: 'Invalid credentials' }); } const token = generateToken(user); return res.status(200).json({ user: { id: user.id, username: user.username, email: user.email, display_name: user.display_name, auth_provider: user.auth_provider, role: user.role, created_at: user.created_at }, token }); } catch (error) { console.log('Failed to log user in:', error); return res.status(500).json({ error: 'Failed to login', 'details': String(error) }); } }); app.post('/api/auth/microsoft', async (req, res) => { try { const { accessToken, idToken } = req.body; if (!accessToken) { return res.status(400).json({ error: 'Access token required' }); } // Get user info directly from Microsoft Graph using the access token const graphResponse = await fetch('https://graph.microsoft.com/v1.0/me', { headers: { 'Authorization': `Bearer ${accessToken}` } }); if (!graphResponse.ok) { return res.status(401).json({ error: 'Invalid Microsoft token' }); } const decoded = jwt.decode(idToken); const roles = decoded.roles || []; let userRole = 'User'; if (roles.includes('Admin')) userRole = 'Admin'; else if (roles.includes('Editor')) userRole = 'Editor'; const msUser = await graphResponse.json(); // Check if user exists in our database let user = getUserByEmail(msUser.mail || msUser.userPrincipalName); if (!user) { // JIT Provisioning - Create new user user = createUser( null, msUser.mail || msUser.userPrincipalName, null, msUser.displayName, 'entra', msUser.id, userRole ); } else if (user.auth_provider === 'local') { return res.status(400).json({ error: 'This email is registered with a local account. Please login with username/password.' }); } const token = generateToken(user); return res.status(200).json({ user: { id: user.id, username: user.username, email: user.email, display_name: user.display_name, auth_provider: user.auth_provider, role: user.role, created_at: user.created_at }, token }); } catch (err) { console.error('Microsoft auth error:', err); return res.status(500).json({ error: 'Microsoft authentication failed', details: String(err) }); } }); app.get('/api/admin/users', authenticateToken, authorizeRoles('Admin'), (req, res) => { try { const users = getAllUsers(); res.status(200).json(users); } catch (error) { console.error('Error fetching users:', error); res.status(500).json({ error: 'Failed to fetch users', details: String(error) }); } }); app.put('/api/admin/users/:id/role', authenticateToken, authorizeRoles('Admin'), (req, res) => { try { const userId = parseInt(req.params.id); const { role } = req.body; if (!['Admin', 'Editor', 'User'].includes(role)) { return res.status(400).json({ error: 'Invalid role. Must be Admin, Editor, or User' }); } if (userId === req.user.id) { return res.status(400).json({ error: 'You cannot change your own role' }); } const user = getUserById(userId); if (!user) { return res.status(404).json({ error: 'User not found' }); } const updatedUser = updateUserRole(userId, role); res.status(200).json(updatedUser); } catch (error) { console.error('Error updating user:', error); return res.status(500).json({ error: 'Failed to update user role', details: String(error) }); } }); app.delete('/api/admin/users/:id', authenticateToken, authorizeRoles('Admin'), (req, res) => { try { const userId = parseInt(req.params.id); if (userId === req.user.id) { return res.status(400).json({ error: 'You cannot delete your own user profile' }); } const user = getUserById(userId); if (!user) return res.status(404).json({ error: 'User not found' }); deleteUser(userId); return res.status(200).json({ message: 'User deleted successfully' }); } catch (error) { console.error('Error deleting user:', error); return res.status(500).json({ error: 'Failed to delete user', details: String(error) }); } }); app.post('/api/articles/:ka_number/media', authenticateToken, authorizeRoles('Admin', 'Editor'), upload.single('file'), async (req, res) => { try { const kaNumber = req.params.ka_number; const file = req.file; if (!file) { return res.status(400).json({ error: 'No file uploaded' }); } const article = getArticle(kaNumber); if (!article) { fs.unlinkSync(file.path); return res.status(404).json({ error: 'Article not found' }); } const mediaDir = path.join('./media', kaNumber); if (!fs.existsSync(mediaDir)) { fs.mkdirSync(mediaDir, { recursive: true }); } const fileExt = path.extname(file.originalname).toLowerCase(); const isImage = ['.jpg', '.jpeg', '.png', '.gif', '.webp'].includes(fileExt); const isVideo = ['.mp4', '.mov', '.avi', '.mkv', '.webm'].includes(fileExt); if (!isImage && !isVideo) { fs.unlinkSync(file.path); return res.status(400).json({ error: 'Invalid file type. Only images and videos are allowed.' }) } const timestamp = Date.now(); const outputFilename = isVideo ? `video_${timestamp}${fileExt}` : `image_${timestamp}${fileExt}` const outputPath = path.join(mediaDir, outputFilename); fs.renameSync(file.path, outputPath); res.json({ success: true, filename: outputFilename, url: `/media/${kaNumber}/${outputFilename}`, type: isVideo ? 'video' : 'image' }); } catch (error) { console.error('Error uploading media:', error); res.status(500).json({ error: 'Failed to upload media', details: String(error) }); } } ); app.get('/api/articles/:ka_number/media', authenticateToken, authorizeRoles('Admin', 'Editor'), (req, res) => { try { const kaNumber = req.params.ka_number; const mediaDir = path.join('./media', kaNumber); if (!fs.existsSync(mediaDir)) { return res.json([]); } const files = fs.readdirSync(mediaDir); const mediaList = files.map(filename => { const fileExt = path.extname(filename).toLocaleLowerCase(); const isVideo = ['.mp4', '.mov', '.avi', '.mkv', '.webm'].includes(fileExt); return { filename: filename, url: `/media/${kaNumber}/${filename}`, type: isVideo ? 'video' : 'image' }; }); res.json(mediaList); } catch (error) { console.error('Error fetching media:', error); res.status(500).json({ error: 'Failed to fetch media', details: String(error) }); } }); app.delete('/api/articles/:ka_number/media/:filename', authenticateToken, authorizeRoles('Admin', 'Editor'), (req, res) => { try { const { ka_number, filename } = req.params; const filePath = path.join('./media', ka_number, filename); if (!fs.existsSync(filePath)) { return res.status(404).json({ error: 'File not found' }); } fs.unlinkSync(filePath); res.json({ message: 'File deleted successfully' }); } catch (error) { console.error('Error when deleting file:', error); res.status(500).json({ error: 'Failed to delete file', details: String(error) }); } } ); app.use('/media', express.static('./media')); app.get('/api/categories', authenticateToken, (req, res) => { try { const categories = getAllCategories(); res.json(categories); } catch (error) { console.error('Error fetching categories:', error); res.status(500).json({ error: 'Failed to fetch categories', details: String(error) }); } }); app.post('/api/categories', authenticateToken, authorizeRoles('Admin', 'Editor'), (req, res) => { try { const { name } = req.body; if (!name || !name.trim()) { return res.status(400).json({ error: 'Category name is required' }); } const category = getOrCreateCategory(name.trim()); res.status(201).json(category); } catch (error) { console.error('Error creating category:', error); res.status(500).json({ error: 'Failed to create category', details: String(error) }); } }); app.get('/api/tags', authenticateToken, (req, res) => { try { const tags = getAllTags(); res.json(tags); } catch (error) { console.error('Error fetching tags:', error); res.status(500).json({ error: 'Failed to fetch tags', details: String(error) }); } }); app.post('/api/tags', authenticateToken, authorizeRoles('Admin', 'Editor'), (req, res) => { try { const { name } = req.body; if (!name || !name.trim()) { return res.status(400).json({ error: 'Tag name is required' }); } const tag = getOrCreateTag(name.trim()); res.status(201).json(tag); } catch (error) { console.error('Error creating tag:', error); res.status(500).json({ error: 'Failed to create tag', details: String(error) }); } }); app.put('/api/articles/:ka_number/categories', authenticateToken, authorizeRoles('Admin', 'Editor'), (req, res) => { try { const kaNumber = req.params.ka_number; const { categories } = req.body; if (!Array.isArray(categories)) { return res.status(400).json({ error: 'Categories must be an array' }); } const article = getArticle(kaNumber); if (!article) { return res.status(404).json({ error: 'Article not found' }); } setArticleCategories(article.id, categories); res.json({ message: 'Categories updated successfully', categories }); } catch (error) { console.error('Error updating article categories:', error); res.status(500).json({ error: 'Failed to update categories', details: String(error) }); } }); app.put('/api/articles/:ka_number/tags', authenticateToken, authorizeRoles('Admin', 'Editor'), (req, res) => { try { const kaNumber = req.params.ka_number; const { tags } = req.body; if (!Array.isArray(tags)) { return res.status(400).json({ error: 'Tags must be an array' }); } const article = getArticle(kaNumber); if (!article) { return res.status(404).json({ error: 'Article not found' }); } setArticleTags(article.id, tags); res.json({ message: 'Tags updated successfully', tags }); } catch (error) { console.error('Error updating article tags:', error); res.status(500).json({ error: 'Failed to update tags', details: String(error) }); } }); app.get('/api/articles/:ka_number/categories', authenticateToken, (req, res) => { try { const article = getArticle(req.params.ka_number); if (!article) { return res.status(404).json({ error: 'Article not found' }); } const categories = getArticleCategories(article.id); res.json(categories); } catch (error) { console.error('Error fetching article categories:', error); res.status(500).json({ error: 'Failed to fetch categories', details: String(error) }); } }); app.get('/api/articles/:ka_number/tags', authenticateToken, (req, res) => { try { const article = getArticle(req.params.ka_number); if (!article) { return res.status(404).json({ error: 'Article not found' }); } const tags = getArticleTags(article.id); res.json(tags); } catch (error) { console.error('Error fetching article tags:', error); res.status(500).json({ error: 'Failed to fetch tags', details: String(error) }); } }); app.use('/media', express.static(path.join(__dirname, 'media'))); app.listen(PORT, () => { console.log(`Server listening on ${PORT}`); }); }).catch(err => { console.error('Failed to initialize database:', err); });