database expansion prior to creating on server

This commit is contained in:
2025-11-20 00:45:53 -08:00
parent 367d12bd00
commit f830a12097
16 changed files with 732 additions and 663 deletions

139
server.ts
View File

@@ -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) {