Made everything pretty

Fix registration
This commit is contained in:
Nico 2025-11-23 00:12:17 -08:00
parent 963240ceb1
commit d9c4a2caf9
13 changed files with 353 additions and 43 deletions

View File

@ -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 });

View File

@ -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);
};

View File

@ -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;
} }

View File

@ -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;

View File

@ -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

View File

@ -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;
}; };

View File

@ -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 } });

View File

@ -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}

View File

@ -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)}

View File

@ -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>
) );
} }

View File

@ -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>
);
}

View 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;
}

View 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;
}