241 lines
6.0 KiB
JavaScript
241 lines
6.0 KiB
JavaScript
const express = require('express');
|
|
const router = express.Router();
|
|
const jwt = require('jsonwebtoken');
|
|
const Recipe = require('../models/Recipe');
|
|
|
|
// Middleware to authenticate JWT token
|
|
function authenticateToken(req, res, next) {
|
|
const authHeader = req.headers['authorization'];
|
|
const token = authHeader && authHeader.split(' ')[1];
|
|
|
|
if (!token) {
|
|
return res.status(401).json({ error: 'Access token required' });
|
|
}
|
|
|
|
jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
|
|
if (err) {
|
|
return res.status(403).json({ error: 'Invalid or expired token' });
|
|
}
|
|
req.userId = decoded.userId;
|
|
next();
|
|
});
|
|
}
|
|
|
|
// Get ingredient suggestions for autocomplete
|
|
router.get('/ingredients/suggestions', async (req, res) => {
|
|
try {
|
|
const { q } = req.query; // Search query
|
|
|
|
if (!q || q.length < 2) {
|
|
return res.json([]);
|
|
}
|
|
|
|
// Aggregate unique ingredient names from all recipes
|
|
const suggestions = await Recipe.aggregate([
|
|
{ $unwind: '$ingredients' },
|
|
{
|
|
$match: {
|
|
'ingredients.name': {
|
|
$regex: q,
|
|
$options: 'i'
|
|
}
|
|
}
|
|
},
|
|
{
|
|
$group: {
|
|
_id: { $toLower: '$ingredients.name' },
|
|
name: { $first: '$ingredients.name' },
|
|
count: { $sum: 1 }
|
|
}
|
|
},
|
|
{ $sort: { count: -1, name: 1 } },
|
|
{ $limit: 10 },
|
|
{
|
|
$project: {
|
|
_id: 0,
|
|
name: 1,
|
|
count: 1
|
|
}
|
|
}
|
|
]);
|
|
|
|
res.json(suggestions);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Get unit suggestions for a specific ingredient
|
|
router.get('/ingredients/units', async (req, res) => {
|
|
try {
|
|
const { ingredient } = req.query;
|
|
|
|
if (!ingredient) {
|
|
return res.json([]);
|
|
}
|
|
|
|
// Find units used with this specific ingredient
|
|
const unitSuggestions = await Recipe.aggregate([
|
|
{ $unwind: '$ingredients' },
|
|
{
|
|
$match: {
|
|
'ingredients.name': {
|
|
$regex: ingredient,
|
|
$options: 'i'
|
|
}
|
|
}
|
|
},
|
|
{
|
|
$group: {
|
|
_id: { $toLower: '$ingredients.unit' },
|
|
unit: { $first: '$ingredients.unit' },
|
|
count: { $sum: 1 },
|
|
avgAmount: { $avg: '$ingredients.amount' }
|
|
}
|
|
},
|
|
{ $sort: { count: -1, unit: 1 } },
|
|
{ $limit: 8 },
|
|
{
|
|
$project: {
|
|
_id: 0,
|
|
unit: 1,
|
|
count: 1,
|
|
avgAmount: { $round: ['$avgAmount', 2] }
|
|
}
|
|
}
|
|
]);
|
|
|
|
res.json(unitSuggestions);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Get all recipes
|
|
router.get('/', async (req, res) => {
|
|
try {
|
|
const { category, difficulty, search } = req.query;
|
|
let filter = {};
|
|
|
|
if (category) filter.category = category;
|
|
if (difficulty) filter.difficulty = difficulty;
|
|
if (search) {
|
|
filter.$or = [
|
|
{ title: { $regex: search, $options: 'i' } },
|
|
{ description: { $regex: search, $options: 'i' } }
|
|
];
|
|
}
|
|
|
|
const recipes = await Recipe.find(filter).sort({ createdAt: -1 });
|
|
res.json(recipes);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Get recipe by ID
|
|
router.get('/:id', async (req, res) => {
|
|
try {
|
|
const recipe = await Recipe.findById(req.params.id);
|
|
if (!recipe) {
|
|
return res.status(404).json({ error: 'Recipe not found' });
|
|
}
|
|
res.json(recipe);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Create new recipe (protected route)
|
|
router.post('/', authenticateToken, async (req, res) => {
|
|
try {
|
|
const recipe = new Recipe({
|
|
...req.body,
|
|
createdBy: req.userId // Add user ID to track who created the recipe
|
|
});
|
|
await recipe.save();
|
|
res.status(201).json(recipe);
|
|
} catch (error) {
|
|
res.status(400).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Update recipe
|
|
router.put('/:id', async (req, res) => {
|
|
try {
|
|
const recipe = await Recipe.findByIdAndUpdate(
|
|
req.params.id,
|
|
req.body,
|
|
{ new: true, runValidators: true }
|
|
);
|
|
if (!recipe) {
|
|
return res.status(404).json({ error: 'Recipe not found' });
|
|
}
|
|
res.json(recipe);
|
|
} catch (error) {
|
|
res.status(400).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Delete recipe (protected route)
|
|
router.delete('/:id', authenticateToken, async (req, res) => {
|
|
try {
|
|
const recipeId = req.params.id;
|
|
|
|
// Find the recipe first to check ownership
|
|
const recipe = await Recipe.findById(recipeId);
|
|
if (!recipe) {
|
|
return res.status(404).json({ error: 'Recipe not found' });
|
|
}
|
|
|
|
// Check if user owns the recipe (only recipe creator can delete)
|
|
if (recipe.createdBy.toString() !== req.userId) {
|
|
return res.status(403).json({ error: 'You can only delete recipes you created' });
|
|
}
|
|
|
|
// Delete the recipe
|
|
await Recipe.findByIdAndDelete(recipeId);
|
|
|
|
// Remove recipe from all user selections
|
|
const UserSelection = require('../models/UserSelection');
|
|
await UserSelection.updateMany(
|
|
{ 'selectedRecipes.recipeId': recipeId },
|
|
{
|
|
$pull: {
|
|
selectedRecipes: { recipeId: recipeId }
|
|
}
|
|
}
|
|
);
|
|
|
|
// Also clean up aggregated ingredients that reference this recipe
|
|
await UserSelection.updateMany(
|
|
{ 'aggregatedIngredients.recipes.recipeId': recipeId },
|
|
{
|
|
$pull: {
|
|
'aggregatedIngredients.$[].recipes': { recipeId: recipeId }
|
|
}
|
|
}
|
|
);
|
|
|
|
// Remove empty aggregated ingredients (those with no recipes left)
|
|
await UserSelection.updateMany(
|
|
{},
|
|
{
|
|
$pull: {
|
|
aggregatedIngredients: { recipes: { $size: 0 } }
|
|
}
|
|
}
|
|
);
|
|
|
|
res.json({
|
|
message: 'Recipe deleted successfully and removed from all user menus',
|
|
deletedRecipeId: recipeId
|
|
});
|
|
} catch (error) {
|
|
console.error('Error deleting recipe:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|