2025-12-09 09:39:48 -06:00

677 lines
24 KiB
JavaScript

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);
});