require('dotenv').config(); const express = require('express'); const session = require('express-session'); const passport = require('passport'); const LocalStrategy = require('passport-local').Strategy; const expressLayouts = require('express-ejs-layouts'); const flash = require('express-flash'); const methodOverride = require('method-override'); const path = require('path'); const SQLiteStore = require('connect-sqlite3')(session); const db = require('./models'); const utils = require('./utils'); const { NutritionAPI, searchAllSources } = require('./utils/api_client'); const app = express(); const PORT = process.env.PORT || 5001; // Initialize API Client const apiClient = new NutritionAPI(process.env.API_NINJAS_KEY); // Passport Config passport.use(new LocalStrategy( async (username, password, done) => { try { const user = await db.User.findOne({ where: { username } }); if (!user) { return done(null, false, { message: 'Incorrect username.' }); } if (!user.validPassword(password)) { return done(null, false, { message: 'Incorrect password.' }); } return done(null, user); } catch (err) { return done(err); } } )); passport.serializeUser((user, done) => { done(null, user.id); }); passport.deserializeUser(async (id, done) => { try { const user = await db.User.findByPk(id); done(null, user); } catch (err) { done(err); } }); // Middleware app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, 'views')); app.set('layout', 'layout'); app.use(expressLayouts); app.use(express.static(path.join(__dirname, 'public'))); app.use(express.urlencoded({ extended: true })); app.use(express.json()); app.use(methodOverride('_method')); // Session app.use(session({ store: new SQLiteStore({ db: 'sessions.db', dir: './data' }), secret: process.env.SECRET_KEY || 'secret', resave: false, saveUninitialized: false, cookie: { maxAge: 30 * 24 * 60 * 60 * 1000 } // 30 days })); app.use(passport.initialize()); app.use(passport.session()); app.use(flash()); // Global Variables app.use((req, res, next) => { res.locals.current_user = req.user; res.locals.success_msg = req.flash('success_msg'); res.locals.error_msg = req.flash('error_msg'); res.locals.error = req.flash('error'); res.locals.path = req.path; next(); }); // Helper functions for templates app.locals.round = Math.round; // Auth Middleware function ensureAuthenticated(req, res, next) { if (req.isAuthenticated()) { return next(); } req.flash('error_msg', 'Please log in to view that resource'); res.redirect('/login'); } // Routes app.get('/', (req, res) => { if (req.isAuthenticated()) { res.redirect('/dashboard'); } else { res.redirect('/login'); } }); app.get('/login', (req, res) => { res.render('login'); }); app.post('/login', passport.authenticate('local', { successRedirect: '/dashboard', failureRedirect: '/login', failureFlash: true })); app.get('/register', (req, res) => { res.render('register'); }); app.post('/register', async (req, res) => { const { username, password, name } = req.body; try { const existingUser = await db.User.findOne({ where: { username } }); if (existingUser) { req.flash('error', 'Username already exists'); return res.redirect('/register'); } await db.User.create({ username, password, name }); req.flash('success_msg', 'You are now registered and can log in'); res.redirect('/login'); } catch (err) { console.error(err); req.flash('error', 'Error registering user'); res.redirect('/register'); } }); app.get('/logout', (req, res, next) => { req.logout((err) => { if (err) { return next(err); } req.flash('success_msg', 'You are logged out'); res.redirect('/login'); }); }); app.get('/dashboard', ensureAuthenticated, async (req, res) => { try { const today = new Date(); const dateStr = today.toISOString().split('T')[0]; // Get daily totals const nutrition = await utils.calculateDailyTotals(req.user.id, dateStr); const water = await utils.calculateWaterTotal(req.user.id, dateStr); // Get user goals let goals = await db.UserGoal.findOne({ where: { UserId: req.user.id } }); if (!goals) { goals = await db.UserGoal.create({ UserId: req.user.id, target_protein_g: 150, target_carbs_g: 200, target_fat_g: 60, target_water_ml: 2000 }); } // Get weight info const weightLogToday = await db.WeightLog.findOne({ where: { UserId: req.user.id, date: dateStr } }); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); const yesterdayStr = yesterday.toISOString().split('T')[0]; const weightLogYesterday = await db.WeightLog.findOne({ where: { UserId: req.user.id, date: yesterdayStr } }); let weightChange = null; if (weightLogToday && weightLogYesterday) { weightChange = weightLogToday.weight_kg - weightLogYesterday.weight_kg; } // Calculate remaining const remaining = { calories: req.user.target_daily_calories - nutrition.calories, protein: goals.target_protein_g - nutrition.protein, carbs: goals.target_carbs_g - nutrition.carbs, fat: goals.target_fat_g - nutrition.fat, water: goals.target_water_ml - water.total_ml }; // Get macro percentages const macroPercentages = utils.getMacroPercentages( nutrition.protein, nutrition.carbs, nutrition.fat ); // Get trends const weightTrend = await utils.getWeightTrend(req.user.id, 7); const calorieTrend = await utils.getCalorieTrend(req.user.id, 7); // Suggestions const suggestions = utils.suggestFoodsForMacros( remaining.protein, remaining.carbs, remaining.fat ); res.render('dashboard', { nutrition, water, goals, remaining, macro_percentages: macroPercentages, weight_today: weightLogToday, weight_change: weightChange, weight_trend: weightTrend, calorie_trend: calorieTrend, suggestions, today: dateStr }); } catch (err) { console.error(err); req.flash('error_msg', 'Error loading dashboard'); res.render('dashboard', { nutrition: { calories: 0, protein: 0, carbs: 0, fat: 0 }, water: { total_ml: 0 }, goals: { target_water_ml: 2000 }, remaining: { calories: 2000, protein: 150, carbs: 200, fat: 60, water: 2000 }, macro_percentages: { protein: 0, carbs: 0, fat: 0 }, weight_trend: [], calorie_trend: [], suggestions: [], today: new Date().toISOString().split('T')[0] }); } }); app.get('/add-meal', ensureAuthenticated, (req, res) => { res.render('add_meal', { today: new Date().toISOString().split('T')[0] }); }); app.post('/add-meal', ensureAuthenticated, async (req, res) => { try { let { date, meal_type, time, 'food_id[]': foodIds, 'quantity[]': quantities } = req.body; if (!foodIds) { req.flash('error_msg', 'Please add at least one food item'); return res.redirect('/add-meal'); } // Normalize input (handle single item vs array) if (!Array.isArray(foodIds)) { foodIds = [foodIds]; quantities = [quantities]; } const meal = await db.Meal.create({ UserId: req.user.id, date: date || new Date(), meal_type, time: time || null }); for (let i = 0; i < foodIds.length; i++) { const foodId = foodIds[i]; const quantity = quantities[i]; if (foodId && quantity) { const food = await db.FoodItem.findByPk(foodId); if (food) { await db.MealFood.create({ MealId: meal.id, FoodItemId: foodId, quantity: quantity, calories_consumed: food.calories * quantity, protein_consumed: food.protein_g * quantity, carbs_consumed: food.carbs_g * quantity, fat_consumed: food.fat_g * quantity }); } } } await utils.updateDailySummary(req.user.id, date); req.flash('success_msg', 'Meal added successfully!'); res.redirect('/dashboard'); } catch (err) { console.error(err); req.flash('error_msg', 'Error adding meal'); res.redirect('/add-meal'); } }); app.get('/api/search-food', ensureAuthenticated, async (req, res) => { const query = req.query.q || ''; if (query.length < 2) { return res.json([]); } try { const results = await searchAllSources(query, apiClient); res.json(results); } catch (err) { console.error(err); res.status(500).json({ error: 'Server error' }); } }); app.post('/api/add-food', ensureAuthenticated, async (req, res) => { try { const data = req.body; const food = await apiClient.saveFoodToDb(data); if (food) { res.json({ success: true, food_id: food.id, name: food.name }); } else { res.status(500).json({ success: false }); } } catch (err) { console.error(err); res.status(500).json({ success: false }); } }); app.post('/add-water', ensureAuthenticated, async (req, res) => { try { const amountMl = parseInt(req.body.amount_ml) || 250; const date = req.body.date || new Date().toISOString().split('T')[0]; await db.WaterLog.create({ UserId: req.user.id, date: date, amount_ml: amountMl, time: new Date() }); await utils.updateDailySummary(req.user.id, date); req.flash('success_msg', `Added ${amountMl}ml of water!`); res.redirect('/dashboard'); } catch (err) { console.error(err); req.flash('error_msg', 'Error adding water'); res.redirect('/dashboard'); } }); app.post('/add-weight', ensureAuthenticated, async (req, res) => { try { const weightKg = parseFloat(req.body.weight_kg); const date = req.body.date || new Date().toISOString().split('T')[0]; let weightLog = await db.WeightLog.findOne({ where: { UserId: req.user.id, date: date } }); if (weightLog) { weightLog.weight_kg = weightKg; weightLog.time = new Date(); await weightLog.save(); } else { await db.WeightLog.create({ UserId: req.user.id, date: date, weight_kg: weightKg, time: new Date() }); } await utils.updateDailySummary(req.user.id, date); res.redirect('/dashboard'); } catch (err) { console.error(err); req.flash('error_msg', 'Error adding weight'); res.redirect('/dashboard'); } }); app.get('/goals', ensureAuthenticated, async (req, res) => { try { const userGoals = await db.UserGoal.findOne({ where: { UserId: req.user.id } }); let bmr = null; let tdee = null; if (req.user.weight_kg && req.user.height_cm && req.user.age) { bmr = utils.calculateBMR( req.user.weight_kg, req.user.height_cm, req.user.age, req.user.gender || 'male' ); tdee = utils.calculateTDEE(bmr, req.user.activity_level || 'moderate'); } res.render('goals', { goals: userGoals, bmr, tdee }); } catch (err) { console.error(err); req.flash('error_msg', 'Error loading goals'); res.redirect('/dashboard'); } }); app.post('/goals', ensureAuthenticated, async (req, res) => { try { // Update user info const user = await db.User.findByPk(req.user.id); user.age = parseInt(req.body.age) || 25; user.gender = req.body.gender || 'male'; user.height_cm = parseFloat(req.body.height_cm) || 170; user.weight_kg = parseFloat(req.body.weight_kg) || 70; user.activity_level = req.body.activity_level || 'moderate'; // Calculate targets const bmr = utils.calculateBMR(user.weight_kg, user.height_cm, user.age, user.gender); const tdee = utils.calculateTDEE(bmr, user.activity_level); const goalType = req.body.goal_type || 'recomp'; let targetCalories; if (goalType === 'weight_loss') { targetCalories = tdee - 500; } else if (goalType === 'muscle_gain') { targetCalories = tdee + 300; } else { targetCalories = tdee; } user.target_daily_calories = Math.round(targetCalories); await user.save(); // Update or create goals let userGoals = await db.UserGoal.findOne({ where: { UserId: req.user.id } }); if (!userGoals) { userGoals = await db.UserGoal.create({ UserId: req.user.id }); } userGoals.goal_type = goalType; userGoals.target_weight_kg = parseFloat(req.body.target_weight_kg) || 70; // Calculate macros const macros = utils.calculateMacroTargets(user.weight_kg, goalType); userGoals.target_protein_g = macros.protein_g; userGoals.target_carbs_g = macros.carbs_g; userGoals.target_fat_g = macros.fat_g; userGoals.target_water_ml = parseInt(req.body.target_water_ml) || 2000; await userGoals.save(); req.flash('success_msg', 'Goals updated successfully!'); res.redirect('/goals'); } catch (err) { console.error(err); req.flash('error_msg', 'Error updating goals'); res.redirect('/goals'); } }); app.get('/foods', ensureAuthenticated, async (req, res) => { try { const category = req.query.category || 'all'; const searchQuery = req.query.q || ''; const whereClause = {}; if (category !== 'all') { whereClause.category = category; } if (searchQuery) { whereClause[db.Sequelize.Op.or] = [ { name: { [db.Sequelize.Op.like]: `%${searchQuery}%` } }, { name_tagalog: { [db.Sequelize.Op.like]: `%${searchQuery}%` } } ]; } const filipinoWhere = { ...whereClause, is_filipino: true }; const otherWhere = { ...whereClause, is_filipino: false }; const filipinoFoods = await db.FoodItem.findAll({ where: filipinoWhere, order: [['name', 'ASC']] }); const otherFoods = await db.FoodItem.findAll({ where: otherWhere, limit: 20, order: [['name', 'ASC']] }); const categories = ['all', 'kanin', 'ulam', 'sabaw', 'gulay', 'meryenda', 'almusal']; res.render('foods', { filipino_foods: filipinoFoods, other_foods: otherFoods, categories, current_category: category, search_query: searchQuery }); } catch (err) { console.error(err); req.flash('error_msg', 'Error loading foods'); res.redirect('/dashboard'); } }); app.get('/progress', ensureAuthenticated, async (req, res) => { try { const days = parseInt(req.query.days) || 30; const weightTrend = await utils.getWeightTrend(req.user.id, days); const calorieTrend = await utils.getCalorieTrend(req.user.id, days); let avgCalories = 0, avgProtein = 0, avgCarbs = 0, avgFat = 0; if (calorieTrend.length > 0) { avgCalories = calorieTrend.reduce((sum, d) => sum + d.calories, 0) / calorieTrend.length; avgProtein = calorieTrend.reduce((sum, d) => sum + d.protein, 0) / calorieTrend.length; avgCarbs = calorieTrend.reduce((sum, d) => sum + d.carbs, 0) / calorieTrend.length; avgFat = calorieTrend.reduce((sum, d) => sum + d.fat, 0) / calorieTrend.length; } let weightChange = null; if (weightTrend.length >= 2) { weightChange = weightTrend[weightTrend.length - 1].weight_kg - weightTrend[0].weight_kg; } res.render('progress', { weight_trend: weightTrend, calorie_trend: calorieTrend, avg_calories: Math.round(avgCalories), avg_protein: Math.round(avgProtein), avg_carbs: Math.round(avgCarbs), avg_fat: Math.round(avgFat), weight_change: weightChange, days }); } catch (err) { console.error(err); req.flash('error_msg', 'Error loading progress'); res.redirect('/dashboard'); } }); app.get('/meal-planner', ensureAuthenticated, async (req, res) => { try { const today = new Date(); // Calculate Monday of current week const day = today.getDay(); const diff = today.getDate() - day + (day === 0 ? -6 : 1); // adjust when day is sunday const startDate = new Date(today); startDate.setDate(diff); const dates = []; for (let i = 0; i < 7; i++) { const d = new Date(startDate); d.setDate(startDate.getDate() + i); dates.push(d); } const mealPlans = {}; for (const d of dates) { const dateStr = d.toISOString().split('T')[0]; const plans = await db.MealPlan.findAll({ where: { UserId: req.user.id, date: dateStr }, include: [{ model: db.PlannedFood, include: [db.FoodItem] }] }); mealPlans[dateStr] = plans.map(p => { let calories = 0, protein = 0, carbs = 0, fat = 0; const foods = []; if (p.PlannedFoods) { p.PlannedFoods.forEach(pf => { if (pf.FoodItem) { const q = pf.quantity || 1; calories += pf.FoodItem.calories * q; protein += pf.FoodItem.protein_g * q; carbs += pf.FoodItem.carbs_g * q; fat += pf.FoodItem.fat_g * q; foods.push({ name: pf.FoodItem.name, quantity: q }); } }); } return { id: p.id, meal_type: p.meal_type, is_completed: p.is_completed, foods: foods, totals: { calories, protein, carbs, fat } }; }); } res.render('meal_planner', { dates: dates.map(d => d.toISOString().split('T')[0]), meal_plans: mealPlans, today: new Date().toISOString().split('T')[0] }); } catch (err) { console.error(err); req.flash('error_msg', 'Error loading meal planner'); res.redirect('/dashboard'); } }); app.post('/meal-planner/auto-generate', ensureAuthenticated, async (req, res) => { try { const { date } = req.body; if (!date) { req.flash('error_msg', 'Please select a date'); return res.redirect('/meal-planner'); } await utils.generateDailyMealPlan(req.user.id, date); req.flash('success_msg', 'Meal plan generated successfully!'); res.redirect('/meal-planner'); } catch (err) { console.error(err); req.flash('error_msg', 'Error generating meal plan'); res.redirect('/meal-planner'); } }); app.post('/meal-planner/add', ensureAuthenticated, async (req, res) => { try { let { date, meal_type, 'food_id[]': foodIds, 'quantity[]': quantities } = req.body; if (!foodIds) { req.flash('error_msg', 'Please add at least one food item'); return res.redirect('/meal-planner'); } // Normalize input if (!Array.isArray(foodIds)) { foodIds = [foodIds]; quantities = [quantities]; } // Find or create meal plan let mealPlan = await db.MealPlan.findOne({ where: { UserId: req.user.id, date: date, meal_type: meal_type } }); if (!mealPlan) { mealPlan = await db.MealPlan.create({ UserId: req.user.id, date: date, meal_type: meal_type }); } // Add foods for (let i = 0; i < foodIds.length; i++) { const foodId = foodIds[i]; const quantity = quantities[i]; if (foodId && quantity) { await db.PlannedFood.create({ MealPlanId: mealPlan.id, FoodItemId: foodId, quantity: quantity }); } } req.flash('success_msg', 'Meal plan updated!'); res.redirect('/meal-planner'); } catch (err) { console.error(err); req.flash('error_msg', 'Error adding meal plan'); res.redirect('/meal-planner'); } }); app.post('/meal-planner/complete/:id', ensureAuthenticated, async (req, res) => { try { const plan = await db.MealPlan.findOne({ where: { id: req.params.id, UserId: req.user.id } }); if (plan) { plan.is_completed = !plan.is_completed; await plan.save(); // If marked complete, optionally create actual meal log? // For now just toggle status as per request simplicity. } res.redirect('/meal-planner'); } catch (err) { console.error(err); req.flash('error_msg', 'Error updating plan status'); res.redirect('/meal-planner'); } }); app.post('/meal-planner/delete/:id', ensureAuthenticated, async (req, res) => { try { await db.MealPlan.destroy({ where: { id: req.params.id, UserId: req.user.id } }); req.flash('success_msg', 'Meal plan deleted'); res.redirect('/meal-planner'); } catch (err) { console.error(err); req.flash('error_msg', 'Error deleting meal plan'); res.redirect('/meal-planner'); } }); // Database Sync and Server Start const startServer = async () => { try { await db.sequelize.sync(); app.listen(PORT, () => console.log(`Server started on port ${PORT}`)); } catch (err) { console.error('Database connection error:', err); } }; startServer();