diff --git a/backend/controllers/auth.controller.js b/backend/controllers/auth.controller.js index 74e243f..222acd8 100644 --- a/backend/controllers/auth.controller.js +++ b/backend/controllers/auth.controller.js @@ -3,11 +3,15 @@ const jwt = require("jsonwebtoken"); const User = require("../models/user.model"); exports.register = async (req, res) => { - const { email, password, role } = req.body; + let { username, password, name } = req.body; + username = username.toLowerCase(); + console.log(`🆕 Registration attempt for ${name} => username:${username}, password:${password}`); try { const hash = await bcrypt.hash(password, 10); - const user = await User.createUser(email, hash, role); + const user = await User.createUser(username, hash, name); + console.log(`✅ User registered: ${username}`); + res.json({ message: "User registered", user }); } catch (err) { res.status(400).json({ message: "Registration failed", error: err }); diff --git a/backend/controllers/users.controller.js b/backend/controllers/users.controller.js index ba6a748..8af2598 100644 --- a/backend/controllers/users.controller.js +++ b/backend/controllers/users.controller.js @@ -1,8 +1,13 @@ const User = require("../models/user.model"); +exports.test = async (req, res) => { + console.log("User route is working"); + res.json({ message: "User route is working" }); +}; + exports.getAllUsers = async (req, res) => { + console.log(req); const users = await User.getAllUsers(); - console.log(users); res.json(users); }; @@ -39,3 +44,12 @@ exports.deleteUser = async (req, res) => { res.status(500).json({ error: "Failed to delete user" }); } }; + + + + +exports.checkIfUserExists = async (req, res) => { + const { username } = req.query; + const users = await User.checkIfUserExists(username); + res.json(users); +}; diff --git a/backend/models/user.model.js b/backend/models/user.model.js index 418d34b..298513b 100644 --- a/backend/models/user.model.js +++ b/backend/models/user.model.js @@ -1,5 +1,11 @@ const pool = require("../db/pool"); +exports.ROLES = { + VIEWER: "viewer", + EDITOR: "editor", + ADMIN: "admin", +} + exports.findByUsername = async (username) => { query = `SELECT * FROM users WHERE username = ${username}`; const result = await pool.query("SELECT * FROM users WHERE username = $1", [username]); @@ -7,12 +13,11 @@ exports.findByUsername = async (username) => { return result.rows[0]; }; -exports.createUser = async (username, hashedPassword, name, role = "viewer") => { +exports.createUser = async (username, hashedPassword, name) => { 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] + VALUES ($1, $2, $3, $4)`, + [username, hashedPassword, name, this.ROLES.EDITOR] ); return result.rows[0]; }; @@ -45,8 +50,12 @@ exports.deleteUser = async (id) => { }; -exports.ROLES = { - VIEWER: "viewer", - EDITOR: "editor", - ADMIN: "admin", -} \ No newline at end of file +exports.checkIfUserExists = async (username) => { + const result = await pool.query( + "SELECT COUNT(*) FROM users WHERE username = $1", + [username] + ); + return result.rows[0].count > 0; +} + + diff --git a/backend/routes/users.routes.js b/backend/routes/users.routes.js index 1cb721b..e89edbf 100644 --- a/backend/routes/users.routes.js +++ b/backend/routes/users.routes.js @@ -4,6 +4,7 @@ const requireRole = require("../middleware/rbac"); const usersController = require("../controllers/users.controller"); const { ROLES } = require("../models/user.model"); -router.get("/", auth, requireRole(ROLES.ADMIN), usersController.getAllUsers); +router.get("/exists", usersController.checkIfUserExists); +router.get("/test", usersController.test); module.exports = router; diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 82306bf..3f5839e 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -5,6 +5,7 @@ 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 Register from "./pages/Register.jsx"; import AppLayout from "./components/AppLayout.jsx"; import PrivateRoute from "./utils/PrivateRoute.jsx"; @@ -20,6 +21,7 @@ function App() { {/* Public route */} } /> + } /> {/* Private routes with layout */} { return res.data; }; -export const registerRequest = async (data) => { - const res = await api.post("/auth/register", data); +export const registerRequest = async (username, password, name) => { + const res = await api.post("/auth/register", { username, password, name }); return res.data; }; \ No newline at end of file diff --git a/frontend/src/api/users.js b/frontend/src/api/users.js index 4ea7b36..89c8d58 100644 --- a/frontend/src/api/users.js +++ b/frontend/src/api/users.js @@ -2,4 +2,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 +export const deleteUser = (id) => api.delete(`/admin/users/${id}`); +export const checkIfUserExists = (username) => api.get(`/users/exists`, { params: { username: username } }); \ No newline at end of file diff --git a/frontend/src/pages/AdminPanel.jsx b/frontend/src/pages/AdminPanel.jsx index 97024ca..31ef718 100644 --- a/frontend/src/pages/AdminPanel.jsx +++ b/frontend/src/pages/AdminPanel.jsx @@ -5,11 +5,14 @@ import { ROLES } from "../constants/roles"; export default function AdminPanel() { const [users, setUsers] = useState([]); + async function loadUsers() { + const allUsers = await getAllUsers(); + console.log(allUsers); + setUsers(allUsers.data); + } + useEffect(() => { - async function load() { - const allUsers = await getAllUsers(); - setUsers(allUsers); - } + loadUsers(); }, []); const changeRole = async (id, role) => { @@ -20,7 +23,6 @@ export default function AdminPanel() { return (

Admin Panel

- {users.map((user) => (
{user.username} - {user.role} diff --git a/frontend/src/pages/GroceryList.jsx b/frontend/src/pages/GroceryList.jsx index d1988ea..1c00749 100644 --- a/frontend/src/pages/GroceryList.jsx +++ b/frontend/src/pages/GroceryList.jsx @@ -109,7 +109,7 @@ export default function GroceryList() { {/* Add Item form (editor/admin only) */} - {[ROLES.ADMIN, ROLES.VIEWER].includes(role) && showAddForm && ( + {[ROLES.ADMIN, ROLES.EDITOR].includes(role) && showAddForm && ( <> - (role === "editor" || role === "admin") && handleBought(item.id) + [ROLES.ADMIN, ROLES.EDITOR].includes(role) && handleBought(item.id) } > {item.item_name} ({item.quantity}) @@ -167,7 +167,7 @@ export default function GroceryList() {
{/* Floating Button (editor/admin only) */} - {[ROLES.ADMIN, ROLES.VIEWER].includes(role) && ( + {[ROLES.ADMIN, ROLES.EDITOR].includes(role) && ( - + {error &&

{error}

} + +
+ setUsername(e.target.value)} + /> + + setPassword(e.target.value)} + /> + + +
+ +

+ Need an account? Register here +

+
- ) -} \ No newline at end of file + ); +} diff --git a/frontend/src/pages/Register.jsx b/frontend/src/pages/Register.jsx index e69de29..3d46424 100644 --- a/frontend/src/pages/Register.jsx +++ b/frontend/src/pages/Register.jsx @@ -0,0 +1,110 @@ +import { useContext, useEffect, useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { loginRequest, registerRequest } from "../api/auth"; +import { checkIfUserExists } from "../api/users"; +import { AuthContext } from "../context/AuthContext"; + +import "../styles/Register.css"; + +export default function Register() { + const navigate = useNavigate(); + const { login } = useContext(AuthContext); + + const [name, setName] = useState(""); + const [username, setUsername] = useState(""); + const [userExists, setUserExists] = useState(false); + const [password, setPassword] = useState(""); + const [passwordMatches, setPasswordMatches] = useState(true); + const [confirm, setConfirm] = useState(""); + const [error, setError] = useState(""); + const [success, setSuccess] = useState(""); + + + useEffect(() => { checkIfUserExistsHandler(); }, [username]); + async function checkIfUserExistsHandler() { + setUserExists((await checkIfUserExists(username)).data); + } + + useEffect(() => { setError(userExists ? `Username '${username}' already taken` : ""); }, [userExists]); + + useEffect(() => { + setPasswordMatches( + !password || + !confirm || + password === confirm + ); + }, [password, confirm]); + + useEffect(() => { setError(passwordMatches ? "" : "Passwords are not matching"); }, [passwordMatches]); + + + + const submit = async (e) => { + e.preventDefault(); + setError(""); + setSuccess(""); + + try { + await registerRequest(username, password, name); + console.log("Registered user:", username); + const data = await loginRequest(username, password); + console.log(data); + login(data); + setSuccess("Account created! Redirecting the grocery list..."); + setTimeout(() => navigate("/"), 2000); + } catch (err) { + setError(err.response?.data?.message || "Registration failed"); + setTimeout(() => { + setError(""); + }, 1000); + } + }; + + + return ( +
+

Register

+ + {

{error}

} + {success &&

{success}

} + +
+ setName(e.target.value)} + required + /> + + setUsername(e.target.value)} + required + /> + + setPassword(e.target.value)} + required + /> + + setConfirm(e.target.value)} + required + /> + + +
+ +

+ Already have an account? Login here +

+
+ ); +} \ No newline at end of file diff --git a/frontend/src/styles/Login.css b/frontend/src/styles/Login.css new file mode 100644 index 0000000..034b067 --- /dev/null +++ b/frontend/src/styles/Login.css @@ -0,0 +1,70 @@ +.login-wrapper { + font-family: Arial, sans-serif; + padding: 1em; + background: #f8f9fa; + min-height: 100vh; + + display: flex; + justify-content: center; + align-items: center; +} + +.login-box { + width: 100%; + max-width: 360px; + background: white; + padding: 1.5em; + border-radius: 8px; + box-shadow: 0 0 10px rgba(0,0,0,0.12); +} + +.login-title { + text-align: center; + font-size: 1.6em; + margin-bottom: 1em; +} + +.login-input { + width: 100%; + padding: 0.6em; + margin: 0.4em 0; + font-size: 1em; + border-radius: 4px; + border: 1px solid #ccc; +} + +.login-button { + width: 100%; + padding: 0.7em; + margin-top: 0.6em; + background: #007bff; + border: none; + color: white; + border-radius: 4px; + cursor: pointer; + font-size: 1em; +} + +.login-button:hover { + background: #0068d1; +} + +.login-error { + color: red; + text-align: center; + margin-bottom: 0.6em; +} + +.login-register { + text-align: center; + margin-top: 1em; +} + +.login-register a { + color: #007bff; + text-decoration: none; +} + +.login-register a:hover { + text-decoration: underline; +} diff --git a/frontend/src/styles/Register.css b/frontend/src/styles/Register.css new file mode 100644 index 0000000..10a57d0 --- /dev/null +++ b/frontend/src/styles/Register.css @@ -0,0 +1,84 @@ +.register-container { + max-width: 400px; + margin: 50px auto; + padding: 2rem; + border-radius: 12px; + background: #ffffff; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + font-family: Arial, sans-serif; +} + +.register-container h1 { + text-align: center; + margin-bottom: 1.5rem; + font-size: 1.8rem; + font-weight: bold; +} + +.register-form { + display: flex; + flex-direction: column; + gap: 12px; +} + +.register-form input { + padding: 12px 14px; + border: 1px solid #ccc; + border-radius: 8px; + font-size: 1rem; + outline: none; + transition: border-color 0.2s ease; +} + +.register-form input:focus { + border-color: #0077ff; +} + +.register-form button { + padding: 12px; + border: none; + background: #0077ff; + color: white; + font-size: 1rem; + border-radius: 8px; + cursor: pointer; + margin-top: 10px; + transition: background 0.2s ease; +} + +.register-form button:hover:not(:disabled) { + background: #005fcc; +} + +.register-form button:disabled { + background: #a8a8a8; + cursor: not-allowed; +} + +.error-message { + height: 15px; + color: red; + text-align: center; + margin-bottom: 10px; +} + +.success-message { + color: green; + text-align: center; + margin-bottom: 10px; +} + +.register-link { + text-align: center; + margin-top: 1rem; +} + +.register-link a { + color: #0077ff; + text-decoration: none; + font-weight: bold; +} + +.register-link a:hover { + text-decoration: underline; +}