const express = require('express'); const argon2 = require('argon2'); const { initDb, getAllArticles, getArticle, createArticle, updateArticle, deleteArticle, searchArticles, createUser, getUserByUsername, getUserByEmail, getUserById, getAllUsers, updateUserRole, deleteUser, getOwnedDrafts } = 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()); const msalClient = new msal.ConfidentialClientApplication(entraConfig); 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(); 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' }); } 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); 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.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.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); }); }).catch(err => { console.error('Failed to initialize database:', err); });