From a37e3de6fc02be8299f0a9ad40490d7878821684 Mon Sep 17 00:00:00 2001 From: Louis Sabaricos Date: Sun, 26 Apr 2026 16:16:02 -0700 Subject: [PATCH] Add user authentication --- client/package-lock.json | 17 +--- client/src/App.jsx | 4 +- client/src/Pages_css/AuthPage.css | 87 ++++++++++++++++ client/src/components/NavBar.css | 15 +++ client/src/components/NavBar.jsx | 39 +++++++- client/src/pages/AuthPage.jsx | 123 +++++++++++++++++++++++ package-lock.json | 131 ++++++++++++++++++++++++ package.json | 4 +- server/controllers/users.js | 159 ++++++++++++++++++++++++++++++ server/routes/users.js | 14 +++ server/server.js | 2 + 11 files changed, 576 insertions(+), 19 deletions(-) create mode 100644 client/src/Pages_css/AuthPage.css create mode 100644 client/src/pages/AuthPage.jsx create mode 100644 server/controllers/users.js create mode 100644 server/routes/users.js diff --git a/client/package-lock.json b/client/package-lock.json index e508fcc27..e1c252483 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -45,7 +45,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1171,7 +1170,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -1427,7 +1425,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, - "peer": true, "engines": { "node": ">=12" }, @@ -1467,7 +1464,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -1479,7 +1475,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -1644,7 +1639,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "dev": true, - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -1744,7 +1738,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, - "peer": true, "requires": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -2390,7 +2383,6 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "dev": true, - "peer": true, "requires": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -2475,8 +2467,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "requires": {} + "dev": true }, "fsevents": { "version": "2.3.3", @@ -2553,8 +2544,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "peer": true + "dev": true }, "postcss": { "version": "8.5.10", @@ -2571,7 +2561,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "peer": true, "requires": { "loose-envify": "^1.1.0" } @@ -2580,7 +2569,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "peer": true, "requires": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -2686,7 +2674,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "dev": true, - "peer": true, "requires": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/client/src/App.jsx b/client/src/App.jsx index b56408803..e852bcd46 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -8,6 +8,7 @@ import ScanAFlower from './pages/scanAFlower'; import FlowerGallery from './pages/flowerGallery'; import DiscoverPage from './pages/discoverPage'; import IndividualFlowerPage from './pages/individualFlowerPage'; +import AuthPage from './pages/AuthPage'; import NavBar from './components/NavBar'; import Footer from './components/Footer'; @@ -28,7 +29,8 @@ const App = () => { { path: '/dictionary', element: }, { path: '/scan', element: }, { path: '/gallery', element: }, - { path: '/gallery/:flowerId', element: } + { path: '/gallery/:flowerId', element: }, + { path: '/login', element: } ]); diff --git a/client/src/Pages_css/AuthPage.css b/client/src/Pages_css/AuthPage.css new file mode 100644 index 000000000..939ea24d4 --- /dev/null +++ b/client/src/Pages_css/AuthPage.css @@ -0,0 +1,87 @@ +.auth-page { + min-height: 70vh; + display: flex; + justify-content: center; + align-items: center; + padding: 40px 16px; +} + +.auth-card { + width: 100%; + max-width: 420px; + background-color: #ffffff; + border: 1px solid #d9d9d9; + border-radius: 12px; + padding: 28px; + box-shadow: 0 4px 14px rgba(0, 0, 0, 0.12); +} + +.auth-card h1 { + margin: 0 0 8px; + color: #373d3f; + text-align: center; +} + +.auth-subtitle { + text-align: center; + color: #555; + margin-bottom: 24px; +} + +.auth-form { + display: flex; + flex-direction: column; + gap: 12px; +} + +.auth-form label { + font-weight: 600; + color: #373d3f; +} + +.auth-form input { + padding: 12px; + border: 1px solid #bdbdbd; + border-radius: 8px; + font-size: 1rem; +} + +.auth-form button { + margin-top: 12px; + padding: 12px; + border: none; + border-radius: 8px; + background-color: #373d3f; + color: white; + font-size: 1rem; + cursor: pointer; +} + +.auth-form button:hover { + background-color: #4f5a5d; +} + +.auth-form button:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.auth-message { + color: #b00020; + font-size: 0.9rem; + margin: 4px 0; +} + +.auth-toggle { + width: 100%; + margin-top: 18px; + background: none; + border: none; + color: #2a7abf; + cursor: pointer; + font-size: 0.95rem; +} + +.auth-toggle:hover { + text-decoration: underline; +} \ No newline at end of file diff --git a/client/src/components/NavBar.css b/client/src/components/NavBar.css index f368bbb76..5a9afa128 100644 --- a/client/src/components/NavBar.css +++ b/client/src/components/NavBar.css @@ -81,3 +81,18 @@ padding: 4px; line-height: 1; } +.navbar-auth-button { + font-family: Avenir, "Century Gothic", sans-serif; + font-size: 0.95rem; + color: #373D3F; + background: none; + border: none; + cursor: pointer; + padding: 4px 2px; + border-bottom: 2px solid transparent; +} + +.navbar-auth-button:hover { + color: #2a7abf; + border-bottom-color: #2a7abf; +} \ No newline at end of file diff --git a/client/src/components/NavBar.jsx b/client/src/components/NavBar.jsx index a4c3a4815..130c7b4f2 100644 --- a/client/src/components/NavBar.jsx +++ b/client/src/components/NavBar.jsx @@ -1,11 +1,27 @@ import { NavLink, useNavigate } from 'react-router'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import './NavBar.css'; const NavBar = () => { const [query, setQuery] = useState(''); + const [user, setUser] = useState(null); const navigate = useNavigate(); + useEffect(() => { + const loadUser = () => { + const savedUser = localStorage.getItem('flowerhuntUser'); + setUser(savedUser ? JSON.parse(savedUser) : null); + }; + + loadUser(); + + window.addEventListener('authChanged', loadUser); + + return () => { + window.removeEventListener('authChanged', loadUser); + }; + }, []); + const handleSearch = (e) => { e.preventDefault(); if (query.trim()) { @@ -14,17 +30,36 @@ const NavBar = () => { } }; + const handleLogout = () => { + localStorage.removeItem('flowerhuntToken'); + localStorage.removeItem('flowerhuntUser'); + setUser(null); + navigate('/'); + }; + return (