Added tag and category API endpoints
This commit is contained in:
parent
f0ebd192a1
commit
6870d304b4
325
server/server.js
325
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}`);
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user