diff --git a/server/server.js b/server/server.js index 6ea1c37..f64c82d 100644 --- a/server/server.js +++ b/server/server.js @@ -1,12 +1,12 @@ const express = require('express'); const argon2 = require('argon2'); -const { - initDb, - getAllArticles, - getArticle, - createArticle, - updateArticle, - deleteArticle, +const { + initDb, + getAllArticles, + getArticle, + createArticle, + updateArticle, + deleteArticle, searchArticles, createUser, getUserByUsername, @@ -15,7 +15,15 @@ const { getAllUsers, updateUserRole, deleteUser, - getOwnedDrafts + getOwnedDrafts, + getAllCategories, + getAllTags, + getArticleCategories, + getArticleTags, + getOrCreateCategory, + getOrCreateTag, + setArticleCategories, + setArticleTags } = require('./db'); const { generateToken, authenticateToken, authorizeRoles } = require('./auth'); const app = express(); @@ -49,16 +57,22 @@ if (!fs.existsSync('./media')) { initDb().then(() => { app.get('/', (req, res) => { - res.json({message : 'Server is running'}); + 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)}); + res.status(500).json({ error: 'Failed to fetch articles', 'details': String(error) }); } }); @@ -66,9 +80,9 @@ initDb().then(() => { try { const drafts = getOwnedDrafts(req.user.display_name); res.json(drafts); - } catch(error) { + } catch (error) { console.error('Error fetching owned drafts', error); - res.status(500).json({error: 'Failed to fetch owned drafts', details: String(error)}); + res.status(500).json({ error: 'Failed to fetch owned drafts', details: String(error) }); } }); @@ -80,10 +94,13 @@ initDb().then(() => { 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)}); + res.status(500).json({ error: 'Failed to fetch article', 'details': String(error) }); } }); @@ -92,7 +109,7 @@ initDb().then(() => { const { title, content, status } = req.body; if (!title && status === 'published') { - return res.status(400).json({error: 'Title is required' }); + return res.status(400).json({ error: 'Title is required' }); } const article = createArticle(title, content, req.user.display_name, status); @@ -100,7 +117,7 @@ initDb().then(() => { } catch (error) { console.error('Error creating article:', error); - res.status(500).json({error: 'Failed to create article', 'details': String(error)}); + res.status(500).json({ error: 'Failed to create article', 'details': String(error) }); } }); @@ -109,40 +126,40 @@ initDb().then(() => { const { title, content, status } = req.body; if (status && !['draft', 'published'].includes(status)) { - return res.status(400).json({error: 'Invalid publishing 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'}); + + 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.params.ka_number, + title, + content, req.user.display_name, status || 'published' ); if (!article) { - return res.status(404).json({error: 'Article not found'}); + 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)}); + 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'}); + 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)}); + return res.status(500).json({ error: 'Failed to delete article', 'details': String(error) }); } }); @@ -151,33 +168,74 @@ initDb().then(() => { const query = req.query.q; if (!query) { - return res.status(400).json({error: 'Search query is required'}); + 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)}); + 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; + 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'}); + 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'}); + 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'}); + if (cur_user_email) { + return res.status(400).json({ error: 'email address already in use' }); } const pass_hash = await argon2.hash(password); @@ -199,16 +257,16 @@ initDb().then(() => { } catch (error) { console.error('Error when registering new user: ', error); - return res.status(500).json({error: 'Failed to create user', 'details': String(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; + const { username, email, password } = req.body; if (!password || (!username && !email)) { - return res.status(400).json({error: 'Username/email & password are required'}); + return res.status(400).json({ error: 'Username/email & password are required' }); } let user; @@ -219,7 +277,7 @@ initDb().then(() => { } if (!user || !await argon2.verify(user.pass_hash, password)) { - return res.status(401).json({error: 'Invalid credentials'}); + return res.status(401).json({ error: 'Invalid credentials' }); } const token = generateToken(user); @@ -238,7 +296,7 @@ initDb().then(() => { }); } catch (error) { console.log('Failed to log user in:', error); - return res.status(500).json({error: 'Failed to login', 'details': String(error)}); + return res.status(500).json({ error: 'Failed to login', 'details': String(error) }); } }); @@ -247,7 +305,7 @@ initDb().then(() => { const { accessToken, idToken } = req.body; if (!accessToken) { - return res.status(400).json({error: 'Access token required'}); + return res.status(400).json({ error: 'Access token required' }); } // Get user info directly from Microsoft Graph using the access token @@ -258,7 +316,7 @@ initDb().then(() => { }); if (!graphResponse.ok) { - return res.status(401).json({error: 'Invalid Microsoft token'}); + return res.status(401).json({ error: 'Invalid Microsoft token' }); } const decoded = jwt.decode(idToken); @@ -304,7 +362,7 @@ initDb().then(() => { }, token }); - } catch(err) { + } catch (err) { console.error('Microsoft auth error:', err); return res.status(500).json({ error: 'Microsoft authentication failed', @@ -317,9 +375,9 @@ initDb().then(() => { try { const users = getAllUsers(); res.status(200).json(users); - } catch(error) { + } catch (error) { console.error('Error fetching users:', error); - res.status(500).json({error: 'Failed to fetch users', details: String(error)}); + res.status(500).json({ error: 'Failed to fetch users', details: String(error) }); } }); @@ -329,23 +387,23 @@ initDb().then(() => { const { role } = req.body; if (!['Admin', 'Editor', 'User'].includes(role)) { - return res.status(400).json({error: 'Invalid role. Must be Admin, Editor, or User'}); + 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'}); + 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'}); + return res.status(404).json({ error: 'User not found' }); } const updatedUser = updateUserRole(userId, role); res.status(200).json(updatedUser); - } catch(error) { + } catch (error) { console.error('Error updating user:', error); - return res.status(500).json({error: 'Failed to update user role', details: String(error)}); + return res.status(500).json({ error: 'Failed to update user role', details: String(error) }); } }); @@ -354,17 +412,17 @@ initDb().then(() => { const userId = parseInt(req.params.id); if (userId === req.user.id) { - return res.status(400).json({error: 'You cannot delete your own user profile'}); + 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'}); + if (!user) return res.status(404).json({ error: 'User not found' }); deleteUser(userId); - return res.status(200).json({message: 'User deleted successfully'}); - } catch(error) { + 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)}); + return res.status(500).json({ error: 'Failed to delete user', details: String(error) }); } }); @@ -378,18 +436,18 @@ initDb().then(() => { const file = req.file; if (!file) { - return res.status(400).json({error: 'No file uploaded'}); + 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'}); + return res.status(404).json({ error: 'Article not found' }); } const mediaDir = path.join('./media', kaNumber); if (!fs.existsSync(mediaDir)) { - fs.mkdirSync(mediaDir, {recursive: true}); + fs.mkdirSync(mediaDir, { recursive: true }); } const fileExt = path.extname(file.originalname).toLowerCase(); @@ -398,7 +456,7 @@ initDb().then(() => { if (!isImage && !isVideo) { fs.unlinkSync(file.path); - return res.status(400).json({error: 'Invalid file type. Only images and videos are allowed.'}) + return res.status(400).json({ error: 'Invalid file type. Only images and videos are allowed.' }) } const timestamp = Date.now(); @@ -406,7 +464,7 @@ initDb().then(() => { ? `video_${timestamp}${fileExt}` : `image_${timestamp}${fileExt}` const outputPath = path.join(mediaDir, outputFilename); - + fs.renameSync(file.path, outputPath); res.json({ @@ -415,10 +473,10 @@ initDb().then(() => { url: `/media/${kaNumber}/${outputFilename}`, type: isVideo ? 'video' : 'image' }); - } catch(error) { + } catch (error) { console.error('Error uploading media:', error); - res.status(500).json({error: 'Failed to upload media', details: String(error)}); - } + res.status(500).json({ error: 'Failed to upload media', details: String(error) }); + } } ); @@ -446,7 +504,7 @@ initDb().then(() => { res.json(mediaList); } catch (error) { console.error('Error fetching media:', error); - res.status(500).json({error: 'Failed to fetch media', details: String(error)}); + res.status(500).json({ error: 'Failed to fetch media', details: String(error) }); } }); @@ -455,24 +513,155 @@ initDb().then(() => { authorizeRoles('Admin', 'Editor'), (req, res) => { try { - const {ka_number, filename } = req.params; + 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'}); + return res.status(404).json({ error: 'File not found' }); } fs.unlinkSync(filePath); - res.json({message: 'File deleted successfully'}); + 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)}); - } + 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.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); });