Made everything pretty
Fix registration
This commit is contained in:
parent
963240ceb1
commit
d9c4a2caf9
@ -3,11 +3,15 @@ const jwt = require("jsonwebtoken");
|
|||||||
const User = require("../models/user.model");
|
const User = require("../models/user.model");
|
||||||
|
|
||||||
exports.register = async (req, res) => {
|
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 {
|
try {
|
||||||
const hash = await bcrypt.hash(password, 10);
|
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 });
|
res.json({ message: "User registered", user });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(400).json({ message: "Registration failed", error: err });
|
res.status(400).json({ message: "Registration failed", error: err });
|
||||||
|
|||||||
@ -1,8 +1,13 @@
|
|||||||
const User = require("../models/user.model");
|
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) => {
|
exports.getAllUsers = async (req, res) => {
|
||||||
|
console.log(req);
|
||||||
const users = await User.getAllUsers();
|
const users = await User.getAllUsers();
|
||||||
console.log(users);
|
|
||||||
res.json(users);
|
res.json(users);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -39,3 +44,12 @@ exports.deleteUser = async (req, res) => {
|
|||||||
res.status(500).json({ error: "Failed to delete user" });
|
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);
|
||||||
|
};
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
const pool = require("../db/pool");
|
const pool = require("../db/pool");
|
||||||
|
|
||||||
|
exports.ROLES = {
|
||||||
|
VIEWER: "viewer",
|
||||||
|
EDITOR: "editor",
|
||||||
|
ADMIN: "admin",
|
||||||
|
}
|
||||||
|
|
||||||
exports.findByUsername = async (username) => {
|
exports.findByUsername = async (username) => {
|
||||||
query = `SELECT * FROM users WHERE username = ${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]);
|
||||||
@ -7,12 +13,11 @@ exports.findByUsername = async (username) => {
|
|||||||
return result.rows[0];
|
return result.rows[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.createUser = async (username, hashedPassword, name, role = "viewer") => {
|
exports.createUser = async (username, hashedPassword, name) => {
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
`INSERT INTO users (username, password, name, role)
|
`INSERT INTO users (username, password, name, role)
|
||||||
VALUES ($1, $2, $3, $4)
|
VALUES ($1, $2, $3, $4)`,
|
||||||
RETURNING id, username, role`,
|
[username, hashedPassword, name, this.ROLES.EDITOR]
|
||||||
[username, hashedPassword, name, role]
|
|
||||||
);
|
);
|
||||||
return result.rows[0];
|
return result.rows[0];
|
||||||
};
|
};
|
||||||
@ -45,8 +50,12 @@ exports.deleteUser = async (id) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
exports.ROLES = {
|
exports.checkIfUserExists = async (username) => {
|
||||||
VIEWER: "viewer",
|
const result = await pool.query(
|
||||||
EDITOR: "editor",
|
"SELECT COUNT(*) FROM users WHERE username = $1",
|
||||||
ADMIN: "admin",
|
[username]
|
||||||
|
);
|
||||||
|
return result.rows[0].count > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ const requireRole = require("../middleware/rbac");
|
|||||||
const usersController = require("../controllers/users.controller");
|
const usersController = require("../controllers/users.controller");
|
||||||
const { ROLES } = require("../models/user.model");
|
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;
|
module.exports = router;
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { AuthProvider } from "./context/AuthContext.jsx";
|
|||||||
import AdminPanel from "./pages/AdminPanel.jsx";
|
import AdminPanel from "./pages/AdminPanel.jsx";
|
||||||
import GroceryList from "./pages/GroceryList.jsx";
|
import GroceryList from "./pages/GroceryList.jsx";
|
||||||
import Login from "./pages/Login.jsx";
|
import Login from "./pages/Login.jsx";
|
||||||
|
import Register from "./pages/Register.jsx";
|
||||||
|
|
||||||
import AppLayout from "./components/AppLayout.jsx";
|
import AppLayout from "./components/AppLayout.jsx";
|
||||||
import PrivateRoute from "./utils/PrivateRoute.jsx";
|
import PrivateRoute from "./utils/PrivateRoute.jsx";
|
||||||
@ -20,6 +21,7 @@ function App() {
|
|||||||
|
|
||||||
{/* Public route */}
|
{/* Public route */}
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route path="/register" element={<Register />} />
|
||||||
|
|
||||||
{/* Private routes with layout */}
|
{/* Private routes with layout */}
|
||||||
<Route
|
<Route
|
||||||
|
|||||||
@ -5,7 +5,7 @@ export const loginRequest = async (username, password) => {
|
|||||||
return res.data;
|
return res.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const registerRequest = async (data) => {
|
export const registerRequest = async (username, password, name) => {
|
||||||
const res = await api.post("/auth/register", data);
|
const res = await api.post("/auth/register", { username, password, name });
|
||||||
return res.data;
|
return res.data;
|
||||||
};
|
};
|
||||||
@ -3,3 +3,4 @@ import api from "./axios";
|
|||||||
export const getAllUsers = () => api.get("/admin/users");
|
export const getAllUsers = () => api.get("/admin/users");
|
||||||
export const updateRole = (id, role) => api.put(`/admin/users/${id}/role`, { role });
|
export const updateRole = (id, role) => api.put(`/admin/users/${id}/role`, { role });
|
||||||
export const deleteUser = (id) => api.delete(`/admin/users/${id}`);
|
export const deleteUser = (id) => api.delete(`/admin/users/${id}`);
|
||||||
|
export const checkIfUserExists = (username) => api.get(`/users/exists`, { params: { username: username } });
|
||||||
@ -5,11 +5,14 @@ import { ROLES } from "../constants/roles";
|
|||||||
export default function AdminPanel() {
|
export default function AdminPanel() {
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
|
|
||||||
|
async function loadUsers() {
|
||||||
|
const allUsers = await getAllUsers();
|
||||||
|
console.log(allUsers);
|
||||||
|
setUsers(allUsers.data);
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function load() {
|
loadUsers();
|
||||||
const allUsers = await getAllUsers();
|
|
||||||
setUsers(allUsers);
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const changeRole = async (id, role) => {
|
const changeRole = async (id, role) => {
|
||||||
@ -20,7 +23,6 @@ export default function AdminPanel() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>Admin Panel</h1>
|
<h1>Admin Panel</h1>
|
||||||
|
|
||||||
{users.map((user) => (
|
{users.map((user) => (
|
||||||
<div key={user.id}>
|
<div key={user.id}>
|
||||||
<strong>{user.username}</strong> - {user.role}
|
<strong>{user.username}</strong> - {user.role}
|
||||||
|
|||||||
@ -109,7 +109,7 @@ export default function GroceryList() {
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
{/* Add Item form (editor/admin only) */}
|
{/* Add Item form (editor/admin only) */}
|
||||||
{[ROLES.ADMIN, ROLES.VIEWER].includes(role) && showAddForm && (
|
{[ROLES.ADMIN, ROLES.EDITOR].includes(role) && showAddForm && (
|
||||||
<>
|
<>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -157,7 +157,7 @@ export default function GroceryList() {
|
|||||||
key={item.id}
|
key={item.id}
|
||||||
className="glist-li"
|
className="glist-li"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
(role === "editor" || role === "admin") && handleBought(item.id)
|
[ROLES.ADMIN, ROLES.EDITOR].includes(role) && handleBought(item.id)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{item.item_name} ({item.quantity})
|
{item.item_name} ({item.quantity})
|
||||||
@ -167,7 +167,7 @@ export default function GroceryList() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Floating Button (editor/admin only) */}
|
{/* Floating Button (editor/admin only) */}
|
||||||
{[ROLES.ADMIN, ROLES.VIEWER].includes(role) && (
|
{[ROLES.ADMIN, ROLES.EDITOR].includes(role) && (
|
||||||
<button
|
<button
|
||||||
className="glist-fab"
|
className="glist-fab"
|
||||||
onClick={() => setShowAddForm(!showAddForm)}
|
onClick={() => setShowAddForm(!showAddForm)}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { useContext, useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
import { loginRequest } from "../api/auth";
|
import { loginRequest } from "../api/auth";
|
||||||
import { AuthContext } from "../context/AuthContext";
|
import { AuthContext } from "../context/AuthContext";
|
||||||
|
import "../styles/Login.css";
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const { login } = useContext(AuthContext);
|
const { login } = useContext(AuthContext);
|
||||||
@ -11,6 +13,7 @@ export default function Login() {
|
|||||||
const submit = async (e) => {
|
const submit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError("");
|
setError("");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await loginRequest(username, password);
|
const data = await loginRequest(username, password);
|
||||||
login(data);
|
login(data);
|
||||||
@ -20,25 +23,35 @@ export default function Login() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="login-wrapper">
|
||||||
<h1>Login</h1>
|
<div className="login-box">
|
||||||
{error && <p style={{ color: "red" }}>{error}</p>}
|
<h1 className="login-title">Login</h1>
|
||||||
|
|
||||||
<form onSubmit={submit}>
|
{error && <p className="login-error">{error}</p>}
|
||||||
<input
|
|
||||||
type="text"
|
<form onSubmit={submit}>
|
||||||
placeholder="Username"
|
<input
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
type="text"
|
||||||
/>
|
className="login-input"
|
||||||
<input
|
placeholder="Username"
|
||||||
type="password"
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
placeholder="Password"
|
/>
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
/>
|
<input
|
||||||
<button type="submit">Login</button>
|
type="password"
|
||||||
</form>
|
className="login-input"
|
||||||
|
placeholder="Password"
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button type="submit" className="login-button">Login</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p className="login-register">
|
||||||
|
Need an account? <Link to="/register">Register here</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@ -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 (
|
||||||
|
<div className="register-container">
|
||||||
|
<h1>Register</h1>
|
||||||
|
|
||||||
|
{<p className="error-message">{error}</p>}
|
||||||
|
{success && <p className="success-message">{success}</p>}
|
||||||
|
|
||||||
|
<form className="register-form" onSubmit={submit}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Name"
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Username"
|
||||||
|
onKeyUp={(e) => setUsername(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="Confirm Password"
|
||||||
|
onChange={(e) => setConfirm(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button disabled={error !== ""} type="submit">
|
||||||
|
Create Account
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p className="register-link">
|
||||||
|
Already have an account? <Link to="/login">Login here</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
70
frontend/src/styles/Login.css
Normal file
70
frontend/src/styles/Login.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
84
frontend/src/styles/Register.css
Normal file
84
frontend/src/styles/Register.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user