This commit is contained in:
Alvin
2025-10-21 20:54:03 +02:00
parent 97c80d7800
commit a5c73ad907
35 changed files with 4899 additions and 0 deletions

86
routes/auth.js Normal file
View File

@@ -0,0 +1,86 @@
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const router = express.Router();
// Register new student
router.post('/register', async (req, res) => {
try {
const { username, password, email } = req.body;
// Check if email already exists
const existingEmail = await User.findOne({ email });
if (existingEmail) {
return res.status(400).json({ message: 'Email already registered' });
}
// Check if username already exists
const existingUsername = await User.findOne({ username });
if (existingUsername) {
return res.status(400).json({ message: 'Username already taken' });
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
// Create new student user
const user = new User({
username,
password: hashedPassword,
email,
role: 'student' // Force role to be student
});
await user.save();
// Create and send token
const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '24h' });
res.status(201).json({
message: 'Student account created successfully',
token,
role: user.role,
username: user.username
});
} catch (error) {
if (error.name === 'ValidationError') {
return res.status(400).json({
message: 'Invalid email format. Email must be in the format: 123456@vistacollege.nl'
});
}
res.status(500).json({ message: 'Server error' });
}
});
// Login
router.post('/login', async (req, res) => {
try {
const { username, password } = req.body;
const user = await User.findOne({ username: username.toLowerCase() });
if (!user) {
return res.status(401).json({ message: 'Invalid credentials' });
}
const passwordMatch = await bcrypt.compare(password, user.password);
if (!passwordMatch) {
return res.status(401).json({ message: 'Invalid credentials' });
}
const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '24h' });
res.json({
token,
role: user.role,
username: user.username
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ message: 'Server error' });
}
});
module.exports = router;

117
routes/items.js Normal file
View File

@@ -0,0 +1,117 @@
const express = require('express');
const multer = require('multer');
const path = require('path');
const { auth, adminOnly } = require('../middleware/auth');
const Item = require('../models/Item');
const router = express.Router();
// Configure multer for image upload
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'public/images/items/');
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, 'item-' + uniqueSuffix + path.extname(file.originalname));
}
});
const upload = multer({
storage: storage,
fileFilter: (req, file, cb) => {
if (file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('Not an image! Please upload an image.'), false);
}
}
});
// Get all items
router.get('/', auth, async (req, res) => {
try {
const items = await Item.find();
res.json(items);
} catch (error) {
res.status(500).json({ message: 'Server error' });
}
});
// Get single item by ID
router.get('/:id', auth, async (req, res) => {
try {
const item = await Item.findById(req.params.id);
if (!item) {
return res.status(404).json({ message: 'Item not found' });
}
res.json(item);
} catch (error) {
res.status(500).json({ message: 'Server error' });
}
});
// Add new item (admin only)
router.post('/', auth, adminOnly, upload.single('image'), async (req, res) => {
try {
const itemData = {
name: req.body.name,
description: req.body.description,
location: req.body.location,
quantity: parseInt(req.body.quantity)
};
// If an image was uploaded, set the imageUrl
if (req.file) {
itemData.imageUrl = `/images/items/${req.file.filename}`;
}
const item = new Item(itemData);
await item.save();
res.status(201).json(item);
} catch (error) {
res.status(400).json({ message: error.message });
}
});
// Update item (admin only)
router.put('/:id', auth, adminOnly, upload.single('image'), async (req, res) => {
try {
const updateData = {
name: req.body.name,
description: req.body.description,
location: req.body.location,
quantity: parseInt(req.body.quantity)
};
// If an image was uploaded, set the new imageUrl
if (req.file) {
updateData.imageUrl = `/images/items/${req.file.filename}`;
} else if (req.body.imageUrl !== undefined) {
// If imageUrl is explicitly provided (including empty string for removal)
updateData.imageUrl = req.body.imageUrl || '/images/default-item.png';
}
const item = await Item.findByIdAndUpdate(req.params.id, updateData, { new: true });
if (!item) {
return res.status(404).json({ message: 'Item not found' });
}
res.json(item);
} catch (error) {
res.status(400).json({ message: error.message });
}
});
// Delete item (admin only)
router.delete('/:id', auth, adminOnly, async (req, res) => {
try {
const item = await Item.findByIdAndDelete(req.params.id);
if (!item) {
return res.status(404).json({ message: 'Item not found' });
}
res.json({ message: 'Item deleted successfully' });
} catch (error) {
res.status(500).json({ message: error.message });
}
});
module.exports = router;

208
routes/reservations.js Normal file
View File

@@ -0,0 +1,208 @@
const express = require('express');
const { auth, adminOnly } = require('../middleware/auth');
const Reservation = require('../models/Reservation');
const Item = require('../models/Item');
const router = express.Router();
// Get all reservations (admin only)
router.get('/', auth, adminOnly, async (req, res) => {
try {
const reservations = await Reservation.find({ status: { $ne: 'ARCHIVED' } })
.populate('userId', 'username')
.populate('itemId')
.exec();
if (!reservations) {
return res.json([]); // Return empty array if no reservations
}
const formattedReservations = reservations
.filter(reservation => reservation.userId && reservation.itemId) // Filter out any invalid references
.map(reservation => ({
_id: reservation._id,
studentName: reservation.userId.username,
itemName: reservation.itemId.name,
location: reservation.itemId.location,
status: reservation.status,
quantity: reservation.quantity || 1,
reservedDate: reservation.reservedDate
}));
res.json(formattedReservations);
} catch (error) {
console.error('Error fetching reservations:', error);
res.status(500).json({
message: 'Failed to load reservations',
error: error.message
});
}
});
// Get user's reservations
router.get('/my', auth, async (req, res) => {
try {
const reservations = await Reservation.find({
userId: req.user._id,
status: { $ne: 'ARCHIVED' }
}).populate('itemId');
// Filter out reservations with deleted items and format the response
const formattedReservations = reservations
.filter(reservation => reservation.itemId) // Only include reservations with valid items
.map(reservation => ({
_id: reservation._id,
itemName: reservation.itemId.name,
location: reservation.itemId.location,
status: reservation.status,
quantity: reservation.quantity || 1,
reservedDate: reservation.reservedDate
}));
res.json(formattedReservations);
} catch (error) {
console.error('Error fetching user reservations:', error);
res.status(500).json({
message: 'Failed to load reservations',
error: error.message
});
}
});
// Create reservation
router.post('/', auth, async (req, res) => {
try {
const item = await Item.findById(req.body.itemId);
if (!item) {
return res.status(404).json({ message: 'Item not found' });
}
const requestedQuantity = parseInt(req.body.quantity) || 1;
if (item.quantity < item.reserved + requestedQuantity) {
return res.status(400).json({ message: 'Requested quantity is not available' });
}
const reservation = new Reservation({
itemId: req.body.itemId,
userId: req.user._id,
quantity: requestedQuantity
});
await reservation.save();
item.reserved += requestedQuantity;
await item.save();
res.status(201).json(reservation);
} catch (error) {
res.status(400).json({ message: error.message });
}
});
// Update reservation status (admin can update any, students can only mark as returned)
router.patch('/:id', auth, async (req, res) => {
try {
const reservation = await Reservation.findById(req.params.id).populate('userId');
if (!reservation) {
return res.status(404).json({ message: 'Reservation not found' });
}
// Check authorization
const isAdmin = req.user.role === 'admin';
const isOwner = reservation.userId._id.toString() === req.user._id.toString();
const isReturning = req.body.status === 'RETURNED';
if (!isAdmin && (!isOwner || !isReturning)) {
return res.status(403).json({
message: 'Not authorized. Students can only return their own items.'
});
}
// Additional validation for students
if (!isAdmin && isReturning && reservation.status !== 'APPROVED') {
return res.status(400).json({
message: 'Can only return approved items'
});
}
const item = await Item.findById(reservation.itemId);
if (!item) {
return res.status(404).json({ message: 'Item not found' });
}
const oldStatus = reservation.status;
const newStatus = req.body.status;
reservation.status = newStatus;
// Include quantity in the update if provided
if (req.body.quantity !== undefined) {
reservation.quantity = req.body.quantity;
}
await reservation.save();
// Update item reserved count based on status change
if (oldStatus === 'PENDING' && newStatus === 'REJECTED') {
item.reserved = Math.max(0, item.reserved - (reservation.quantity || 1));
await item.save();
} else if (oldStatus === 'APPROVED' && newStatus === 'RETURNED') {
item.reserved = Math.max(0, item.reserved - (reservation.quantity || 1));
await item.save();
}
res.json(reservation);
} catch (error) {
console.error('Error updating reservation:', error);
res.status(400).json({ message: error.message });
}
});
// Delete reservation (admin can delete any, students can only delete their own)
router.delete('/:id', auth, async (req, res) => {
try {
const reservation = await Reservation.findById(req.params.id).populate('userId');
if (!reservation) {
return res.status(404).json({ message: 'Reservation not found' });
}
// Check if user is authorized to delete this reservation
if (req.user.role !== 'admin' && reservation.userId._id.toString() !== req.user._id.toString()) {
return res.status(403).json({ message: 'Not authorized to delete this reservation' });
}
// Update item's reserved count
const item = await Item.findById(reservation.itemId);
if (item) {
item.reserved = Math.max(0, item.reserved - 1); // Ensure it doesn't go below 0
await item.save();
}
await Reservation.findByIdAndDelete(req.params.id);
res.json({ message: 'Reservation deleted successfully' });
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// Archive reservation (admin only)
router.patch('/:id/archive', auth, adminOnly, async (req, res) => {
try {
const reservation = await Reservation.findById(req.params.id);
if (!reservation) {
return res.status(404).json({ message: 'Reservation not found' });
}
// Only allow archiving of returned reservations
if (reservation.status !== 'RETURNED') {
return res.status(400).json({ message: 'Can only archive returned reservations' });
}
reservation.status = 'ARCHIVED';
await reservation.save();
res.json({ message: 'Reservation archived successfully' });
} catch (error) {
console.error('Error archiving reservation:', error);
res.status(500).json({ message: error.message });
}
});
module.exports = router;

43
routes/upload.js Normal file
View File

@@ -0,0 +1,43 @@
const express = require('express');
const multer = require('multer');
const path = require('path');
const { auth, adminOnly } = require('../middleware/auth');
const router = express.Router();
// Configure multer for image upload
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'public/images/items/');
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, 'item-' + uniqueSuffix + path.extname(file.originalname));
}
});
const upload = multer({
storage: storage,
fileFilter: (req, file, cb) => {
if (file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('Not an image! Please upload an image.'), false);
}
}
});
// Upload image route
router.post('/', auth, adminOnly, upload.single('image'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ message: 'No file uploaded' });
}
const imageUrl = `/images/items/${req.file.filename}`;
res.json({ imageUrl });
} catch (error) {
res.status(500).json({ message: error.message });
}
});
module.exports = router;