Update backend for RBAC
This commit is contained in:
parent
d972335de5
commit
2a7457c614
0
.gitignore
vendored
Executable file → Normal file
0
.gitignore
vendored
Executable file → Normal file
0
backend/.dockerignore
Executable file → Normal file
0
backend/.dockerignore
Executable file → Normal file
0
backend/Dockerfile
Executable file → Normal file
0
backend/Dockerfile
Executable file → Normal file
46
backend/app.js
Normal file
46
backend/app.js
Normal file
@ -0,0 +1,46 @@
|
||||
require("dotenv").config();
|
||||
const express = require("express");
|
||||
const cors = require("cors");
|
||||
|
||||
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
const allowedOrigins = [
|
||||
"http://localhost:3000",
|
||||
"https://costco.nicosaya.com",
|
||||
"https://costco.api.nicosaya.com",
|
||||
];
|
||||
app.use(
|
||||
cors({
|
||||
origin: function (origin, callback) {
|
||||
if (!origin) return callback(null, true);
|
||||
if (allowedOrigins.includes(origin)) return callback(null, true);
|
||||
if (/^http:\/\/192\.168\.\d+\.\d+/.test(origin)) return callback(null, true);
|
||||
if (/^https:\/\/192\.168\.\d+\.\d+/.test(origin)) return callback(null, true);
|
||||
callback(new Error("Not allowed by CORS"));
|
||||
},
|
||||
methods: ["GET", "POST", "PUT", "DELETE"],
|
||||
})
|
||||
);
|
||||
|
||||
app.get('/', async (req, res) => {
|
||||
res.status(200).send('Grocery List API is running.');
|
||||
});
|
||||
|
||||
|
||||
const authRoutes = require("./routes/auth.routes");
|
||||
app.use("/auth", authRoutes);
|
||||
|
||||
const listRoutes = require("./routes/list.routes");
|
||||
app.use("/list", listRoutes);
|
||||
|
||||
const adminRoutes = require("./routes/admin.routes");
|
||||
app.use("/admin", adminRoutes);
|
||||
|
||||
const usersRoutes = require("./routes/users.routes");
|
||||
app.use("/users", usersRoutes);
|
||||
|
||||
|
||||
module.exports = app;
|
||||
33
backend/controllers/auth.controller.js
Normal file
33
backend/controllers/auth.controller.js
Normal file
@ -0,0 +1,33 @@
|
||||
const bcrypt = require("bcryptjs");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const User = require("../models/user.model");
|
||||
|
||||
exports.register = async (req, res) => {
|
||||
const { email, password, role } = req.body;
|
||||
|
||||
try {
|
||||
const hash = await bcrypt.hash(password, 10);
|
||||
const user = await User.createUser(email, hash, role);
|
||||
res.json({ message: "User registered", user });
|
||||
} catch (err) {
|
||||
res.status(400).json({ message: "Registration failed", error: err });
|
||||
}
|
||||
};
|
||||
|
||||
exports.login = async (req, res) => {
|
||||
const { email, password } = req.body;
|
||||
|
||||
const user = await User.findByEmail(email);
|
||||
if (!user) return res.status(401).json({ message: "Invalid credentials" });
|
||||
|
||||
const valid = await bcrypt.compare(password, user.password);
|
||||
if (!valid) return res.status(401).json({ message: "Invalid credentials" });
|
||||
|
||||
const token = jwt.sign(
|
||||
{ id: user.id, role: user.role },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: "1d" }
|
||||
);
|
||||
|
||||
res.json({ token, role: user.role });
|
||||
};
|
||||
24
backend/controllers/lists.controller.js
Normal file
24
backend/controllers/lists.controller.js
Normal file
@ -0,0 +1,24 @@
|
||||
const List = require("../models/list.model");
|
||||
|
||||
|
||||
exports.getList = async (req, res) => {
|
||||
const items = await List.getUnboughtItems();
|
||||
res.json(items);
|
||||
};
|
||||
|
||||
|
||||
exports.addItem = async (req, res) => {
|
||||
const { item_name, quantity } = req.body;
|
||||
|
||||
const id = await List.addOrUpdateItem(item_name, quantity);
|
||||
|
||||
await List.addHistoryRecord(id, quantity);
|
||||
|
||||
res.json({ message: "Item added/updated" });
|
||||
};
|
||||
|
||||
|
||||
exports.markBought = async (req, res) => {
|
||||
await List.setBought(req.body.id);
|
||||
res.json({ message: "Item marked bought" });
|
||||
};
|
||||
6
backend/controllers/users.controller.js
Normal file
6
backend/controllers/users.controller.js
Normal file
@ -0,0 +1,6 @@
|
||||
const User = require("../models/user.model");
|
||||
|
||||
exports.getAllUsers = async (req, res) => {
|
||||
const users = await User.getAllUsers();
|
||||
res.json(users);
|
||||
};
|
||||
11
backend/db/pool.js
Normal file
11
backend/db/pool.js
Normal file
@ -0,0 +1,11 @@
|
||||
const { Pool } = require("pg");
|
||||
|
||||
const pool = new Pool({
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASS,
|
||||
host: process.env.DB_HOST,
|
||||
database: process.env.DB_NAME,
|
||||
port: 5432,
|
||||
});
|
||||
|
||||
module.exports = pool;
|
||||
19
backend/middleware/auth.js
Normal file
19
backend/middleware/auth.js
Normal file
@ -0,0 +1,19 @@
|
||||
const jwt = require("jsonwebtoken");
|
||||
|
||||
function auth(req, res, next) {
|
||||
const header = req.headers.authorization;
|
||||
if (!header) return res.status(401).json({ message: "Missing token" });
|
||||
|
||||
const token = header.split(" ")[1];
|
||||
if (!token) return res.status(401).json({ message: "Invalid token format" });
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
req.user = decoded; // id + role
|
||||
next();
|
||||
} catch (err) {
|
||||
res.status(401).json({ message: "Invalid or expired token" });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = auth;
|
||||
11
backend/middleware/rbac.js
Normal file
11
backend/middleware/rbac.js
Normal file
@ -0,0 +1,11 @@
|
||||
function requireRole(...allowedRoles) {
|
||||
return (req, res, next) => {
|
||||
if (!req.user) return res.status(401).json({ message: "Authentication required" });
|
||||
if (!allowedRoles.includes(req.user.role))
|
||||
return res.status(403).json({ message: "Forbidden" });
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = requireRole;
|
||||
46
backend/models/list.model.js
Normal file
46
backend/models/list.model.js
Normal file
@ -0,0 +1,46 @@
|
||||
const pool = require("../db/pool");
|
||||
|
||||
|
||||
exports.getUnboughtItems = async () => {
|
||||
const result = await pool.query(
|
||||
"SELECT * FROM grocery_list WHERE bought = FALSE ORDER BY id ASC"
|
||||
);
|
||||
return result.rows;
|
||||
};
|
||||
|
||||
|
||||
exports.addOrUpdateItem = async (item_name, quantity) => {
|
||||
const result = await pool.query(
|
||||
"SELECT id, bought FROM grocery_list WHERE item_name = $1",
|
||||
[item_name]
|
||||
);
|
||||
|
||||
if (result.rowCount > 0) {
|
||||
await pool.query(
|
||||
"UPDATE grocery_list SET quantity = $1, bought = FALSE WHERE id = $2",
|
||||
[quantity, result.rows[0].id]
|
||||
);
|
||||
return result.rows[0].id;
|
||||
} else {
|
||||
const insert = await pool.query(
|
||||
"INSERT INTO grocery_list (item_name, quantity) VALUES ($1, $2) RETURNING id",
|
||||
[item_name, quantity]
|
||||
);
|
||||
return insert.rows[0].id;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
exports.setBought = async (id) => {
|
||||
await pool.query("UPDATE grocery_list SET bought = TRUE WHERE id = $1", [id]);
|
||||
};
|
||||
|
||||
|
||||
exports.addHistoryRecord = async (itemId, quantity) => {
|
||||
await pool.query(
|
||||
`INSERT INTO grocery_history (list_item_id, quantity, added_on)
|
||||
VALUES ($1, $2, NOW())`,
|
||||
[itemId, quantity]
|
||||
);
|
||||
};
|
||||
|
||||
22
backend/models/user.model.js
Normal file
22
backend/models/user.model.js
Normal file
@ -0,0 +1,22 @@
|
||||
const pool = require("../db/pool");
|
||||
|
||||
exports.findByUsername = async (username) => {
|
||||
const result = await pool.query("SELECT * FROM users WHERE username = $1", [username]);
|
||||
return result.rows[0];
|
||||
};
|
||||
|
||||
exports.createUser = async (username, hashedPassword, name, role = "viewer") => {
|
||||
const result = await pool.query(
|
||||
`INSERT INTO users (username, password, name, role)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id, username, role`,
|
||||
[username, hashedPassword, name, role]
|
||||
);
|
||||
return result.rows[0];
|
||||
};
|
||||
|
||||
|
||||
exports.getAllUsers = async () => {
|
||||
const result = await pool.query("SELECT id, username, name, role FROM users ORDER BY id ASC");
|
||||
return result.rows;
|
||||
};
|
||||
99
backend/package-lock.json
generated
Executable file → Normal file
99
backend/package-lock.json
generated
Executable file → Normal file
@ -5,9 +5,11 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.3",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"pg": "^8.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -607,6 +609,14 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bcryptjs": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
||||
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
|
||||
"bin": {
|
||||
"bcrypt": "bin/bcrypt"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz",
|
||||
@ -669,6 +679,11 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
@ -1044,6 +1059,14 @@
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ecdsa-sig-formatter": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
@ -1906,6 +1929,46 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonwebtoken": {
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
||||
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
|
||||
"dependencies": {
|
||||
"jws": "^3.2.2",
|
||||
"lodash.includes": "^4.3.0",
|
||||
"lodash.isboolean": "^3.0.3",
|
||||
"lodash.isinteger": "^4.0.4",
|
||||
"lodash.isnumber": "^3.0.3",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.isstring": "^4.0.1",
|
||||
"lodash.once": "^4.0.0",
|
||||
"ms": "^2.1.1",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12",
|
||||
"npm": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jwa": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
|
||||
"integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "^1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jws": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
||||
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
|
||||
"dependencies": {
|
||||
"jwa": "^1.4.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/kind-of": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
|
||||
@ -1918,6 +1981,41 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
|
||||
},
|
||||
"node_modules/lodash.isboolean": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
|
||||
},
|
||||
"node_modules/lodash.isinteger": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
|
||||
},
|
||||
"node_modules/lodash.isnumber": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
|
||||
},
|
||||
"node_modules/lodash.isstring": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
|
||||
},
|
||||
"node_modules/lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz",
|
||||
@ -3331,7 +3429,6 @@
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
|
||||
2
backend/package.json
Executable file → Normal file
2
backend/package.json
Executable file → Normal file
@ -1,8 +1,10 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.3",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"pg": "^8.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
9
backend/routes/admin.routes.js
Normal file
9
backend/routes/admin.routes.js
Normal file
@ -0,0 +1,9 @@
|
||||
const router = require("express").Router();
|
||||
const auth = require("../middleware/auth");
|
||||
const requireRole = require("../middleware/rbac");
|
||||
const usersController = require("../controllers/users.controller");
|
||||
|
||||
// Admin-only route
|
||||
router.get("/users", auth, requireRole("admin"), usersController.getAllUsers);
|
||||
|
||||
module.exports = router;
|
||||
7
backend/routes/auth.routes.js
Normal file
7
backend/routes/auth.routes.js
Normal file
@ -0,0 +1,7 @@
|
||||
const router = require("express").Router();
|
||||
const controller = require("../controllers/auth.controller");
|
||||
|
||||
router.post("/register", controller.register);
|
||||
router.post("/login", controller.login);
|
||||
|
||||
module.exports = router;
|
||||
14
backend/routes/list.routes.js
Normal file
14
backend/routes/list.routes.js
Normal file
@ -0,0 +1,14 @@
|
||||
const router = require("express").Router();
|
||||
const controller = require("../controllers/lists.controller");
|
||||
const auth = require("../middleware/auth");
|
||||
const requireRole = require("../middleware/rbac");
|
||||
|
||||
|
||||
router.get("/", auth, requireRole("viewer", "editor", "admin"), controller.getList);
|
||||
|
||||
|
||||
router.post("/add", auth, requireRole("editor", "admin"), controller.addItem);
|
||||
router.post("/mark-bought", auth, requireRole("editor", "admin"), controller.markBought);
|
||||
|
||||
|
||||
module.exports = router;
|
||||
8
backend/routes/users.routes.js
Normal file
8
backend/routes/users.routes.js
Normal file
@ -0,0 +1,8 @@
|
||||
const router = require("express").Router();
|
||||
const auth = require("../middleware/auth");
|
||||
const requireRole = require("../middleware/rbac");
|
||||
const usersController = require("../controllers/users.controller");
|
||||
|
||||
router.get("/", auth, requireRole("admin"), usersController.getAllUsers);
|
||||
|
||||
module.exports = router;
|
||||
104
backend/server.js
Executable file → Normal file
104
backend/server.js
Executable file → Normal file
@ -1,103 +1,7 @@
|
||||
require('dotenv').config();
|
||||
const app = require("./app");
|
||||
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const { Pool } = require('pg');
|
||||
const PORT = process.env.PORT || 5000;
|
||||
|
||||
const app = express();
|
||||
const port = 5000;
|
||||
|
||||
const pool = new Pool({
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASS,
|
||||
host: process.env.DB_HOST,
|
||||
database: process.env.DB_NAME,
|
||||
port: 5432,
|
||||
});
|
||||
|
||||
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
const allowedOrigins = [
|
||||
"http://localhost:3000",
|
||||
"https://costco.nicosaya.com",
|
||||
"https://costco.api.nicosaya.com",
|
||||
];
|
||||
app.use(cors({
|
||||
origin: function (origin, callback) {
|
||||
if (!origin) return callback(null, true);
|
||||
if (allowedOrigins.includes(origin)) return callback(null, true);
|
||||
if (/^http:\/\/192\.168\.\d+\.\d+/.test(origin)) return callback(null, true);
|
||||
if (/^https:\/\/192\.168\.\d+\.\d+/.test(origin)) return callback(null, true);
|
||||
callback(new Error("Not allowed by CORS"));
|
||||
},
|
||||
methods: ["GET", "POST"],
|
||||
}));
|
||||
|
||||
|
||||
|
||||
app.get('/', async (req, res) => {
|
||||
const { query } = req.query;
|
||||
const { rows } = await pool.query(
|
||||
'SELECT DISTINCT item_name FROM grocery_list WHERE item_name ILIKE $1 LIMIT 10',
|
||||
[`%${query}%`]
|
||||
app.listen(PORT, () =>
|
||||
console.log(`Backend running on http://localhost:${PORT}`)
|
||||
);
|
||||
res.status(200).send('Grocery List API is running.');
|
||||
});
|
||||
|
||||
|
||||
app.get('/suggest', async (req, res) => {
|
||||
const { query } = req.query;
|
||||
const { rows } = await pool.query(
|
||||
'SELECT DISTINCT item_name FROM grocery_list WHERE item_name ILIKE $1 LIMIT 10',
|
||||
[`%${query}%`]
|
||||
);
|
||||
res.json(rows.map(r => r.item_name));
|
||||
});
|
||||
|
||||
|
||||
app.post('/add', async (req, res) => {
|
||||
const { item_name, quantity } = req.body;
|
||||
const result = await pool.query(
|
||||
'SELECT id, bought FROM grocery_list WHERE item_name = $1',
|
||||
[item_name]
|
||||
);
|
||||
|
||||
let listItemId;
|
||||
if (result.rowCount > 0) {
|
||||
listItemId = result.rows[0].id;
|
||||
await pool.query(
|
||||
'UPDATE grocery_list SET quantity = $1, bought = FALSE WHERE id = $2',
|
||||
[quantity, listItemId]
|
||||
);
|
||||
res.json({ message: 'Item re-added with updated quantity.' });
|
||||
} else {
|
||||
const insertResult = await pool.query(
|
||||
'INSERT INTO grocery_list (item_name, quantity) VALUES ($1, $2) RETURNING id',
|
||||
[item_name, quantity]
|
||||
);
|
||||
listItemId = insertResult.rows[0].id;
|
||||
res.json({ message: 'Item added to list.' });
|
||||
}
|
||||
|
||||
await pool.query(
|
||||
'INSERT INTO grocery_history (list_item_id, quantity, added_on) VALUES ($1, $2, NOW())',
|
||||
[listItemId, quantity]
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
app.post('/mark-bought', async (req, res) => {
|
||||
const { id } = req.body;
|
||||
await pool.query('UPDATE grocery_list SET bought = TRUE WHERE id = $1', [id]);
|
||||
res.json({ message: 'Item marked as bought.' });
|
||||
});
|
||||
|
||||
|
||||
app.get('/list', async (req, res) => {
|
||||
const { rows } = await pool.query('SELECT * FROM grocery_list WHERE bought = FALSE');
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
app.listen(port, () => console.log(`Listening at http://localhost:${port}`));
|
||||
|
||||
15
build.js
15
build.js
@ -1,15 +0,0 @@
|
||||
// build.js
|
||||
const esbuild = require('esbuild');
|
||||
const fs = require('fs');
|
||||
|
||||
// Ensure dist folder exists
|
||||
if (!fs.existsSync('dist')) fs.mkdirSync('dist', { recursive: true });
|
||||
|
||||
esbuild.build({
|
||||
entryPoints: ['server.js'],
|
||||
outfile: 'dist/server.js',
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
target: 'node18',
|
||||
minify: false,
|
||||
}).catch(() => process.exit(1));
|
||||
4
docker-compose.dev.yml
Executable file → Normal file
4
docker-compose.dev.yml
Executable file → Normal file
@ -16,7 +16,9 @@ services:
|
||||
|
||||
|
||||
backend:
|
||||
build: ./backend
|
||||
build:
|
||||
context: ./backend
|
||||
command: npm run dev
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- /app/node_modules
|
||||
|
||||
0
docker-compose.prod.yml
Executable file → Normal file
0
docker-compose.prod.yml
Executable file → Normal file
Loading…
Reference in New Issue
Block a user