database expansion prior to creating on server
This commit is contained in:
139
server.ts
139
server.ts
@@ -13,7 +13,7 @@ import fs from 'fs/promises';
|
||||
import multer from 'multer';
|
||||
import * as db from './src/services/db';
|
||||
import { logger } from './src/services/logger';
|
||||
import { extractItemsFromReceiptImage } from './src/services/geminiService';
|
||||
import { extractItemsFromReceiptImage, extractCoreDataFromFlyerImage } from './src/services/geminiService';
|
||||
import { Profile, UserProfile, ShoppingListItem, ReceiptItem } from './src/types';
|
||||
|
||||
// Load environment variables from a .env file at the root of your project
|
||||
@@ -499,9 +499,18 @@ app.get('/api/users/best-sale-prices', passport.authenticate('jwt', { session: f
|
||||
});
|
||||
|
||||
app.get('/api/pantry-items/:id/conversions', passport.authenticate('jwt', { session: false }), async (req: Request, res: Response, next: NextFunction) => {
|
||||
const user = req.user as { id: string };
|
||||
const pantryItemId = parseInt(req.params.id, 10);
|
||||
// TODO: Add an ownership check to ensure the user owns this pantry item.
|
||||
try {
|
||||
// --- Ownership Check ---
|
||||
const owner = await db.findPantryItemOwner(pantryItemId);
|
||||
if (!owner) {
|
||||
return res.status(404).json({ message: 'Pantry item not found.' });
|
||||
}
|
||||
if (owner.user_id !== user.id) {
|
||||
logger.warn(`User ${user.id} attempted to access unauthorized pantry item ${pantryItemId}.`);
|
||||
return res.status(403).json({ message: 'Forbidden: You do not have access to this item.' });
|
||||
}
|
||||
const conversions = await db.suggestPantryItemConversions(pantryItemId);
|
||||
res.json(conversions);
|
||||
} catch (error) {
|
||||
@@ -581,6 +590,30 @@ app.put('/api/users/me/dietary-restrictions', passport.authenticate('jwt', { ses
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/users/me/appliances', passport.authenticate('jwt', { session: false }), async (req: Request, res: Response, next: NextFunction) => {
|
||||
const user = req.user as { id: string };
|
||||
try {
|
||||
const appliances = await db.getUserAppliances(user.id);
|
||||
res.json(appliances);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/users/me/appliances', passport.authenticate('jwt', { session: false }), async (req: Request, res: Response, next: NextFunction) => {
|
||||
const user = req.user as { id: string };
|
||||
const { applianceIds } = req.body;
|
||||
if (!Array.isArray(applianceIds)) {
|
||||
return res.status(400).json({ message: 'applianceIds must be an array of numbers.' });
|
||||
}
|
||||
try {
|
||||
await db.setUserAppliances(user.id, applianceIds);
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/users/me/compatible-recipes', passport.authenticate('jwt', { session: false }), async (req: Request, res: Response, next: NextFunction) => {
|
||||
const user = req.user as { id: string };
|
||||
try {
|
||||
@@ -1153,7 +1186,7 @@ app.put('/api/users/profile/preferences', passport.authenticate('jwt', { session
|
||||
});
|
||||
|
||||
// Protected Route to update user password
|
||||
app.put('/api/users/profile/password', passport.authenticate('jwt', { session: false }), async (req: Request, res: Response) => {
|
||||
app.put('/api/users/profile/password', passport.authenticate('jwt', { session: false }), async (req: Request, res: Response, next: NextFunction) => {
|
||||
const authenticatedUser = req.user as { id: string; email: string };
|
||||
const { newPassword } = req.body;
|
||||
|
||||
@@ -1161,30 +1194,28 @@ app.put('/api/users/profile/password', passport.authenticate('jwt', { session: f
|
||||
return res.status(400).json({ message: 'Password must be a string of at least 6 characters.' });
|
||||
}
|
||||
|
||||
// Hash the password before updating it
|
||||
const saltRounds = 10;
|
||||
const hashedPassword = await bcrypt.hash(newPassword, saltRounds);
|
||||
logger.info(`Hashing new password for user: ${authenticatedUser.email}`);
|
||||
|
||||
// We pass the hashed password to the db function
|
||||
await db.updateUserPassword(authenticatedUser.id, hashedPassword);
|
||||
|
||||
// --- Password Strength Check ---
|
||||
const MIN_PASSWORD_SCORE = 3; // Require a 'Good' or 'Strong' password (score 3 or 4)
|
||||
const strength = zxcvbn(newPassword);
|
||||
if (strength.score < MIN_PASSWORD_SCORE) {
|
||||
logger.warn(`Weak password update rejected for user: ${authenticatedUser.email}. Score: ${strength.score}`);
|
||||
logger.warn(`Weak password update rejected for user: ${authenticatedUser.email}. Score: ${strength.score}`, { userId: authenticatedUser.id });
|
||||
// Provide the user with helpful feedback from the strength analysis.
|
||||
const feedback = strength.feedback.warning || (strength.feedback.suggestions && strength.feedback.suggestions[0]);
|
||||
return res.status(400).json({ message: `New password is too weak. ${feedback || 'Please choose a stronger password.'}`.trim() });
|
||||
}
|
||||
|
||||
try {
|
||||
// Hash the password only after it has passed the strength check
|
||||
const saltRounds = 10;
|
||||
const hashedPassword = await bcrypt.hash(newPassword, saltRounds);
|
||||
logger.info(`Hashing new, validated password for user: ${authenticatedUser.email}`, { userId: authenticatedUser.id });
|
||||
|
||||
await db.updateUserPassword(authenticatedUser.id, hashedPassword);
|
||||
logger.info(`Successfully updated password for user: ${authenticatedUser.email}`);
|
||||
res.status(200).json({ message: 'Password updated successfully.' });
|
||||
} catch (error) {
|
||||
logger.error('Error during password update:', { error });
|
||||
res.status(500).json({ message: 'Failed to update password.' });
|
||||
logger.error('Error during password update:', { error, userId: authenticatedUser.id });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1233,6 +1264,41 @@ app.get('/api/admin/corrections', passport.authenticate('jwt', { session: false
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/admin/brands', passport.authenticate('jwt', { session: false }), isAdmin, async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const brands = await db.getAllBrands();
|
||||
res.json(brands);
|
||||
} catch (error) {
|
||||
logger.error('Error fetching brands in /api/admin/brands:', { error });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/ai/process-flyer', passport.authenticate('jwt', { session: false }), upload.array('flyerImages'), async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
if (!req.files || !Array.isArray(req.files) || req.files.length === 0) {
|
||||
return res.status(400).json({ message: 'Flyer image files are required.' });
|
||||
}
|
||||
|
||||
// Master items are sent as a JSON string in a separate field
|
||||
const masterItems = JSON.parse(req.body.masterItems);
|
||||
|
||||
const imagePaths = req.files.map(file => ({
|
||||
path: file.path,
|
||||
mimetype: file.mimetype
|
||||
}));
|
||||
|
||||
logger.info(`Starting AI flyer data extraction for ${imagePaths.length} image(s).`);
|
||||
const extractedData = await extractCoreDataFromFlyerImage(imagePaths, masterItems);
|
||||
logger.info(`Completed AI flyer data extraction. Found ${extractedData.items.length} items.`);
|
||||
|
||||
res.status(200).json({ data: extractedData });
|
||||
} catch (error) {
|
||||
logger.error('Error in /api/ai/process-flyer endpoint:', { error });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/admin/stats', passport.authenticate('jwt', { session: false }), isAdmin, async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const stats = await db.getApplicationStats();
|
||||
@@ -1304,6 +1370,23 @@ app.put('/api/admin/recipes/:id/status', passport.authenticate('jwt', { session:
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/admin/brands/:id/logo', passport.authenticate('jwt', { session: false }), isAdmin, upload.single('logoImage'), async (req: Request, res: Response, next: NextFunction) => {
|
||||
const brandId = parseInt(req.params.id, 10);
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ message: 'Logo image file is required.' });
|
||||
}
|
||||
|
||||
const logoUrl = `/assets/${req.file.filename}`;
|
||||
await db.updateBrandLogo(brandId, logoUrl);
|
||||
|
||||
logger.info(`Brand logo updated for brand ID: ${brandId}`, { brandId, logoUrl });
|
||||
res.status(200).json({ message: 'Brand logo updated successfully.', logoUrl });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
app.get('/api/admin/unmatched-items', passport.authenticate('jwt', { session: false }), isAdmin, async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
@@ -1315,21 +1398,6 @@ app.get('/api/admin/unmatched-items', passport.authenticate('jwt', { session: fa
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/admin/recipes/:id/status', passport.authenticate('jwt', { session: false }), isAdmin, async (req: Request, res: Response, next: NextFunction) => {
|
||||
const recipeId = parseInt(req.params.id, 10);
|
||||
const { status } = req.body;
|
||||
|
||||
if (!status || !['private', 'pending_review', 'public'].includes(status)) {
|
||||
return res.status(400).json({ message: 'A valid status (private, pending_review, public) is required.' });
|
||||
}
|
||||
try {
|
||||
const updatedRecipe = await db.updateRecipeStatus(recipeId, status);
|
||||
res.status(200).json(updatedRecipe);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/admin/comments/:id/status', passport.authenticate('jwt', { session: false }), isAdmin, async (req: Request, res: Response, next: NextFunction) => {
|
||||
const commentId = parseInt(req.params.id, 10);
|
||||
const { status } = req.body;
|
||||
@@ -1378,10 +1446,17 @@ app.post('/api/receipts/upload', passport.authenticate('jwt', { session: false }
|
||||
app.get('/api/receipts/:id/deals', passport.authenticate('jwt', { session: false }), async (req: Request, res: Response, next: NextFunction) => {
|
||||
const user = req.user as { id: string };
|
||||
const receiptId = parseInt(req.params.id, 10);
|
||||
|
||||
// TODO: Add an ownership check to ensure the user owns this receipt.
|
||||
|
||||
try {
|
||||
// --- Ownership Check ---
|
||||
const owner = await db.findReceiptOwner(receiptId);
|
||||
if (!owner) {
|
||||
return res.status(404).json({ message: 'Receipt not found.' });
|
||||
}
|
||||
if (owner.user_id !== user.id) {
|
||||
logger.warn(`User ${user.id} attempted to access unauthorized receipt ${receiptId}.`);
|
||||
return res.status(403).json({ message: 'Forbidden: You do not have access to this receipt.' });
|
||||
}
|
||||
|
||||
const deals = await db.findDealsForReceipt(receiptId);
|
||||
res.json(deals);
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user