fix: user session management

This commit is contained in:
2026-06-06 08:47:24 +05:30
parent 4ee2649d84
commit 9168ece209
8 changed files with 307 additions and 46 deletions

View File

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

View File

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

View File

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

View File

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