From 9168ece20919dabb1cb6184368db889df7abd038 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sat, 6 Jun 2026 08:47:24 +0530 Subject: [PATCH] fix: user session management --- server/.env | 3 -- server/index.js | 49 +++++++++++++++++++++++-- server/package.json | 1 + src/App.jsx | 74 ++++++++++++++++++++++++++++++++++++- src/AuthContext.jsx | 89 +++++++++++++++++++++++++++++++++++++++++++++ src/Login.jsx | 53 ++++++++++++++++++--------- src/Register.jsx | 79 +++++++++++++++++++++++++++++++--------- src/main.jsx | 5 ++- 8 files changed, 307 insertions(+), 46 deletions(-) delete mode 100644 server/.env create mode 100644 src/AuthContext.jsx diff --git a/server/.env b/server/.env deleted file mode 100644 index 5499840..0000000 --- a/server/.env +++ /dev/null @@ -1,3 +0,0 @@ -DATABASE_URL=postgresql://user:password@localhost:5432/brew -JWT_SECRET=your_jwt_secret_here -PORT=5000 diff --git a/server/index.js b/server/index.js index 4bbdc8f..df10aa8 100644 --- a/server/index.js +++ b/server/index.js @@ -11,11 +11,29 @@ const PORT = process.env.PORT || 5000; app.use(cors()); app.use(express.json()); +// Middleware to verify JWT token +const authenticateToken = (req, res, next) => { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + + if (!token) return res.status(401).json({ error: 'No token provided' }); + + jwt.verify(token, process.env.JWT_SECRET || 'fallback_secret', (err, user) => { + if (err) return res.status(403).json({ error: 'Invalid token' }); + req.user = user; + next(); + }); +}; + // Registration app.post('/api/register', async (req, res) => { try { const { username, email, password } = req.body; + if (!username || !email || !password) { + return res.status(400).json({ error: 'All fields are required' }); + } + // Check if user exists const userExists = await pool.query('SELECT * FROM users WHERE email = $1 OR username = $2', [email, username]); if (userExists.rows.length > 0) { @@ -32,7 +50,7 @@ app.post('/api/register', async (req, res) => { [username, email, passwordHash] ); - res.status(201).json({ user: newUser.rows[0] }); + res.status(201).json({ message: 'Registration successful', user: newUser.rows[0] }); } catch (err) { console.error(err); res.status(500).json({ error: 'Server error' }); @@ -44,6 +62,10 @@ app.post('/api/login', async (req, res) => { try { const { email, password } = req.body; + if (!email || !password) { + return res.status(400).json({ error: 'Email and password are required' }); + } + // Find user const user = await pool.query('SELECT * FROM users WHERE email = $1', [email]); if (user.rows.length === 0) { @@ -60,7 +82,7 @@ app.post('/api/login', async (req, res) => { const token = jwt.sign( { id: user.rows[0].id, username: user.rows[0].username }, process.env.JWT_SECRET || 'fallback_secret', - { expiresIn: '1h' } + { expiresIn: '24h' } ); res.json({ token, user: { id: user.rows[0].id, username: user.rows[0].username, email: user.rows[0].email } }); @@ -70,6 +92,25 @@ app.post('/api/login', async (req, res) => { } }); -app.listen(PORT, () => { - console.log(\`Server running on port \${PORT}\`); +// Get user profile (protected route) +app.get('/api/profile', authenticateToken, async (req, res) => { + try { + const user = await pool.query('SELECT id, username, email, created_at FROM users WHERE id = $1', [req.user.id]); + if (user.rows.length === 0) { + return res.status(404).json({ error: 'User not found' }); + } + res.json(user.rows[0]); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Server error' }); + } +}); + +// Verify token +app.post('/api/verify-token', authenticateToken, (req, res) => { + res.json({ valid: true, user: req.user }); +}); + +app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); }); diff --git a/server/package.json b/server/package.json index cc4a8e7..7701909 100644 --- a/server/package.json +++ b/server/package.json @@ -4,6 +4,7 @@ "description": "", "main": "index.js", "scripts": { + "start": "node index.js", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], diff --git a/src/App.jsx b/src/App.jsx index 0770780..31739a8 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,7 @@ -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback, useContext } from "react"; +import { AuthContext } from "./AuthContext"; +import Login from "./Login"; +import Register from "./Register"; // ─── Storage helpers ─── const STORAGE_KEY = "coffee-logbook-data"; @@ -714,6 +717,8 @@ function BrewCard({ log, beanName }) { // ─── Main App ─── export default function CoffeeLogbook() { + const { token, user, loading, logout } = useContext(AuthContext); + const [authView, setAuthView] = useState("login"); // login | register const [data, setData] = useState(null); const [view, setView] = useState("dashboard"); // dashboard | beans | brews const [modal, setModal] = useState(null); // null | "addBean" | "editBean" | "addBrew" @@ -731,6 +736,56 @@ export default function CoffeeLogbook() { await saveData(newData); }, []); + if (loading) { + return ( +
+ +
+
+
Loading…
+
+
+ ); + } + + // If not authenticated, show login/register + if (!token || !user) { + return ( +
+ + {authView === "login" ? ( + <> + {}} /> +
+

Don't have an account?{' '} + +

+
+ + ) : ( + <> + setAuthView("login")} /> +
+

Already have an account?{' '} + +

+
+ + )} +
+ ); + } + if (!data) return
Loading…
; const { beans, brewLogs } = data; @@ -777,7 +832,22 @@ export default function CoffeeLogbook() {
Coffee Logbook

Brew Journal

-
+
+ {user?.username} + +
{/* Nav */} diff --git a/src/AuthContext.jsx b/src/AuthContext.jsx new file mode 100644 index 0000000..0a05bec --- /dev/null +++ b/src/AuthContext.jsx @@ -0,0 +1,89 @@ +import { createContext, useState, useEffect, useCallback } from 'react'; + +export const AuthContext = createContext(); + +export function AuthProvider({ children }) { + const [token, setToken] = useState(null); + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + const verifyToken = useCallback(async (tkn) => { + try { + const res = await fetch('http://localhost:5000/api/verify-token', { + headers: { 'Authorization': `Bearer ${tkn}` } + }); + if (res.ok) { + const data = await res.json(); + setUser(data.user); + setLoading(false); + } else { + localStorage.removeItem('authToken'); + setToken(null); + setLoading(false); + } + } catch (err) { + console.error('Token verification failed', err); + localStorage.removeItem('authToken'); + setToken(null); + setLoading(false); + } + }, []); + + // Load token from localStorage on mount + useEffect(() => { + const savedToken = localStorage.getItem('authToken'); + if (savedToken) { + setToken(savedToken); + verifyToken(savedToken); + } else { + setLoading(false); + } + }, [verifyToken]); + + const login = async (email, password) => { + try { + const res = await fetch('http://localhost:5000/api/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password }) + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error); + + setToken(data.token); + setUser(data.user); + localStorage.setItem('authToken', data.token); + return { success: true }; + } catch (err) { + return { success: false, error: err.message }; + } + }; + + const register = async (username, email, password) => { + try { + const res = await fetch('http://localhost:5000/api/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, email, password }) + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error); + + return { success: true, message: data.message }; + } catch (err) { + return { success: false, error: err.message }; + } + }; + + const logout = () => { + setToken(null); + setUser(null); + localStorage.removeItem('authToken'); + }; + + return ( + + {children} + + ); +} diff --git a/src/Login.jsx b/src/Login.jsx index 0ccb6ef..d42064c 100644 --- a/src/Login.jsx +++ b/src/Login.jsx @@ -1,30 +1,30 @@ -import { useState } from 'react'; +import { useState, useContext } from 'react'; +import { AuthContext } from './AuthContext'; -export default function Login({ setToken }) { +export default function Login({ onLoginSuccess }) { const [formData, setFormData] = useState({ email: '', password: '' }); const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + const { login } = useContext(AuthContext); const handleSubmit = async (e) => { e.preventDefault(); - try { - const res = await fetch('http://localhost:5000/api/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(formData) - }); - const data = await res.json(); - if (!res.ok) throw new Error(data.error); - setToken(data.token); - alert('Login successful!'); - } catch (err) { - setError(err.message); + setError(''); + setLoading(true); + + const result = await login(formData.email, formData.password); + if (result.success) { + onLoginSuccess?.(); + } else { + setError(result.error); } + setLoading(false); }; return ( -
-

Login

- {error &&

{error}

} +
+

Login to Brew Journal

+ {error &&

{error}

}
setFormData({...formData, email: e.target.value})} required + style={{ marginBottom: '12px', padding: '10px', width: '100%', borderRadius: '8px', border: '1px solid #e8dfd3' }} /> setFormData({...formData, password: e.target.value})} required + style={{ marginBottom: '12px', padding: '10px', width: '100%', borderRadius: '8px', border: '1px solid #e8dfd3' }} /> - +
); diff --git a/src/Register.jsx b/src/Register.jsx index 4ed9a86..1d5d1f4 100644 --- a/src/Register.jsx +++ b/src/Register.jsx @@ -1,29 +1,46 @@ -import { useState } from 'react'; +import { useState, useContext } from 'react'; +import { AuthContext } from './AuthContext'; -export default function Register({ setToken }) { - const [formData, setFormData] = useState({ username: '', email: '', password: '' }); +export default function Register({ onRegisterSuccess }) { + const [formData, setFormData] = useState({ username: '', email: '', password: '', confirmPassword: '' }); const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + const [loading, setLoading] = useState(false); + const { register } = useContext(AuthContext); const handleSubmit = async (e) => { e.preventDefault(); - try { - const res = await fetch('http://localhost:5000/api/register', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(formData) - }); - const data = await res.json(); - if (!res.ok) throw new Error(data.error); - alert('Registration successful! Please login.'); - } catch (err) { - setError(err.message); + setError(''); + setSuccess(''); + + if (formData.password !== formData.confirmPassword) { + setError('Passwords do not match'); + return; } + + if (formData.password.length < 6) { + setError('Password must be at least 6 characters'); + return; + } + + setLoading(true); + const result = await register(formData.username, formData.email, formData.password); + + if (result.success) { + setSuccess(result.message); + setFormData({ username: '', email: '', password: '', confirmPassword: '' }); + onRegisterSuccess?.(); + } else { + setError(result.error); + } + setLoading(false); }; return ( -
-

Register

- {error &&

{error}

} +
+

Register for Brew Journal

+ {error &&

{error}

} + {success &&

{success}

}
setFormData({...formData, username: e.target.value})} required + style={{ marginBottom: '12px', padding: '10px', width: '100%', borderRadius: '8px', border: '1px solid #e8dfd3' }} /> setFormData({...formData, email: e.target.value})} required + style={{ marginBottom: '12px', padding: '10px', width: '100%', borderRadius: '8px', border: '1px solid #e8dfd3' }} /> setFormData({...formData, password: e.target.value})} required + style={{ marginBottom: '12px', padding: '10px', width: '100%', borderRadius: '8px', border: '1px solid #e8dfd3' }} /> - + setFormData({...formData, confirmPassword: e.target.value})} + required + style={{ marginBottom: '12px', padding: '10px', width: '100%', borderRadius: '8px', border: '1px solid #e8dfd3' }} + /> +
); diff --git a/src/main.jsx b/src/main.jsx index b9a1a6d..6a139d3 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -2,9 +2,12 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' import App from './App.jsx' +import { AuthProvider } from './AuthContext.jsx' createRoot(document.getElementById('root')).render( - + + + , )