const express = require('express'); const argon2 = require('argon2'); const { initDb, getAllArticles, getArticle, createArticle, updateArticle, deleteArticle, searchArticles, createUser, getUserByUsername, getUserByEmail, getUserById, getAllUsers, updateUserRole, deleteUser } = 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 PORT = 9000; app.use(cors()) app.use(express.json()); const msalClient = new msal.ConfidentialClientApplication(entraConfig); 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/: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 } = req.body; if (!title) { return res.status(400).json({error: 'Title is required' }); } const article = createArticle(title, content, req.user.display_name); 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 } = req.body; if (!title) { return res.status(400).json({error: 'Title is required' }); } const article = updateArticle(req.params.ka_number, title, content, req.user.display_name); 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.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); }); }).catch(err => { console.error('Failed to initialize database:', err); });