wip
This commit is contained in:
parent
4d8c3cb2e4
commit
ffbaa2a8e0
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -40,7 +40,7 @@
|
|||||||
// Prettier
|
// Prettier
|
||||||
// ============================
|
// ============================
|
||||||
"[javascript]": { "editor.defaultFormatter": "vscode.typescript-language-features" },
|
"[javascript]": { "editor.defaultFormatter": "vscode.typescript-language-features" },
|
||||||
"[javascriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
|
"[javascriptreact]": { "editor.defaultFormatter": "vscode.typescript-language-features" },
|
||||||
"[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
|
"[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
|
||||||
"[typescriptreact]": { "editor.defaultFormatter": "vscode.typescript-language-features" },
|
"[typescriptreact]": { "editor.defaultFormatter": "vscode.typescript-language-features" },
|
||||||
|
|
||||||
|
|||||||
@ -3,8 +3,6 @@ const express = require("express");
|
|||||||
const cors = require("cors");
|
const cors = require("cors");
|
||||||
const User = require("./models/user.model");
|
const User = require("./models/user.model");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
|
|||||||
@ -15,9 +15,10 @@ exports.register = async (req, res) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
exports.login = async (req, res) => {
|
exports.login = async (req, res) => {
|
||||||
const { email, password } = req.body;
|
const { username, password } = req.body;
|
||||||
|
console.log(`Login attempt for user: ${username} with password: ${password}`);
|
||||||
|
|
||||||
const user = await User.findByEmail(email);
|
const user = await User.findByUsername(username);
|
||||||
if (!user) return res.status(401).json({ message: "Invalid credentials" });
|
if (!user) return res.status(401).json({ message: "Invalid credentials" });
|
||||||
|
|
||||||
const valid = await bcrypt.compare(password, user.password);
|
const valid = await bcrypt.compare(password, user.password);
|
||||||
|
|||||||
@ -2,6 +2,7 @@ const User = require("../models/user.model");
|
|||||||
|
|
||||||
exports.getAllUsers = async (req, res) => {
|
exports.getAllUsers = async (req, res) => {
|
||||||
const users = await User.getAllUsers();
|
const users = await User.getAllUsers();
|
||||||
|
console.log(users);
|
||||||
res.json(users);
|
res.json(users);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
const pool = require("../db/pool");
|
const pool = require("../db/pool");
|
||||||
|
|
||||||
exports.findByUsername = async (username) => {
|
exports.findByUsername = async (username) => {
|
||||||
|
query = `SELECT * FROM users WHERE username = ${username}`;
|
||||||
const result = await pool.query("SELECT * FROM users WHERE username = $1", [username]);
|
const result = await pool.query("SELECT * FROM users WHERE username = $1", [username]);
|
||||||
|
console.log(query);
|
||||||
return result.rows[0];
|
return result.rows[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -24,10 +26,13 @@ exports.getAllUsers = async () => {
|
|||||||
|
|
||||||
exports.updateUserRole = async (id, role) => {
|
exports.updateUserRole = async (id, role) => {
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
`UPDATE users SET role = $1 WHERE id = $2 RETURNING id`,
|
`UPDATE users
|
||||||
|
SET role = $1
|
||||||
|
WHERE id = $2
|
||||||
|
RETURNING id, username, name, role`,
|
||||||
[role, id]
|
[role, id]
|
||||||
);
|
);
|
||||||
return result.rowCount > 0;
|
return result.rows[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -36,7 +41,7 @@ exports.deleteUser = async (id) => {
|
|||||||
`DELETE FROM users WHERE id = $1 RETURNING id`,
|
`DELETE FROM users WHERE id = $1 RETURNING id`,
|
||||||
[id]
|
[id]
|
||||||
);
|
);
|
||||||
return result.rowCount > 0;
|
return result.rowCount;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -3,5 +3,11 @@ const controller = require("../controllers/auth.controller");
|
|||||||
|
|
||||||
router.post("/register", controller.register);
|
router.post("/register", controller.register);
|
||||||
router.post("/login", controller.login);
|
router.post("/login", controller.login);
|
||||||
|
router.post("/", async (req, res) => {
|
||||||
|
resText = `Grocery List API is running.\n` +
|
||||||
|
`Roles available: ${Object.values(User.ROLES).join(', ')}`
|
||||||
|
|
||||||
|
res.status(200).type("text/plain").send(resText);
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -3,7 +3,7 @@ const controller = require("../controllers/lists.controller");
|
|||||||
const auth = require("../middleware/auth");
|
const auth = require("../middleware/auth");
|
||||||
const requireRole = require("../middleware/rbac");
|
const requireRole = require("../middleware/rbac");
|
||||||
const { ROLES } = require("../models/user.model");
|
const { ROLES } = require("../models/user.model");
|
||||||
const User = require("./models/user.model");
|
const User = require("../models/user.model");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,29 +1,29 @@
|
|||||||
services:
|
services:
|
||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
context: ./frontend
|
context: ./frontend
|
||||||
dockerfile: Dockerfile.dev
|
dockerfile: Dockerfile.dev
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=development
|
- NODE_ENV=development
|
||||||
volumes:
|
volumes:
|
||||||
- ./frontend:/app
|
- ./frontend:/app
|
||||||
- /app/node_modules
|
- /app/node_modules
|
||||||
ports:
|
ports:
|
||||||
- "3000:5173"
|
- "3000:5173"
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
build:
|
build:
|
||||||
context: ./backend
|
context: ./backend
|
||||||
command: npm run dev
|
command: npm run dev
|
||||||
volumes:
|
volumes:
|
||||||
- ./backend:/app
|
- ./backend:/app
|
||||||
- /app/node_modules
|
- /app/node_modules
|
||||||
ports:
|
ports:
|
||||||
- "5000:5000"
|
- "5000:5000"
|
||||||
env_file:
|
env_file:
|
||||||
- ./backend/.env
|
- ./backend/.env
|
||||||
restart: always
|
restart: always
|
||||||
40
frontend/src/App.jsx
Normal file
40
frontend/src/App.jsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||||
|
import { ROLES } from "./constants/roles";
|
||||||
|
import { AuthProvider } from "./context/AuthContext.jsx";
|
||||||
|
|
||||||
|
import AdminPanel from "./pages/AdminPanel.jsx";
|
||||||
|
import GroceryList from "./pages/GroceryList.jsx";
|
||||||
|
import Login from "./pages/Login.jsx";
|
||||||
|
|
||||||
|
import PrivateRoute from "./utils/PrivateRoute.jsx";
|
||||||
|
import RoleGuard from "./utils/RoleGuard.jsx";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<AuthProvider>
|
||||||
|
<BrowserRouter>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route
|
||||||
|
path="/"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<GroceryList />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/admin"
|
||||||
|
element={
|
||||||
|
<RoleGuard allowed={[ROLES.ADMIN]}>
|
||||||
|
<AdminPanel />
|
||||||
|
</RoleGuard>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
</AuthProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
@ -1,155 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import GroceryItem from "./components/GroceryItem";
|
|
||||||
import SuggestionList from "./components/SuggestionList";
|
|
||||||
import type { GroceryItemType } from "./types";
|
|
||||||
|
|
||||||
|
|
||||||
const API_BASE_URL = import.meta.env.VITE_API_URL;
|
|
||||||
|
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
const [items, setItems] = useState<GroceryItemType[]>([]);
|
|
||||||
const [suggestions, setSuggestions] = useState<string[]>([]);
|
|
||||||
const [itemInput, setItemInput] = useState<string>("");
|
|
||||||
const [quantity, setQuantity] = useState<number>(1);
|
|
||||||
|
|
||||||
|
|
||||||
// Load grocery list
|
|
||||||
const loadList = async () => {
|
|
||||||
const res = await fetch(`${API_BASE_URL}/list`);
|
|
||||||
const data: GroceryItemType[] = await res.json();
|
|
||||||
setItems(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadList();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
|
||||||
// Suggestion logic
|
|
||||||
const handleSuggest = async (query: string) => {
|
|
||||||
setItemInput(query);
|
|
||||||
|
|
||||||
if (!query.trim()) return setSuggestions([]);
|
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE_URL}/suggest?query=${encodeURIComponent(query)}`);
|
|
||||||
const data: string[] = await res.json();
|
|
||||||
setSuggestions(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// Add Item
|
|
||||||
const addItem = async () => {
|
|
||||||
if (!itemInput.trim() || !quantity) return;
|
|
||||||
|
|
||||||
const existing = items.find((i) => i.item_name.toLowerCase() === itemInput.toLowerCase());
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
if (!confirm("Item already exists. Add more?")) return;
|
|
||||||
|
|
||||||
await fetch(`${API_BASE_URL}/add`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
item_name: itemInput,
|
|
||||||
quantity: existing.quantity + quantity,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await fetch(`${API_BASE_URL}/add`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ item_name: itemInput, quantity }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setItemInput("");
|
|
||||||
setQuantity(1);
|
|
||||||
setSuggestions([]);
|
|
||||||
loadList();
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// Mark bought
|
|
||||||
const markBought = async (id: number) => {
|
|
||||||
if (!confirm("Mark this item as bought?")) return;
|
|
||||||
|
|
||||||
await fetch(`${API_BASE_URL}/mark-bought`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ id }),
|
|
||||||
});
|
|
||||||
|
|
||||||
loadList();
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="container"
|
|
||||||
style={{
|
|
||||||
maxWidth: "480px",
|
|
||||||
margin: "auto",
|
|
||||||
background: "white",
|
|
||||||
padding: "1em",
|
|
||||||
borderRadius: "8px",
|
|
||||||
boxShadow: "0 0 10px rgba(0,0,0,0.1)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h1 style={{ textAlign: "center", fontSize: "1.5em" }}>
|
|
||||||
Costco Grocery List
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={itemInput}
|
|
||||||
placeholder="Item name"
|
|
||||||
onChange={(e) => handleSuggest(e.target.value)}
|
|
||||||
style={inputStyle}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SuggestionList
|
|
||||||
suggestions={suggestions}
|
|
||||||
onSelect={(val: string) => {
|
|
||||||
setItemInput(val);
|
|
||||||
setSuggestions([]);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={quantity}
|
|
||||||
min={1}
|
|
||||||
onChange={(e) => setQuantity(Number(e.target.value))}
|
|
||||||
placeholder="Quantity"
|
|
||||||
style={inputStyle}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button onClick={addItem} style={buttonStyle}>
|
|
||||||
Add Item
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<ul style={{ listStyle: "none", padding: 0 }}>
|
|
||||||
{items.map((item) => (
|
|
||||||
<GroceryItem key={item.id} item={item} onClick={markBought} />
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputStyle: React.CSSProperties = {
|
|
||||||
fontSize: "1em",
|
|
||||||
margin: "0.3em 0",
|
|
||||||
padding: "0.5em",
|
|
||||||
width: "100%",
|
|
||||||
boxSizing: "border-box",
|
|
||||||
};
|
|
||||||
|
|
||||||
const buttonStyle = {
|
|
||||||
...inputStyle,
|
|
||||||
cursor: "pointer",
|
|
||||||
};
|
|
||||||
@ -1,7 +1,8 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { API_BASE_URL } from "../config";
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: process.env.VITE_API_URL || "http://localhost:5000",
|
baseURL: API_BASE_URL,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import api from "./axios";
|
import api from "./axios";
|
||||||
|
|
||||||
export const getList = () => api.get("/list");
|
export const getList = () => api.get("/list");
|
||||||
export cosnt addItem = (itemName, quantiy) =>
|
export const addItem = (itemName, quantiy) => api.post("/list/add", { itemName, quantiy });
|
||||||
api.post("/list/add", { itemName, quantiy });
|
|
||||||
export const markBought = (id) => api.post("/list/mark-bought", { id });
|
export const markBought = (id) => api.post("/list/mark-bought", { id });
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { useContext } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { ROLES } from "../constants/roles";
|
||||||
|
import { AuthContext } from "../context/AuthContext";
|
||||||
|
|
||||||
|
export default function Navbar() {
|
||||||
|
const { role, logout } = useContext(AuthContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav>
|
||||||
|
<Link to="/">List</Link>
|
||||||
|
{role === ROLES.ADMIN && <Link to="/admin">Admin Panel</Link>}
|
||||||
|
<button onClick={logout}>Logout</button>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1 +1 @@
|
|||||||
export const API_BASE_URL = import.meta.env.VITE_API_URL || "/api";
|
export const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:5000";
|
||||||
5
frontend/src/constants/roles.js
Normal file
5
frontend/src/constants/roles.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export const ROLES = {
|
||||||
|
VIEWER: "viewer",
|
||||||
|
EDITOR: "editor",
|
||||||
|
ADMIN: "admin",
|
||||||
|
};
|
||||||
@ -1,7 +1,12 @@
|
|||||||
import { createContext, useState } from 'react';
|
import { createContext, useState } from 'react';
|
||||||
import { ROLES } from '../../../backend/models/user.model';
|
|
||||||
|
|
||||||
export const authContext = createContext();
|
export const AuthContext = createContext({
|
||||||
|
token: null,
|
||||||
|
role: null,
|
||||||
|
username: null,
|
||||||
|
login: () => { },
|
||||||
|
logout: () => { },
|
||||||
|
});
|
||||||
|
|
||||||
export const AuthProvider = ({ children }) => {
|
export const AuthProvider = ({ children }) => {
|
||||||
const [token, setToken] = useState(localStorage.getItem('token') || null);
|
const [token, setToken] = useState(localStorage.getItem('token') || null);
|
||||||
@ -19,14 +24,23 @@ export const AuthProvider = ({ children }) => {
|
|||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
|
|
||||||
setToken(null);
|
setToken(null);
|
||||||
setRole(null);
|
setRole(null);
|
||||||
setUsername(null);
|
setUsername(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
token,
|
||||||
|
role,
|
||||||
|
username,
|
||||||
|
login,
|
||||||
|
logout
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<authContext.Provider value={{ token, role, username, login, logout, ROLES }}>
|
<AuthContext.Provider value={value}>
|
||||||
{children}
|
{children}
|
||||||
</authContext.Provider>
|
</AuthContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { StrictMode } from 'react'
|
import { StrictMode } from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import App from './App.jsx'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App.tsx'
|
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
|||||||
@ -0,0 +1,37 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { getAllUsers, updateRole } from "../api/users";
|
||||||
|
import { ROLES } from "../constants/roles";
|
||||||
|
|
||||||
|
export default function AdminPanel() {
|
||||||
|
const [users, setUsers] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function load() {
|
||||||
|
const allUsers = await getAllUsers();
|
||||||
|
setUsers(allUsers);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const changeRole = async (id, role) => {
|
||||||
|
const updated = await updateRole(id, role);
|
||||||
|
setUsers(users.map(u => (u.id === id ? updated.data : u)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Admin Panel</h1>
|
||||||
|
|
||||||
|
{users.map((user) => (
|
||||||
|
<div key={user.id}>
|
||||||
|
<strong>{user.username}</strong> - {user.role}
|
||||||
|
<select onChange={(e) => changeRole(user.id, e.target.value)} value={user.role}>
|
||||||
|
<option value={ROLES.VIEWER}>Viewer</option>
|
||||||
|
<option value={ROLES.EDITOR}>Editor</option>
|
||||||
|
<option value={ROLES.ADMIN}>Admin</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div >
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,103 @@
|
|||||||
|
import { useContext, useEffect, useState } from "react";
|
||||||
|
import { addItem, getList, markBought } from "../api/list";
|
||||||
|
import { AuthContext } from "../context/AuthContext";
|
||||||
|
|
||||||
|
export default function GroceryList() {
|
||||||
|
const { role, username } = useContext(AuthContext);
|
||||||
|
|
||||||
|
const [items, setItems] = useState([]);
|
||||||
|
const [itemName, setItemName] = useState("");
|
||||||
|
const [quantity, setQuantity] = useState(1);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
// Load list
|
||||||
|
const loadItems = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await getList();
|
||||||
|
setItems(response.data);
|
||||||
|
setLoading(false);
|
||||||
|
} catch (err) {
|
||||||
|
setError("Failed to load grocery list");
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadItems();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Add item (editor/admin)
|
||||||
|
const handleAdd = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
await addItem(itemName, quantity);
|
||||||
|
setItemName("");
|
||||||
|
setQuantity(1);
|
||||||
|
loadItems();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
setError("Failed to add item");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mark bought (editor/admin)
|
||||||
|
const handleBought = async (id) => {
|
||||||
|
try {
|
||||||
|
await markBought(id);
|
||||||
|
loadItems();
|
||||||
|
} catch (err) {
|
||||||
|
setError("Failed to mark item as bought");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) return <p>Loading...</p>;
|
||||||
|
if (error) return <p style={{ color: "red" }}>{error}</p>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: "20px" }}>
|
||||||
|
<h1>Grocery List</h1>
|
||||||
|
<p>Logged in as: <strong>{username}</strong> ({role})</p>
|
||||||
|
|
||||||
|
{/* Add Item Section (editor/admin only) */}
|
||||||
|
{(role === "editor" || role === "admin") && (
|
||||||
|
<form onSubmit={handleAdd} style={{ margin: "20px 0" }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Item name"
|
||||||
|
value={itemName}
|
||||||
|
onChange={(e) => setItemName(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={quantity}
|
||||||
|
onChange={(e) => setQuantity(e.target.value)}
|
||||||
|
style={{ width: "70px", marginLeft: "10px" }}
|
||||||
|
/>
|
||||||
|
<button style={{ marginLeft: "10px" }}>Add</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Grocery List */}
|
||||||
|
<ul>
|
||||||
|
{items.map((item) => (
|
||||||
|
<li key={item.id} style={{ margin: "10px 0" }}>
|
||||||
|
{item.item_name} — {item.quantity}
|
||||||
|
|
||||||
|
{(role === "editor" || role === "admin") && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleBought(item.id)}
|
||||||
|
style={{ marginLeft: "15px" }}
|
||||||
|
>
|
||||||
|
Mark Bought
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
import { useContext, useState } from "react";
|
||||||
|
import { loginRequest } from "../api/auth";
|
||||||
|
import { AuthContext } from "../context/AuthContext";
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
const { login } = useContext(AuthContext);
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
const submit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError("");
|
||||||
|
try {
|
||||||
|
const data = await loginRequest(username, password);
|
||||||
|
login(data);
|
||||||
|
window.location.href = "/";
|
||||||
|
} catch (err) {
|
||||||
|
setError("Invalid username or password");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Login</h1>
|
||||||
|
{error && <p style={{ color: "red" }}>{error}</p>}
|
||||||
|
|
||||||
|
<form onSubmit={submit}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Username"
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import { useContext } from "react";
|
||||||
|
import { Navigate } from "react-router-dom";
|
||||||
|
import { AuthContext } from "../context/AuthContext";
|
||||||
|
|
||||||
|
export default function PrivateRoute({ children }) {
|
||||||
|
const { token } = useContext(AuthContext);
|
||||||
|
return token ? children : <Navigate to="/login" />;
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
import { useContext } from "react";
|
||||||
|
import { Navigate } from "react-router-dom";
|
||||||
|
import { AuthContext } from "../context/AuthContext";
|
||||||
|
|
||||||
|
export default function RoleGuard({ allowed, children }) {
|
||||||
|
const { role } = useContext(AuthContext);
|
||||||
|
|
||||||
|
if (!role) return <Navigate to="/login" />;
|
||||||
|
if (!allowed.includes(role)) return <Navigate to="/" />;
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function usageExample() {
|
||||||
|
<Route
|
||||||
|
path="/admin"
|
||||||
|
element={
|
||||||
|
<RoleGuard allowed={[ROLES.ADMIN]}>
|
||||||
|
<AdminPanel />
|
||||||
|
</RoleGuard>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user