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 (
+
+ );
+ }
+
+ // 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 ;
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}
}
);
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}
}
);
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(
-
+
+
+
,
)