From aacb26d8c8e041afd234058c283c91a48ad6f755 Mon Sep 17 00:00:00 2001 From: TSylv Date: Thu, 13 Nov 2025 00:28:13 -0600 Subject: [PATCH] Final project submission --- app.js | 13 +++ db/schema.sql | 27 +++++- db/seed.js | 59 +++++++++++- example.env | 2 - middleware/getUserFromToken.js | 20 +++- package-lock.json | 13 +++ package.json | 1 + routes/orders.js | 163 +++++++++++++++++++++++++++++++++ routes/products.js | 68 ++++++++++++++ routes/users.js | 85 +++++++++++++++++ server.js | 1 + 11 files changed, 443 insertions(+), 9 deletions(-) delete mode 100644 example.env create mode 100644 routes/orders.js create mode 100644 routes/products.js create mode 100644 routes/users.js diff --git a/app.js b/app.js index 8882fb6..d49d824 100644 --- a/app.js +++ b/app.js @@ -1,3 +1,16 @@ import express from "express"; +import usersRouter from "./routes/users.js"; +import productsRouter from "./routes/products.js"; +import ordersRouter from "./routes/orders.js"; +import getUserFromToken from "./middleware/getUserFromToken.js"; + const app = express(); + +app.use(express.json()); +app.use(getUserFromToken); + +app.use("/users", usersRouter); +app.use("/products", productsRouter); +app.use("/orders", ordersRouter); + export default app; diff --git a/db/schema.sql b/db/schema.sql index 2126a4a..c119ee5 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -1 +1,26 @@ --- TODO +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + username TEXT NOT NULL UNIQUE, + password TEXT NOT NULL +); + +CREATE TABLE orders ( + id SERIAL PRIMARY KEY, + date DATE NOT NULL, + note TEXT, + user_id INT NOT NULL REFERENCES users(id) +); + +CREATE TABLE products ( + id SERIAL PRIMARY KEY, + title TEXT NOT NULL, + description TEXT NOT NULL, + price DECIMAL NOT NULL +); + +CREATE TABLE orders_products ( + order_id INT NOT NULL REFERENCES orders(id), + product_id INT NOT NULL REFERENCES products(id), + quantity INT NOT NULL, + PRIMARY KEY (order_id, product_id) +); \ No newline at end of file diff --git a/db/seed.js b/db/seed.js index 57ca74a..72cc05c 100644 --- a/db/seed.js +++ b/db/seed.js @@ -1,3 +1,5 @@ +import "dotenv/config"; +import bcrypt from "bcrypt"; import db from "#db/client"; await db.connect(); @@ -6,5 +8,58 @@ await db.end(); console.log("🌱 Database seeded."); async function seed() { - // TODO -} + const products = [ + { title: "PC Case", description: "ATX mid-tower computer case with tempered glass side panel", price: 89.99 }, + { title: "Power Supply Unit", description: "750W 80+ Gold certified fully modular PSU", price: 119.99 }, + { title: "Graphics Card", description: "High-performance GPU suitable for gaming and video editing", price: 599.99 }, + { title: "CPU", description: "8-core, 16-thread processor for multitasking and gaming", price: 329.99 }, + { title: "Motherboard", description: "ATX motherboard with Wi-Fi and RGB support", price: 159.99 }, + { title: "RAM", description: "16GB DDR4 3200MHz memory kit", price: 69.99 }, + { title: "SSD", description: "1TB NVMe SSD with fast read/write speeds", price: 94.99 }, + { title: "CPU Cooler", description: "Dual-fan air cooler for optimal thermal performance", price: 49.99 }, + { title: "Case Fans", description: "RGB 120mm cooling fan pack (3-pack)", price: 39.99 }, + { title: "Wi-Fi Card", description: "PCIe Wi-Fi 6 wireless adapter", price: 29.99 } + ]; + + + for (const product of products) { + await db.query( + `INSERT INTO products (title, description, price) + VALUES ($1, $2, $3);`, + [product.title, product.description, product.price] + ); + } + + +const hashedPassword = await bcrypt.hash("password123", 10); + const userResult = await db.query( + `INSERT INTO users (username, password) + VALUES ($1, $2) + RETURNING id;`, + ["testuser", hashedPassword] + ); + const userId = userResult.rows[0].id; + + +const orderResult = await db.query( + `INSERT INTO orders (date, note, user_id) + VALUES (NOW(), 'First order', $1) + RETURNING id;`, + [userId] + ); + const orderId = orderResult.rows[0].id; + + +const productsResult = await db.query( + `SELECT id FROM products ORDER BY id LIMIT 5;` + ); + const productIds = productsResult.rows.map((row) => row.id); + + for (const productId of productIds) { + await db.query( + `INSERT INTO orders_products (order_id, product_id, quantity) + VALUES ($1, $2, $3);`, + [orderId, productId, 1] + ); + } +} \ No newline at end of file diff --git a/example.env b/example.env deleted file mode 100644 index 6574dd2..0000000 --- a/example.env +++ /dev/null @@ -1,2 +0,0 @@ -DATABASE_URL=postgres://user:password@localhost:5432/market -JWT_SECRET=youshouldreallyreplacethis diff --git a/middleware/getUserFromToken.js b/middleware/getUserFromToken.js index d12acc6..4c3258a 100644 --- a/middleware/getUserFromToken.js +++ b/middleware/getUserFromToken.js @@ -1,15 +1,27 @@ -import { getUserById } from "#db/queries/users"; -import { verifyToken } from "#utils/jwt"; +import db from "../db/client.js"; +import { verifyToken } from "../utils/jwt.js"; -/** Attaches the user to the request if a valid token is provided */ export default async function getUserFromToken(req, res, next) { const authorization = req.get("authorization"); + if (!authorization || !authorization.startsWith("Bearer ")) return next(); const token = authorization.split(" ")[1]; + try { const { id } = verifyToken(token); - const user = await getUserById(id); + + const result = await db.query( + `SELECT id, username FROM users WHERE id = $1`, + [id] + ); + + const user = result.rows[0]; + + if (!user) { + return res.status(401).send("Invalid token."); + } + req.user = user; next(); } catch (e) { diff --git a/package-lock.json b/package-lock.json index 9085be3..9a8f475 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "name": "market", "dependencies": { "bcrypt": "^5.1.1", + "dotenv": "^17.2.3", "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "pg": "^8.14.1" @@ -1280,6 +1281,18 @@ "wrappy": "1" } }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", diff --git a/package.json b/package.json index e4eb210..55212ef 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ }, "dependencies": { "bcrypt": "^5.1.1", + "dotenv": "^17.2.3", "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "pg": "^8.14.1" diff --git a/routes/orders.js b/routes/orders.js new file mode 100644 index 0000000..0ebafa7 --- /dev/null +++ b/routes/orders.js @@ -0,0 +1,163 @@ +import express from "express"; +import db from "../db/client.js"; +import requireUser from "../middleware/requireUser.js"; + +const router = express.Router(); + +router.get("/", requireUser, async (req, res, next) => { + try { + const userId = req.user.id; + + const result = await db.query( + ` + SELECT id, date, note, user_id + FROM orders + WHERE user_id = $1; + `, + [userId] + ); + + res.send(result.rows); + } catch (err) { + next(err); + } +}); + + +router.post("/", requireUser, async (req, res, next) => { + try { + const userId = req.user.id; + const { note } = req.body; + + const { + rows: [order], +} = await db.query( + ` + INSERT INTO orders (date, note, user_id) + VALUES (NOW(), $1, $2) + RETURNING id, date, note, user_id; + `, + [note, userId] +); + + + res.status(201).send(order); + } catch (err) { + next(err); + } +}); + + +router.get("/:id", requireUser, async (req, res, next) => { + try { + const userId = req.user.id; + const orderId = req.params.id; + + const { + rows: [order], + } = await db.query( + ` + SELECT id, date, note, user_id + FROM orders + WHERE id = $1 + AND user_id = $2; + `, + [orderId, userId] + ); + + if (!order) { + return res.status(404).send({ message: "Order not found" }); + } + + res.send(order); + } catch (err) { + next(err); + } +}); + + +router.post("/:id/products", requireUser, async (req, res, next) => { + try { + const userId = req.user.id; + const orderId = req.params.id; + const { product_id, quantity } = req.body; + + const { + rows: [order], + } = await db.query( + ` + SELECT id, user_id + FROM orders + WHERE id = $1 + AND user_id = $2; + `, + [orderId, userId] + ); + + if (!order) { + return res.status(404).send({ message: "Order not found" }); + } + + const { + rows: [orderProduct], +} = await db.query( + ` + INSERT INTO orders_products (order_id, product_id, quantity) + VALUES ($1, $2, $3) + RETURNING order_id, product_id, quantity; + `, + [orderId, product_id, quantity] +); + + + res.status(201).send(orderProduct); + } catch (err) { + next(err); + } +}); + + +router.get("/:id/products", requireUser, async (req, res, next) => { + try { + const userId = req.user.id; + const orderId = req.params.id; + + const { + rows: [order], + } = await db.query( + ` + SELECT id, user_id + FROM orders + WHERE id = $1 + AND user_id = $2; + `, + [orderId, userId] + ); + + if (!order) { + return res.status(404).send({ message: "Order not found" }); + } + + const result = await db.query( + ` + SELECT + products.id, + products.title, + products.description, + products.price, + orders_products.quantity + FROM orders_products + JOIN products + ON products.id = orders_products.product_id + WHERE orders_products.order_id = $1; + `, + [orderId] + ); + + res.send(result.rows); + } catch (err) { + next(err); + } +}); + +export default router; diff --git a/routes/products.js b/routes/products.js new file mode 100644 index 0000000..b38c29b --- /dev/null +++ b/routes/products.js @@ -0,0 +1,68 @@ +import express from "express"; +import db from "../db/client.js"; + +const router = express.Router(); + +router.get("/", async (req, res, next) => { + try { + const result = await db.query( + "SELECT id, title, description, price FROM products;" + ); + res.send(result.rows); + } catch (err) { + next(err); + } +}); + + +router.get("/:id", async (req, res, next) => { + try { + const { id } = req.params; + + const result = await db.query( + `SELECT id, title, description, price + FROM products + WHERE id = $1;`, + [id] + ); + + const product = result.rows[0]; + + if (!product) { + return res.status(404).send({ message: "Product not found" }); + } + + res.send(product); + } catch (err) { + next(err); + } +}); + + +router.get("/:id/orders", async (req, res, next) => { + try { + const { id } = req.params; + + const result = await db.query( + ` + SELECT + o.id AS order_id, + o.date, + o.note, + o.user_id, + op.quantity + FROM orders o + JOIN orders_products op ON o.id = op.order_id + WHERE op.product_id = $1; + `, + [id] + ); + + res.send(result.rows); + } catch (err) { + next(err); + } +}); + + +export default router; diff --git a/routes/users.js b/routes/users.js new file mode 100644 index 0000000..8a53039 --- /dev/null +++ b/routes/users.js @@ -0,0 +1,85 @@ +import express from "express"; +import db from "../db/client.js"; +import requireUser from "../middleware/requireUser.js"; +import requireBody from "../middleware/requireBody.js"; +import bcrypt from "bcrypt"; +import { createToken } from "../utils/jwt.js"; + +const router = express.Router(); + +router.get("/", async (req, res, next) => { + try { + const result = await db.query("SELECT id, username FROM users;"); + res.send(result.rows); + } catch (err) { + next(err); + } +}); + +router.post( + "/register", + requireBody(["username", "password"]), + async (req, res, next) => { + try { + const { username, password } = req.body; + + const hashedPassword = await bcrypt.hash(password, 10); + + const result = await db.query( + `INSERT INTO users (username, password) + VALUES ($1, $2) + RETURNING id, username;`, + [username, hashedPassword] + ); + + const user = result.rows[0]; + + const token = createToken({ id: user.id }); + + res.status(201).send({ user, token }); + } catch (err) { + next(err); + } + } +); + +router.post( + "/login", + requireBody(["username", "password"]), + async (req, res, next) => { + try { + const { username, password } = req.body; + + const result = await db.query( + `SELECT id, username, password + FROM users + WHERE username = $1;`, + [username] + ); + + const user = result.rows[0]; + + if (!user) { + return res.status(401).send({ message: "Invalid username or password" }); + } + + const isMatch = await bcrypt.compare(password, user.password); + + if (!isMatch) { + return res.status(401).send({ message: "Invalid username or password" }); + } + + const token = createToken({ id: user.id }); + + res.send({ + user: { id: user.id, username: user.username }, + token, + }); + + } catch (err) { + next(err); + } + } +); + +export default router; \ No newline at end of file diff --git a/server.js b/server.js index 28405e5..33adac0 100644 --- a/server.js +++ b/server.js @@ -1,3 +1,4 @@ +import "dotenv/config"; import app from "#app"; import db from "#db/client";