From 4d8c3cb2e4348e970eae1b8e928dd31fd87d7e07 Mon Sep 17 00:00:00 2001 From: Nico Date: Fri, 21 Nov 2025 18:30:42 -0800 Subject: [PATCH] Add roles enum within backend Add more admin functions --- backend/app.js | 6 ++++- backend/controllers/lists.controller.js | 4 +-- backend/controllers/users.controller.js | 34 +++++++++++++++++++++++++ backend/models/user.model.js | 25 ++++++++++++++++++ backend/routes/admin.routes.js | 6 +++-- backend/routes/list.routes.js | 9 ++++--- backend/routes/users.routes.js | 3 ++- frontend/src/api/auth.js | 11 ++++++++ frontend/src/api/axios.js | 18 +++++++++++++ frontend/src/api/list.js | 6 +++++ frontend/src/api/users.js | 5 ++++ frontend/src/context/AuthContext.jsx | 32 +++++++++++++++++++++++ 12 files changed, 150 insertions(+), 9 deletions(-) diff --git a/backend/app.js b/backend/app.js index c2b02bc..79cb762 100644 --- a/backend/app.js +++ b/backend/app.js @@ -1,6 +1,7 @@ require("dotenv").config(); const express = require("express"); const cors = require("cors"); +const User = require("./models/user.model"); @@ -26,7 +27,10 @@ app.use( ); app.get('/', async (req, res) => { - res.status(200).send('Grocery List API is running.'); + resText = `Grocery List API is running.\n` + + `Roles available: ${Object.values(User.ROLES).join(', ')}` + + res.status(200).type("text/plain").send(resText); }); diff --git a/backend/controllers/lists.controller.js b/backend/controllers/lists.controller.js index 2dfe2c2..8c06638 100644 --- a/backend/controllers/lists.controller.js +++ b/backend/controllers/lists.controller.js @@ -8,9 +8,9 @@ exports.getList = async (req, res) => { exports.addItem = async (req, res) => { - const { item_name, quantity } = req.body; + const { itemName, quantity } = req.body; - const id = await List.addOrUpdateItem(item_name, quantity); + const id = await List.addOrUpdateItem(itemName, quantity); await List.addHistoryRecord(id, quantity); diff --git a/backend/controllers/users.controller.js b/backend/controllers/users.controller.js index 6f7f460..b951424 100644 --- a/backend/controllers/users.controller.js +++ b/backend/controllers/users.controller.js @@ -4,3 +4,37 @@ exports.getAllUsers = async (req, res) => { const users = await User.getAllUsers(); res.json(users); }; + + +exports.updateUserRole = async (req, res) => { + try { + const { id } = req.params; + const { role } = req.body; + + if (!Object.values(ROLES).includes(role)) + return res.status(400).json({ error: "Invalid role" }); + + const updated = await User.updateUserRole(id, role); + if (!updated) + return res.status(404).json({ error: "User not found" }); + + res.json({ message: "Role updated", id, role }); + } catch (err) { + res.status(500).json({ error: "Failed to update role" }); + } +}; + +exports.deleteUser = async (req, res) => { + try { + const { id } = req.params; + + const deleted = await User.deleteUser(id); + if (!deleted) + return res.status(404).json({ error: "User not found" }); + + + res.json({ message: "User deleted", id }); + } catch (err) { + res.status(500).json({ error: "Failed to delete user" }); + } +}; diff --git a/backend/models/user.model.js b/backend/models/user.model.js index f82aa1b..0267ce8 100644 --- a/backend/models/user.model.js +++ b/backend/models/user.model.js @@ -20,3 +20,28 @@ exports.getAllUsers = async () => { const result = await pool.query("SELECT id, username, name, role FROM users ORDER BY id ASC"); return result.rows; }; + + +exports.updateUserRole = async (id, role) => { + const result = await pool.query( + `UPDATE users SET role = $1 WHERE id = $2 RETURNING id`, + [role, id] + ); + return result.rowCount > 0; +}; + + +exports.deleteUser = async (id) => { + const result = await pool.query( + `DELETE FROM users WHERE id = $1 RETURNING id`, + [id] + ); + return result.rowCount > 0; +}; + + +exports.ROLES = { + VIEWER: "viewer", + EDITOR: "editor", + ADMIN: "admin", +} \ No newline at end of file diff --git a/backend/routes/admin.routes.js b/backend/routes/admin.routes.js index 83a9e7b..5a15e05 100644 --- a/backend/routes/admin.routes.js +++ b/backend/routes/admin.routes.js @@ -2,8 +2,10 @@ const router = require("express").Router(); const auth = require("../middleware/auth"); const requireRole = require("../middleware/rbac"); const usersController = require("../controllers/users.controller"); +const { ROLES } = require("../models/user.model"); -// Admin-only route -router.get("/users", auth, requireRole("admin"), usersController.getAllUsers); +router.get("/users", auth, requireRole(ROLES.ADMIN), usersController.getAllUsers); +router.put("/users", auth, requireRole(ROLES.ADMIN), usersController.updateUserRole); +router.delete("/users", auth, requireRole(ROLES.ADMIN), usersController.deleteUser); module.exports = router; diff --git a/backend/routes/list.routes.js b/backend/routes/list.routes.js index 74cfd65..e48a713 100644 --- a/backend/routes/list.routes.js +++ b/backend/routes/list.routes.js @@ -2,13 +2,16 @@ const router = require("express").Router(); const controller = require("../controllers/lists.controller"); const auth = require("../middleware/auth"); const requireRole = require("../middleware/rbac"); +const { ROLES } = require("../models/user.model"); +const User = require("./models/user.model"); -router.get("/", auth, requireRole("viewer", "editor", "admin"), controller.getList); + +router.get("/", auth, requireRole(ROLES.VIEWER, ROLES.EDITOR, ROLES.ADMIN), controller.getList); -router.post("/add", auth, requireRole("editor", "admin"), controller.addItem); -router.post("/mark-bought", auth, requireRole("editor", "admin"), controller.markBought); +router.post("/add", auth, requireRole(ROLES.EDITOR, ROLES.ADMIN), controller.addItem); +router.post("/mark-bought", auth, requireRole(ROLES.EDITOR, ROLES.ADMIN), controller.markBought); module.exports = router; diff --git a/backend/routes/users.routes.js b/backend/routes/users.routes.js index eea991a..1cb721b 100644 --- a/backend/routes/users.routes.js +++ b/backend/routes/users.routes.js @@ -2,7 +2,8 @@ const router = require("express").Router(); const auth = require("../middleware/auth"); const requireRole = require("../middleware/rbac"); const usersController = require("../controllers/users.controller"); +const { ROLES } = require("../models/user.model"); -router.get("/", auth, requireRole("admin"), usersController.getAllUsers); +router.get("/", auth, requireRole(ROLES.ADMIN), usersController.getAllUsers); module.exports = router; diff --git a/frontend/src/api/auth.js b/frontend/src/api/auth.js index e69de29..ad0ca3a 100644 --- a/frontend/src/api/auth.js +++ b/frontend/src/api/auth.js @@ -0,0 +1,11 @@ +import api from "./axios"; + +export const loginRequest = async (username, password) => { + const res = await api.post("/auth/login", { username, password }); + return res.data; +}; + +export const registerRequest = async (data) => { + const res = await api.post("/auth/register", data); + return res.data; +}; \ No newline at end of file diff --git a/frontend/src/api/axios.js b/frontend/src/api/axios.js index e69de29..7153c4e 100644 --- a/frontend/src/api/axios.js +++ b/frontend/src/api/axios.js @@ -0,0 +1,18 @@ +import axios from "axios"; + +const api = axios.create({ + baseURL: process.env.VITE_API_URL || "http://localhost:5000", + headers: { + "Content-Type": "application/json", + }, +}); + +api.interceptors.request.use((config => { + const token = localStorage.getItem("token"); + if (token) { + config.headers["Authorization"] = `Bearer ${token}`; + } + return config; +})); + +export default api; \ No newline at end of file diff --git a/frontend/src/api/list.js b/frontend/src/api/list.js index e69de29..0997c2a 100644 --- a/frontend/src/api/list.js +++ b/frontend/src/api/list.js @@ -0,0 +1,6 @@ +import api from "./axios"; + +export const getList = () => api.get("/list"); +export cosnt addItem = (itemName, quantiy) => + api.post("/list/add", { itemName, quantiy }); +export const markBought = (id) => api.post("/list/mark-bought", { id }); \ No newline at end of file diff --git a/frontend/src/api/users.js b/frontend/src/api/users.js index e69de29..4ea7b36 100644 --- a/frontend/src/api/users.js +++ b/frontend/src/api/users.js @@ -0,0 +1,5 @@ +import api from "./axios"; + +export const getAllUsers = () => api.get("/admin/users"); +export const updateRole = (id, role) => api.put(`/admin/users/${id}/role`, { role }); +export const deleteUser = (id) => api.delete(`/admin/users/${id}`); \ No newline at end of file diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx index e69de29..f428041 100644 --- a/frontend/src/context/AuthContext.jsx +++ b/frontend/src/context/AuthContext.jsx @@ -0,0 +1,32 @@ +import { createContext, useState } from 'react'; +import { ROLES } from '../../../backend/models/user.model'; + +export const authContext = createContext(); + +export const AuthProvider = ({ children }) => { + const [token, setToken] = useState(localStorage.getItem('token') || null); + const [role, setRole] = useState(localStorage.getItem('role') || null); + const [username, setUsername] = useState(localStorage.getItem('username') || null); + + const login = (data) => { + localStorage.setItem('token', data.token); + localStorage.setItem('role', data.role); + localStorage.setItem('username', data.username); + setToken(data.token); + setRole(data.role); + setUsername(data.username); + }; + + const logout = () => { + localStorage.clear(); + setToken(null); + setRole(null); + setUsername(null); + }; + + return ( + + {children} + + ); +}; \ No newline at end of file