fix: user session management
This commit is contained in:
74
src/App.jsx
74
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 (
|
||||
<div className="app" style={{ display: "flex", alignItems: "center", justifyContent: "center", minHeight: "100vh" }}>
|
||||
<style>{styles}</style>
|
||||
<div style={{ textAlign: "center", color: "var(--text3)" }}>
|
||||
<div style={{ fontSize: 32 }}>☕</div>
|
||||
<div style={{ marginTop: 8 }}>Loading…</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If not authenticated, show login/register
|
||||
if (!token || !user) {
|
||||
return (
|
||||
<div className="app">
|
||||
<style>{styles}</style>
|
||||
{authView === "login" ? (
|
||||
<>
|
||||
<Login onLoginSuccess={() => {}} />
|
||||
<div style={{ textAlign: "center", marginTop: "20px" }}>
|
||||
<p style={{ color: "var(--text3)" }}>Don't have an account?{' '}
|
||||
<button
|
||||
onClick={() => setAuthView("register")}
|
||||
style={{ background: 'none', border: 'none', color: 'var(--accent)', cursor: 'pointer', textDecoration: 'underline' }}
|
||||
>
|
||||
Register
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Register onRegisterSuccess={() => setAuthView("login")} />
|
||||
<div style={{ textAlign: "center", marginTop: "20px" }}>
|
||||
<p style={{ color: "var(--text3)" }}>Already have an account?{' '}
|
||||
<button
|
||||
onClick={() => setAuthView("login")}
|
||||
style={{ background: 'none', border: 'none', color: 'var(--accent)', cursor: 'pointer', textDecoration: 'underline' }}
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) return <div className="app" style={{ display: "flex", alignItems: "center", justifyContent: "center", minHeight: "100vh" }}><div style={{ textAlign: "center", color: "var(--text3)" }}><div style={{ fontSize: 32 }}>☕</div><div style={{ marginTop: 8 }}>Loading…</div></div></div>;
|
||||
|
||||
const { beans, brewLogs } = data;
|
||||
@@ -777,7 +832,22 @@ export default function CoffeeLogbook() {
|
||||
<div className="header-sub">Coffee Logbook</div>
|
||||
<h1>Brew Journal</h1>
|
||||
</div>
|
||||
<div style={{ fontSize: 28 }}>☕</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
|
||||
<span style={{ fontSize: "12px", color: "var(--text2)" }}>{user?.username}</span>
|
||||
<button
|
||||
onClick={() => logout()}
|
||||
style={{
|
||||
fontSize: 28,
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
padding: '4px 8px'
|
||||
}}
|
||||
title="Logout"
|
||||
>
|
||||
🚪
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Nav */}
|
||||
|
||||
89
src/AuthContext.jsx
Normal file
89
src/AuthContext.jsx
Normal file
@@ -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 (
|
||||
<AuthContext.Provider value={{ token, user, loading, login, register, logout }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="auth-form">
|
||||
<h2>Login</h2>
|
||||
{error && <p style={{color: 'red'}}>{error}</p>}
|
||||
<div className="auth-form" style={{ maxWidth: '400px', margin: '50px auto' }}>
|
||||
<h2>Login to Brew Journal</h2>
|
||||
{error && <p style={{ color: '#B44040', marginBottom: '12px' }}>{error}</p>}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
type="email"
|
||||
@@ -32,6 +32,7 @@ export default function Login({ setToken }) {
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({...formData, email: e.target.value})}
|
||||
required
|
||||
style={{ marginBottom: '12px', padding: '10px', width: '100%', borderRadius: '8px', border: '1px solid #e8dfd3' }}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
@@ -39,8 +40,24 @@ export default function Login({ setToken }) {
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({...formData, password: e.target.value})}
|
||||
required
|
||||
style={{ marginBottom: '12px', padding: '10px', width: '100%', borderRadius: '8px', border: '1px solid #e8dfd3' }}
|
||||
/>
|
||||
<button type="submit">Login</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
style={{
|
||||
padding: '10px 16px',
|
||||
width: '100%',
|
||||
backgroundColor: '#8B6914',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
cursor: loading ? 'not-allowed' : 'pointer',
|
||||
opacity: loading ? 0.6 : 1
|
||||
}}
|
||||
>
|
||||
{loading ? 'Logging in...' : 'Login'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<div className="auth-form">
|
||||
<h2>Register</h2>
|
||||
{error && <p style={{color: 'red'}}>{error}</p>}
|
||||
<div className="auth-form" style={{ maxWidth: '400px', margin: '50px auto' }}>
|
||||
<h2>Register for Brew Journal</h2>
|
||||
{error && <p style={{ color: '#B44040', marginBottom: '12px' }}>{error}</p>}
|
||||
{success && <p style={{ color: '#4A7C59', marginBottom: '12px' }}>{success}</p>}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
type="text"
|
||||
@@ -31,6 +48,7 @@ export default function Register({ setToken }) {
|
||||
value={formData.username}
|
||||
onChange={(e) => setFormData({...formData, username: e.target.value})}
|
||||
required
|
||||
style={{ marginBottom: '12px', padding: '10px', width: '100%', borderRadius: '8px', border: '1px solid #e8dfd3' }}
|
||||
/>
|
||||
<input
|
||||
type="email"
|
||||
@@ -38,6 +56,7 @@ export default function Register({ setToken }) {
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({...formData, email: e.target.value})}
|
||||
required
|
||||
style={{ marginBottom: '12px', padding: '10px', width: '100%', borderRadius: '8px', border: '1px solid #e8dfd3' }}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
@@ -45,8 +64,32 @@ export default function Register({ setToken }) {
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({...formData, password: e.target.value})}
|
||||
required
|
||||
style={{ marginBottom: '12px', padding: '10px', width: '100%', borderRadius: '8px', border: '1px solid #e8dfd3' }}
|
||||
/>
|
||||
<button type="submit">Register</button>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Confirm Password"
|
||||
value={formData.confirmPassword}
|
||||
onChange={(e) => setFormData({...formData, confirmPassword: e.target.value})}
|
||||
required
|
||||
style={{ marginBottom: '12px', padding: '10px', width: '100%', borderRadius: '8px', border: '1px solid #e8dfd3' }}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
style={{
|
||||
padding: '10px 16px',
|
||||
width: '100%',
|
||||
backgroundColor: '#8B6914',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
cursor: loading ? 'not-allowed' : 'pointer',
|
||||
opacity: loading ? 0.6 : 1
|
||||
}}
|
||||
>
|
||||
{loading ? 'Registering...' : 'Register'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
<StrictMode>
|
||||
<App />
|
||||
<AuthProvider>
|
||||
<App />
|
||||
</AuthProvider>
|
||||
</StrictMode>,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user