481 lines
17 KiB
JavaScript
481 lines
17 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
|
|
} = 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);
|
|
}); |