diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d6a15f3 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +PORT=3000 + +NODE_ENV=development + +SESSION_SECRET=change_this_secret + +DATABASE=database.sqlite \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..391c439 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +node_modules/ + +.env + +database.sqlite + +storage/logs/* +storage/backups/* + +public/uploads/* + +.vscode + +.idea \ No newline at end of file diff --git a/.open b/.open new file mode 100644 index 0000000..e69de29 diff --git a/app.js b/app.js index 979f0c5..3c51a4c 100644 --- a/app.js +++ b/app.js @@ -1,13 +1,30 @@ require("dotenv").config(); const http = require("http"); +const { Server } = require("socket.io"); const createApp = require("./config/express"); +const session = require("./config/session"); +const socketHandlers = require("./sockets/handlers"); const app = createApp(); const server = http.createServer(app); +const io = new Server(server, { + cors: { + origin: "*", + methods: ["GET", "POST"] + } +}); + +io.use((socket, next) => session(socket.request, {}, next)); + +socketHandlers(io); + +// Make io available in routes +app.locals.io = io; + const PORT = process.env.PORT || 3000; server.listen(PORT, () => { @@ -22,4 +39,6 @@ server.listen(PORT, () => { console.log("==================================="); -}); \ No newline at end of file + console.log(""); + +}); diff --git a/config/database.js b/config/database.js index 41e35b3..ffb170d 100644 --- a/config/database.js +++ b/config/database.js @@ -1,15 +1,14 @@ const Database = require("better-sqlite3"); const path = require("path"); -const databaseFile = path.join( - __dirname, - "..", - process.env.DATABASE || "database.sqlite" -); +const configuredDatabase = process.env.DATABASE || "database.sqlite"; +const databaseFile = path.isAbsolute(configuredDatabase) + ? configuredDatabase + : path.join(__dirname, "..", configuredDatabase); const db = new Database(databaseFile); db.pragma("journal_mode = WAL"); db.pragma("foreign_keys = ON"); -module.exports = db; \ No newline at end of file +module.exports = db; diff --git a/config/express.js b/config/express.js index 35ebbe3..e0267d9 100644 --- a/config/express.js +++ b/config/express.js @@ -57,9 +57,24 @@ module.exports = () => { app.use(session); -app.use(userMiddleware); + app.use(flash()); -app.use(flash()); + app.use(userMiddleware); + + app.use((req, res, next) => { + + res.locals.user = req.user || null; + + res.locals.success = req.flash("success"); + res.locals.error = req.flash("error"); + res.locals.info = req.flash("info"); + res.locals.warning = req.flash("warning"); + + res.locals.title = res.locals.title || "PantryHub"; + + next(); + + }); app.use( @@ -69,6 +84,13 @@ app.use(flash()); ); + // API Routes + app.use( + "/api", + require("../routes/api") + ); + + // Web Routes app.use( "/", require("../routes/auth") diff --git a/config/session.js b/config/session.js index 0c44709..ad93068 100644 --- a/config/session.js +++ b/config/session.js @@ -14,14 +14,17 @@ module.exports = session({ } }), - secret: process.env.SESSION_SECRET, + secret: process.env.SESSION_SECRET || "change-this-development-secret", resave: false, saveUninitialized: false, cookie: { - maxAge: 1000 * 60 * 60 * 24 * 30 + maxAge: 1000 * 60 * 60 * 24 * 30, + httpOnly: true, + sameSite: "lax", + secure: process.env.NODE_ENV === "production" } -}); \ No newline at end of file +}); diff --git a/controllers/AuthController.js b/controllers/AuthController.js index dd1f9cd..b29406c 100644 --- a/controllers/AuthController.js +++ b/controllers/AuthController.js @@ -10,20 +10,23 @@ class AuthController { registerForm(req, res) { - res.render("auth/register", { - title: "Registreren" - }); + res.locals.title = "Registreren"; + + res.render("auth/register"); } async register(req, res) { - const { - name, - email, - password - } = req.body; + const name = (req.body.name || "").trim(); + const email = (req.body.email || "").trim().toLowerCase(); + const password = req.body.password || ""; + + if (name.length < 2 || !/^\S+@\S+\.\S+$/.test(email) || password.length < 8) { + req.flash("error", "Vul een naam, geldig e-mailadres en een wachtwoord van minimaal 8 tekens in."); + return res.redirect("/register"); + } const existingUser = db.prepare( @@ -67,7 +70,7 @@ class AuthController { ); - const userId = result.lastInsertRowid; + const userId = result.lastInsertRowid; HouseholdService.createForUser( @@ -78,6 +81,10 @@ class AuthController { req.session.userId = userId; + req.flash( + "success", + "Welkom bij PantryHub!" + ); res.redirect("/"); @@ -87,9 +94,18 @@ class AuthController { loginForm(req, res) { - res.render("auth/login", { - title: "Inloggen" - }); + res.locals.title = "Inloggen"; + + if (req.query.logout) { + + req.flash( + "info", + "Je bent uitgelogd." + ); + + } + + res.render("auth/login"); } @@ -97,10 +113,8 @@ class AuthController { async login(req, res) { - const { - email, - password - } = req.body; + const email = (req.body.email || "").trim().toLowerCase(); + const password = req.body.password || ""; const user = db.prepare( @@ -141,6 +155,10 @@ class AuthController { req.session.userId = user.id; + req.flash( + "success", + `Welkom terug ${user.name}!` + ); res.redirect("/"); @@ -152,7 +170,9 @@ class AuthController { req.session.destroy(() => { - res.redirect("/login"); + res.clearCookie("connect.sid"); + + res.redirect("/login?logout=1"); }); @@ -162,4 +182,4 @@ class AuthController { } -module.exports = new AuthController(); \ No newline at end of file +module.exports = new AuthController(); diff --git a/controllers/DashboardController.js b/controllers/DashboardController.js index 686d599..cb9cb30 100644 --- a/controllers/DashboardController.js +++ b/controllers/DashboardController.js @@ -1,25 +1,28 @@ -class DashboardController { +const ShoppingListRepository = require("../repositories/ShoppingListRepository"); +class DashboardController { index(req, res) { + const lists = ShoppingListRepository.getByHousehold( + req.user.household_id + ); + + const recentLists = lists.slice(0, 5); + + res.locals.title = "Dashboard"; res.render( "dashboard/index", { - - title: "Dashboard", - - user: req.user - + user: req.user, + lists: recentLists, + totalLists: lists.length } ); - } - } - module.exports = new DashboardController(); \ No newline at end of file diff --git a/controllers/HomeController.js b/controllers/HomeController.js index 6750343..274e61f 100644 --- a/controllers/HomeController.js +++ b/controllers/HomeController.js @@ -2,11 +2,9 @@ class HomeController { index(req, res) { - res.render("home/index", { + res.locals.title = "PantryHub"; - title: "PantryHub" - - }); + res.render("home/index"); } diff --git a/controllers/HouseholdController.js b/controllers/HouseholdController.js index 66cb980..5e8448f 100644 --- a/controllers/HouseholdController.js +++ b/controllers/HouseholdController.js @@ -1,21 +1,21 @@ +const UserRepository = require("../repositories/UserRepository"); + class HouseholdController { + index(req, res) { - index(req,res){ + res.locals.title = "Huishouden"; res.render( "household/index", { - title:"Huishouden", - user:req.user + user: req.user, + members: UserRepository.getHouseholdMembers(req.user.household_id) } ); } - } - -module.exports = -new HouseholdController(); \ No newline at end of file +module.exports = new HouseholdController(); diff --git a/controllers/ProductController.js b/controllers/ProductController.js index c39bf1a..2e39d0f 100644 --- a/controllers/ProductController.js +++ b/controllers/ProductController.js @@ -1,35 +1,53 @@ const ProductService = require("../services/ProductService"); +const db = require("../config/database"); + class ProductController { - - index(req,res) { - + index(req, res) { const products = ProductService.getProducts(); + const categories = db.prepare(` + SELECT * FROM categories ORDER BY name + `).all(); + + res.locals.title = "Producten"; res.render( "products/index", { - - title:"Producten", - - products - + products, + categories, + user: req.user } ); + } + + create(req, res) { + + const { name, category_id } = req.body; + + if (!name || name.trim().length === 0) { + req.flash("error", "Geef een productnaam op"); + return res.redirect("/products"); + } + + ProductService.createProduct({ + name: name.trim(), + category_id: category_id || null + }); + + req.flash("success", "Product toegevoegd"); + res.redirect("/products"); } - - } - module.exports = new ProductController(); \ No newline at end of file diff --git a/controllers/ShoppingListController.js b/controllers/ShoppingListController.js new file mode 100644 index 0000000..6b23544 --- /dev/null +++ b/controllers/ShoppingListController.js @@ -0,0 +1,179 @@ +const ShoppingListRepository = require("../repositories/ShoppingListRepository"); +const ProductRepository = require("../repositories/ProductRepository"); + +class ShoppingListController { + + index(req, res) { + + const lists = ShoppingListRepository.getByHousehold( + req.user.household_id + ); + + res.locals.title = "Boodschappenlijsten"; + + res.render("lists/index", { + lists, + user: req.user + }); + + } + + create(req, res) { + + const { name } = req.body; + + if (!name || name.trim().length === 0) { + req.flash("error", "Geef een naam op"); + return res.redirect("/lists"); + } + + // Check if user has a household + if (!req.user || !req.user.household_id) { + req.flash("error", "Je bent niet aan een huishouden gekoppeld"); + return res.redirect("/lists"); + } + + ShoppingListRepository.create({ + household_id: req.user.household_id, + name: name.trim() + }); + + req.flash("success", "Boodschappenlijst aangemaakt"); + res.redirect("/lists"); + + } + + show(req, res) { + + const { id } = req.params; + + const list = ShoppingListRepository.getById(id); + + if (!list) { + return res.status(404).render("errors/404"); + } + + if (list.household_id !== req.user.household_id) { + return res.status(403).render("errors/404"); + } + + const items = ShoppingListRepository.getItems(id); + + res.locals.title = list.name; + + res.render("lists/show", { + list, + items, + user: req.user + }); + + } + + addItem(req, res) { + + const { id } = req.params; + const { product_id, amount } = req.body; + + const list = ShoppingListRepository.getById(id); + + if (!list) { + return res.status(404).json({ error: "Lijst niet gevonden" }); + } + + if (list.household_id !== req.user.household_id) { + return res.status(403).json({ error: "Niet gemachtigd" }); + } + + const product = ProductRepository.findById(product_id); + + if (!product) { + return res.status(404).json({ error: "Product niet gevonden" }); + } + + const quantity = Number(amount); + if (!Number.isFinite(quantity) || quantity <= 0 || quantity > 999) { + req.flash("error", "Vul een geldige hoeveelheid in"); + return res.redirect(`/lists/${id}`); + } + + const item = ShoppingListRepository.addItem({ + list_id: id, + product_id, + amount: quantity + }); + + req.app.locals.io.to(`household:${req.user.household_id}`).emit("item:added", { + listId: Number(id) + }); + + req.flash("success", "Product toegevoegd"); + + if (req.xhr || req.headers.accept?.includes("application/json")) { + return res.json({ success: true, item }); + } + + res.redirect(`/lists/${id}`); + + } + + toggleItem(req, res) { + + const { id, itemId } = req.params; + + const list = ShoppingListRepository.getById(id); + + if (!list || list.household_id !== req.user.household_id) { + return res.status(403).json({ error: "Niet gemachtigd" }); + } + + if (!ShoppingListRepository.findItem(id, itemId)) { + return res.status(404).json({ error: "Item niet gevonden" }); + } + + ShoppingListRepository.toggleItem(itemId); + + req.app.locals.io.to(`household:${req.user.household_id}`).emit("item:toggled", { + listId: Number(id), itemId: Number(itemId) + }); + + if (req.xhr || req.headers.accept?.includes("application/json")) { + return res.json({ success: true }); + } + + res.redirect(`/lists/${id}`); + + } + + deleteItem(req, res) { + + const { id, itemId } = req.params; + + const list = ShoppingListRepository.getById(id); + + if (!list || list.household_id !== req.user.household_id) { + return res.status(403).json({ error: "Niet gemachtigd" }); + } + + if (!ShoppingListRepository.findItem(id, itemId)) { + return res.status(404).json({ error: "Item niet gevonden" }); + } + + ShoppingListRepository.deleteItem(itemId); + + req.app.locals.io.to(`household:${req.user.household_id}`).emit("item:removed", { + listId: Number(id), itemId: Number(itemId) + }); + + req.flash("success", "Product verwijderd"); + + if (req.method === "DELETE" || req.xhr || req.headers.accept?.includes("application/json")) { + return res.json({ success: true }); + } + + res.redirect(`/lists/${id}`); + + } + +} + +module.exports = new ShoppingListController(); diff --git a/database.sqlite-shm b/database.sqlite-shm index fe9ac28..2039af5 100644 Binary files a/database.sqlite-shm and b/database.sqlite-shm differ diff --git a/database.sqlite-wal b/database.sqlite-wal index e69de29..e990415 100644 Binary files a/database.sqlite-wal and b/database.sqlite-wal differ diff --git a/database/seed.js b/database/seed.js index f4326ad..91c1f4d 100644 --- a/database/seed.js +++ b/database/seed.js @@ -49,16 +49,29 @@ const products = [ ]; +const insertCategory = db.prepare(` + +INSERT INTO categories (name, icon) +SELECT ?, ? +WHERE NOT EXISTS (SELECT 1 FROM categories WHERE name = ?) + +`); + +const seedCategories = db.transaction(() => { + + for (const category of categories) { + insertCategory.run(category[0], category[1], category[0]); + } + +}); + +seedCategories(); + const insertProduct = db.prepare(` -INSERT OR IGNORE INTO products - -( - name, - category_id -) - -VALUES (?,?) +INSERT INTO products (name, category_id) +SELECT ?, ? +WHERE NOT EXISTS (SELECT 1 FROM products WHERE name = ?) `); @@ -70,7 +83,7 @@ const seedProducts = db.transaction(() => { for(const product of products){ - insertProduct.run(product); + insertProduct.run(product[0], product[1], product[0]); } @@ -80,28 +93,5 @@ const seedProducts = db.transaction(() => { seedProducts(); -const insert = db.prepare(` -INSERT OR IGNORE INTO categories -(name, icon) - -VALUES (?,?) - -`); - - -const seed = db.transaction(() => { - - for (const category of categories) { - - insert.run(category); - - } - -}); - - -seed(); - - -console.log("Seed voltooid."); \ No newline at end of file +console.log("Seed voltooid."); diff --git a/middleware/auth.js b/middleware/auth.js index 01c7f78..2b957d4 100644 --- a/middleware/auth.js +++ b/middleware/auth.js @@ -3,6 +3,10 @@ module.exports = function(req, res, next) { if (!req.session.userId) { + if (req.originalUrl.startsWith("/api/")) { + return res.status(401).json({ error: "Je bent niet ingelogd" }); + } + return res.redirect("/login"); } @@ -10,4 +14,4 @@ module.exports = function(req, res, next) { next(); -}; \ No newline at end of file +}; diff --git a/open b/open new file mode 100644 index 0000000..e69de29 diff --git a/public/css/style.css b/public/css/style.css index ec192fa..c16a7da 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1,12 +1,43 @@ body { + background: #f5f7fb; +} - background-color: #f6f8fb; +.navbar { + margin-bottom: 25px; +} + +.card { + border: 0; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,.06); +} + +.page-title { + font-weight: 700; +} + +.alert { + + animation: fadeIn .3s ease; } +@keyframes fadeIn { -.navbar-brand { + from{ - font-weight: 700; + opacity:0; + + transform:translateY(-10px); + + } + + to{ + + opacity:1; + + transform:translateY(0); + + } } \ No newline at end of file diff --git a/public/icons/icon.svg b/public/icons/icon.svg new file mode 100644 index 0000000..1f6ea9a --- /dev/null +++ b/public/icons/icon.svg @@ -0,0 +1,5 @@ + diff --git a/public/js/app.js b/public/js/app.js new file mode 100644 index 0000000..71b89d7 --- /dev/null +++ b/public/js/app.js @@ -0,0 +1,48 @@ +// Socket.IO Client +const socket = typeof io === 'function' ? io() : null; + +// Join household room if available +const householdId = document.querySelector('[data-household-id]')?.dataset.householdId; +if (socket && householdId) { + socket.emit('join:household', householdId); +} + +// Real-time list updates +socket?.on('list:updated', (data) => { + console.log('📝 Lijst bijgewerkt:', data); + window.location.reload(); +}); + +socket?.on('item:added', (data) => { + console.log('➕ Item toegevoegd:', data); + window.location.reload(); +}); + +socket?.on('item:toggled', (data) => { + console.log('✓ Item afgevinkt:', data); + window.location.reload(); +}); + +socket?.on('item:removed', (data) => { + console.log('✕ Item verwijderd:', data); + window.location.reload(); +}); + +// Auto-remove alerts +document.addEventListener("DOMContentLoaded", () => { + + setTimeout(() => { + + document + .querySelectorAll(".alert") + .forEach(alert => { + + alert.remove(); + + }); + + }, 4000); + +}); + +console.log('✅ PantryHub app geladen'); diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..08d60c5 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,43 @@ +{ + "name": "PantryHub", + "short_name": "PantryHub", + "description": "Realtime boodschappenapp", + "start_url": "/", + "scope": "/", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#0066cc", + "icons": [ + { + "src": "/icons/icon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any" + }, + { + "src": "/icons/icon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any" + } + ], + "shortcuts": [ + { + "name": "Lijsten", + "short_name": "Lijsten", + "description": "Bekijk je boodschappenlijsten", + "url": "/lists", + "icons": [ + { + "src": "/icons/icon.svg", + "sizes": "any", + "type": "image/svg+xml" + } + ] + } + ], + "categories": [ + "shopping", + "lifestyle" + ] +} diff --git a/public/service-worker.js b/public/service-worker.js new file mode 100644 index 0000000..7e1d1d7 --- /dev/null +++ b/public/service-worker.js @@ -0,0 +1,98 @@ +const CACHE_NAME = 'pantryhub-v2'; +const urlsToCache = [ + '/', + '/css/style.css', + '/js/app.js', + '/manifest.json', + '/icons/icon.svg' +]; + +// Install event +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => { + return cache.addAll(urlsToCache).catch(err => { + console.log('Cache addAll error:', err); + }); + }) + ); +}); + +// Activate event +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => { + if (cacheName !== CACHE_NAME) { + return caches.delete(cacheName); + } + }) + ); + }) + ); +}); + +// Fetch event +self.addEventListener('fetch', (event) => { + // Skip non-GET requests + if (event.request.method !== 'GET') { + return; + } + + // Browser extensions and third-party resources cannot be stored by Cache API. + // Only handle same-origin HTTP(S) requests belonging to PantryHub. + const url = new URL(event.request.url); + if (!['http:', 'https:'].includes(url.protocol) || url.origin !== self.location.origin) { + return; + } + + // For API calls, use network-first strategy + if (event.request.url.includes('/api/')) { + event.respondWith( + fetch(event.request) + .then(response => { + const responseClone = response.clone(); + caches.open(CACHE_NAME).then(cache => { + cache.put(event.request, responseClone); + }); + return response; + }) + .catch(() => { + return caches.match(event.request); + }) + ); + return; + } + + // For other requests, use cache-first strategy + event.respondWith( + caches.match(event.request) + .then(response => { + if (response) { + return response; + } + return fetch(event.request).then(response => { + if (!response || response.status !== 200 || response.type !== 'basic') { + return response; + } + const responseClone = response.clone(); + caches.open(CACHE_NAME) + .then(cache => { + cache.put(event.request, responseClone); + }); + return response; + }); + }) + .catch(() => { + // Return offline page or empty response + return new Response('Offline - pagina niet beschikbaar', { + status: 503, + statusText: 'Service Unavailable', + headers: new Headers({ + 'Content-Type': 'text/plain' + }) + }); + }) + ); +}); diff --git a/repositories/ShoppingListRepository.js b/repositories/ShoppingListRepository.js new file mode 100644 index 0000000..f58f80f --- /dev/null +++ b/repositories/ShoppingListRepository.js @@ -0,0 +1,108 @@ +const db = require("../config/database"); + +class ShoppingListRepository { + + getByHousehold(householdId) { + + return db.prepare(` + SELECT + shopping_lists.*, + COUNT(shopping_list_items.id) as item_count, + SUM(CASE WHEN shopping_list_items.checked = 1 THEN 1 ELSE 0 END) as checked_count + FROM shopping_lists + LEFT JOIN shopping_list_items + ON shopping_lists.id = shopping_list_items.list_id + WHERE shopping_lists.household_id = ? + AND shopping_lists.archived = 0 + GROUP BY shopping_lists.id + ORDER BY shopping_lists.created_at DESC + `).all(householdId); + + } + + getById(id) { + + return db.prepare(` + SELECT * FROM shopping_lists WHERE id = ? + `).get(id); + + } + + create(data) { + + return db.prepare(` + INSERT INTO shopping_lists + (household_id, name) + VALUES (?, ?) + `).run(data.household_id, data.name); + + } + + getItems(listId) { + + return db.prepare(` + SELECT + shopping_list_items.*, + products.name as product_name, + categories.name as category_name, + categories.icon as category_icon + FROM shopping_list_items + JOIN products ON products.id = shopping_list_items.product_id + LEFT JOIN categories ON categories.id = products.category_id + WHERE shopping_list_items.list_id = ? + ORDER BY shopping_list_items.checked ASC, shopping_list_items.sort_order ASC + `).all(listId); + + } + + addItem(data) { + + return db.prepare(` + INSERT INTO shopping_list_items + (list_id, product_id, amount) + VALUES (?, ?, ?) + `).run(data.list_id, data.product_id, data.amount); + + } + + findItem(listId, itemId) { + + return db.prepare(` + SELECT id FROM shopping_list_items + WHERE id = ? AND list_id = ? + `).get(itemId, listId); + + } + + toggleItem(itemId) { + + return db.prepare(` + UPDATE shopping_list_items + SET checked = CASE WHEN checked = 1 THEN 0 ELSE 1 END + WHERE id = ? + `).run(itemId); + + } + + deleteItem(itemId) { + + return db.prepare(` + DELETE FROM shopping_list_items + WHERE id = ? + `).run(itemId); + + } + + archiveList(listId) { + + return db.prepare(` + UPDATE shopping_lists + SET archived = 1 + WHERE id = ? + `).run(listId); + + } + +} + +module.exports = new ShoppingListRepository(); diff --git a/repositories/UserRepository.js b/repositories/UserRepository.js index 8a38e32..67be4c7 100644 --- a/repositories/UserRepository.js +++ b/repositories/UserRepository.js @@ -28,8 +28,20 @@ class UserRepository { } + getHouseholdMembers(householdId) { + + return db.prepare(` + SELECT users.id, users.name, users.email, household_members.role + FROM household_members + JOIN users ON users.id = household_members.user_id + WHERE household_members.household_id = ? + ORDER BY CASE household_members.role WHEN 'OWNER' THEN 0 ELSE 1 END, users.name + `).all(householdId); + + } + } -module.exports = new UserRepository(); \ No newline at end of file +module.exports = new UserRepository(); diff --git a/routes/api.js b/routes/api.js new file mode 100644 index 0000000..ae7f8f3 --- /dev/null +++ b/routes/api.js @@ -0,0 +1,43 @@ +const router = require("express").Router(); + +const auth = require("../middleware/auth"); + +const ProductRepository = require("../repositories/ProductRepository"); +const ShoppingListRepository = require("../repositories/ShoppingListRepository"); + +// Get all products +router.get("/products", auth, (req, res) => { + + const products = ProductRepository.getAll(); + + res.json(products); + +}); + +// Get shopping lists for household +router.get("/lists", auth, (req, res) => { + + const lists = ShoppingListRepository.getByHousehold( + req.user.household_id + ); + + res.json(lists); + +}); + +// Get shopping list items +router.get("/lists/:id/items", auth, (req, res) => { + + const list = ShoppingListRepository.getById(req.params.id); + + if (!list || list.household_id !== req.user.household_id) { + return res.status(403).json({ error: "Not authorized" }); + } + + const items = ShoppingListRepository.getItems(req.params.id); + + res.json(items); + +}); + +module.exports = router; diff --git a/routes/auth.js b/routes/auth.js index 2fb4382..3a4737b 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -4,9 +4,25 @@ const router = require("express").Router(); const AuthController = require("../controllers/AuthController"); +const HomeController = +require("../controllers/HomeController"); + +// Home page +router.get( + "/home", + HomeController.index +); + +// Register router.get( "/register", + (req, res, next) => { + if (req.user) { + return res.redirect("/"); + } + next(); + }, AuthController.registerForm ); @@ -17,9 +33,15 @@ router.post( ); - +// Login router.get( "/login", + (req, res, next) => { + if (req.user) { + return res.redirect("/"); + } + next(); + }, AuthController.loginForm ); @@ -30,7 +52,7 @@ router.post( ); - +// Logout router.get( "/logout", AuthController.logout diff --git a/routes/web.js b/routes/web.js index 22d6ebf..0702f36 100644 --- a/routes/web.js +++ b/routes/web.js @@ -11,8 +11,8 @@ require("../controllers/HouseholdController"); const ProductController = require("../controllers/ProductController"); -console.log("AUTH:", auth); -console.log("DASHBOARD INDEX:", DashboardController.index); +const ShoppingListController = +require("../controllers/ShoppingListController"); router.get( "/", @@ -20,6 +20,42 @@ router.get( DashboardController.index ); +router.get( + "/lists", + auth, + ShoppingListController.index.bind(ShoppingListController) +); + +router.post( + "/lists", + auth, + ShoppingListController.create.bind(ShoppingListController) +); + +router.get( + "/lists/:id", + auth, + ShoppingListController.show.bind(ShoppingListController) +); + +router.post( + "/lists/:id/items", + auth, + ShoppingListController.addItem.bind(ShoppingListController) +); + +router.post( + "/lists/:id/items/:itemId/toggle", + auth, + ShoppingListController.toggleItem.bind(ShoppingListController) +); + +router.delete( + "/lists/:id/items/:itemId", + auth, + ShoppingListController.deleteItem.bind(ShoppingListController) +); + router.get( "/household", auth, @@ -27,12 +63,15 @@ router.get( ); router.get( - "/products", - auth, - ProductController.index.bind(ProductController) - ); + +router.post( + "/products", + auth, + ProductController.create.bind(ProductController) +); + module.exports = router; \ No newline at end of file diff --git a/sessions.sqlite b/sessions.sqlite index c3805c5..a62a3af 100644 Binary files a/sessions.sqlite and b/sessions.sqlite differ diff --git a/sockets/handlers.js b/sockets/handlers.js new file mode 100644 index 0000000..b269d7a --- /dev/null +++ b/sockets/handlers.js @@ -0,0 +1,55 @@ +const UserRepository = require("../repositories/UserRepository"); + +module.exports = function(io) { + + io.on("connection", (socket) => { + + const userId = socket.request.session?.userId; + const user = userId && UserRepository.findById(userId); + + if (!user || !user.household_id) { + socket.disconnect(true); + return; + } + + socket.join(`household:${user.household_id}`); + + console.log("🔌 Gebruiker verbonden:", socket.id); + + socket.on("join:household", () => { + // The room is derived from the authenticated session above. + }); + + socket.on("list:updated", (data) => { + + io.to(`household:${data.householdId}`).emit("list:updated", data); + + }); + + socket.on("item:added", (data) => { + + io.to(`household:${data.householdId}`).emit("item:added", data); + + }); + + socket.on("item:toggled", (data) => { + + io.to(`household:${data.householdId}`).emit("item:toggled", data); + + }); + + socket.on("item:removed", (data) => { + + io.to(`household:${data.householdId}`).emit("item:removed", data); + + }); + + socket.on("disconnect", () => { + + console.log("❌ Gebruiker verbroken:", socket.id); + + }); + + }); + +}; diff --git a/views/auth/login.ejs b/views/auth/login.ejs index 0835a5d..ed12ea3 100644 --- a/views/auth/login.ejs +++ b/views/auth/login.ejs @@ -1,55 +1,81 @@ -
+ + Nog geen account? + + Registreren + +
+ ++ + Al een account? + + Inloggen + +
+ ++ Huishouden: <%= user.household_name || "Geen huishouden" %> +
-+
- -Rol: - - -<%= user.role %> - - -
- - - +Rol: <%= user.role %>
Delen van deze code met anderen:
+ +Je hebt nog geen boodschappenlijsten. Maak er een aan!
+ + <% } else { %> + +Nog geen producten op de lijst.
+ + <% } else { %> + +Nog geen producten.
+ <% } else { %> -+