Photo by Volodymyr Hryshchenko on Unsplash
Building a Comprehensive User Authentication Flow with React and Node.js (Part 2)
In this blog post, we’re delving into the front-end setup for our crypto exchange platform’s user authentication. Following the backend setup we explored earlier, let’s now build a user-friendly, responsive React interface that includes registration, login, password reset, and a wallet management feature.
Front-End Development with React
Prerequisites
Ensure that you’ve already:
Created a new React application (
npx create-react-app mycrypt
).Installed essential dependencies for styling and icons (e.g.,
react-router-dom
,axios
, andreact-icons
).
Step 1: Folder and Component Structure
Our front-end app will consist of multiple components, each handling a specific part of the authentication flow. Components like RegisterPage
, LoginPage
, ForgotPasswordPage
, and ResetPasswordPage
are organized separately, with each containing its own styles and form handling logic.
Setting up Routes
Inside Routes.js
, define the application routes with useRoutes
. This enables easy navigation and helps in setting up secure routes for protected pages such as the Wallet.
import React from 'react';
import { useRoutes } from 'react-router-dom';
import NotFound from './NotFound';
import ForgotPasswordPage from './ForgotPasswordPage';
import LoginPage from './LoginPage';
import RegisterPage from './RegisterPage';
import WalletScreen from './WalletScreen';
import SwapScreen from './SwapScreen';
import ResetPasswordPage from './ResetPasswordPage';
import SuccessPage from './SuccessPage';
const Routes = () => {
const element = useRoutes([
{
children: [
{
path: '/',
element: <LoginPage />
},
{
path: '/register',
element: <RegisterPage />
},
{
path: '/forgot',
element: <ForgotPasswordPage />
},
{
path: '/reset/:token', // Update this to accept a token parameter
element: <ResetPasswordPage />
},
{
path: '/success',
element: <SuccessPage />
},
{
path: '/wallet',
element: <WalletScreen />
},
{
path: '/swap',
element: <SwapScreen />
},
{
path: '*',
element: <NotFound />
},
]
}
])
return element
}
export default Routes
Step 2: Creating Registration and Login Pages
Each page component (e.g., RegisterPage
, LoginPage
) uses form validation and Axios to communicate with the backend. For example, the RegisterPage
component collects email, password, and confirmation fields, with validation to ensure that passwords match. Error and success messages provide feedback, enhancing the user experience.
LOGIN
import React, { useState } from 'react';
import { FaBitcoin } from 'react-icons/fa';
import '../css/LoginPage.css';
import { Link, useNavigate } from 'react-router-dom';
import websiteConstants from '../components/websiteConstants';
import axios from 'axios';
const LoginPage = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errorMessage, setErrorMessage] = useState('');
const navigate = useNavigate();
const handleLogin = async (e) => {
e.preventDefault();
setErrorMessage('');
try {
const response = await axios.post('http://localhost:5000/login', {
email,
password,
}, { withCredentials: true });
if (response.status === 200) {
navigate('/wallet');
}
} catch (error) {
if (error.response && error.response.data && error.response.data.message) {
setErrorMessage(error.response.data.message);
} else {
setErrorMessage('An error occurred during login. Please try again.');
}
}
};
return (
<div className="login-page">
<div className="login-container">
<div className="crypto-icon">
<FaBitcoin size={50} color="#00ffcc" />
</div>
<h2>Login to {websiteConstants.WEBSITE_NAME}</h2>
<form onSubmit={handleLogin} className="login-form">
<div className="form-group">
<label htmlFor="email">Email Address</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
{errorMessage && <p className="error-text">{errorMessage}</p>}
<button type="submit" className="login-button">
Login
</button>
</form>
<p className="link-text">
Don't have an account? <Link to="/register">Sign Up</Link>
</p>
<p className="link-text">
Forgot your password? <Link to="/forgot">Reset Password</Link>
</p>
</div>
</div>
);
};
export default LoginPage;
Register
import React, { useState } from 'react';
import { FaBitcoin } from 'react-icons/fa';
import '../css/LoginPage.css';
import { Link, useNavigate } from 'react-router-dom';
import websiteConstants from '../components/websiteConstants';
import axios from 'axios';
const RegisterPage = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [errorMessage, setErrorMessage] = useState('');
const [successMessage, setSuccessMessage] = useState('');
const navigate = useNavigate();
const handleSignup = async (e) => {
e.preventDefault();
setErrorMessage('');
setSuccessMessage('');
try {
const response = await axios.post('http://localhost:5000/register', {
email,
password,
confirmPassword,
}, { withCredentials: true });
if (response.status === 201) {
setSuccessMessage('User registered successfully!');
setTimeout(() => {
navigate('/wallet');
}, 2000); // Redirect to wallet page after 2 seconds
}
} catch (error) {
if (error.response && error.response.data && error.response.data.message) {
setErrorMessage(error.response.data.message);
} else {
setErrorMessage('An error occurred during signup. Please try again.');
}
}
};
return (
<div className="login-page">
<div className="login-container">
<div className="crypto-icon">
<FaBitcoin size={50} color="#00ffcc" />
</div>
<h2>Sign Up for {websiteConstants.WEBSITE_NAME}</h2>
<form onSubmit={handleSignup} className="login-form">
<div className="form-group">
<label htmlFor="email">Email Address</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<div className="form-group">
<label htmlFor="confirmPassword">Confirm Password</label>
<input
type="password"
id="confirmPassword"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
/>
</div>
{errorMessage && <p className="error-text">{errorMessage}</p>}
{successMessage && <p className="success-text">{successMessage}</p>}
<button type="submit" className="login-button">
Sign Up
</button>
</form>
<p className="link-text">
Already have an account? <Link to="/">Login</Link>
</p>
</div>
</div>
);
};
export default RegisterPage;
Step 3: Forgot and Reset Password Components
The ForgotPasswordPage
allows users to request a password reset link, while ResetPasswordPage
enables users to update their password securely by validating the reset token.
In ForgotPasswordPage
, an Axios POST request sends the user’s email to the backend, which, if valid, generates a reset token.
import React, { useState } from 'react';
import { FaBitcoin } from 'react-icons/fa';
import '../css/LoginPage.css';
import { Link } from 'react-router-dom';
import axios from 'axios';
const ForgotPasswordPage = () => {
const [email, setEmail] = useState('');
const [errorMessage, setErrorMessage] = useState('');
const [successMessage, setSuccessMessage] = useState('');
const handleForgotPassword = async (e) => {
e.preventDefault();
setErrorMessage('');
setSuccessMessage('');
try {
console.log('Sending forgot password request for:', email);
const response = await axios.post('http://localhost:5000/forgot', { email }, { withCredentials: true });
console.log('Forgot password response:', response);
if (response.status === 200) {
setSuccessMessage('Password reset link sent to your email');
}
} catch (error) {
console.log('Forgot password error response:', error);
if (error.response && error.response.data && error.response.data.message) {
setErrorMessage(error.response.data.message);
} else {
setErrorMessage('An error occurred. Please try again.');
}
}
};
return (
<div className="login-page">
<div className="login-container">
<div className="crypto-icon">
<FaBitcoin size={50} color="#00ffcc" />
</div>
<h2>Forgot Password</h2>
<form onSubmit={handleForgotPassword} className="login-form">
<div className="form-group">
<label htmlFor="email">Email Address</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
{errorMessage && <p className="error-text">{errorMessage}</p>}
{successMessage && <p className="success-text">{successMessage}</p>}
<button type="submit" className="login-button">
Reset Password
</button>
</form>
<p className="link-text">
Remembered your password? <Link to="/">Login</Link>
</p>
</div>
</div>
);
};
export default ForgotPasswordPage;
import React, { useState } from 'react';
import { FaBitcoin } from 'react-icons/fa';
import '../css/LoginPage.css';
import { useParams, useNavigate, Link } from 'react-router-dom';
import axios from 'axios';
const ResetPasswordPage = () => {
const { token } = useParams(); // Extract the token from the URL
const navigate = useNavigate(); // Hook to navigate programmatically
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [errorMessage, setErrorMessage] = useState('');
const [successMessage, setSuccessMessage] = useState('');
const handleResetPassword = async (e) => {
e.preventDefault();
setErrorMessage('');
setSuccessMessage('');
if (!token) {
setErrorMessage('Invalid or expired reset token');
return;
}
try {
const response = await axios.post(`http://localhost:5000/reset/${token}`, {
password,
confirmPassword,
});
if (response.status === 200) {
setSuccessMessage('Password updated successfully');
setTimeout(() => {
navigate('/success'); // Redirect to the success page after a short delay
}, 2000);
}
} catch (error) {
if (error.response && error.response.data && error.response.data.message) {
setErrorMessage(error.response.data.message);
} else {
setErrorMessage('An error occurred. Please try again.');
}
}
};
return (
<div className="login-page">
<div className="login-container">
<div className="crypto-icon">
<FaBitcoin size={50} color="#00ffcc" />
</div>
<h2>Reset Password</h2>
<form onSubmit={handleResetPassword} className="login-form">
<div className="form-group">
<label htmlFor="password">New Password</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<div className="form-group">
<label htmlFor="confirmPassword">Confirm New Password</label>
<input
type="password"
id="confirmPassword"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
/>
</div>
{errorMessage && <p className="error-text">{errorMessage}</p>}
{successMessage && <p className="success-text">{successMessage}</p>}
<button type="submit" className="login-button">
Update Password
</button>
</form>
<p className="link-text">
Remembered your password? <Link to="/">Login</Link>
</p>
</div>
</div>
);
};
export default ResetPasswordPage;
import React from 'react';
import { FaBitcoin } from 'react-icons/fa';
import '../css/LoginPage.css';
const SuccessPage = () => {
return (
<div className="login-page">
<div className="login-container">
<div className="crypto-icon">
<FaBitcoin size={50} color="#00ffcc" />
</div>
<h2>Success!</h2>
<p>Your action was completed successfully.</p>
<button className="login-button" onClick={() => window.location.href = '/'}>
Go to Login
</button>
</div>
</div>
);
};
export default SuccessPage;
Step 4: Wallet Management Interface
The WalletScreen
component displays the user’s cryptocurrency balance and integrates with the logout endpoint to securely end user sessions.
Inside the WalletScreen
, action buttons (e.g., Send, Receive, Buy) and bottom navigation tabs enable seamless navigation within the wallet section.
import React, { useEffect, useState } from 'react';
import '../css/WalletScreen.css';
import { IconButton, Button } from '@mui/material';
import SendIcon from '@mui/icons-material/Send';
import DownloadIcon from '@mui/icons-material/Download';
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
import AttachMoneyIcon from '@mui/icons-material/AttachMoney';
import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet';
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
import ExploreIcon from '@mui/icons-material/Explore';
import LanguageIcon from '@mui/icons-material/Language';
import SettingsIcon from '@mui/icons-material/Settings';
import { NavLink, useNavigate } from 'react-router-dom';
import axios from 'axios';
const WalletScreen = () => {
const [user, setUser] = useState(null);
const navigate = useNavigate();
useEffect(() => {
const fetchUser = async () => {
try {
const response = await axios.get('http://localhost:5000/user', { withCredentials: true });
if (response.status === 200) {
setUser(response.data.user);
} else {
navigate('/');
}
} catch (error) {
navigate('/');
}
};
fetchUser();
}, [navigate]);
// Handle logout
const handleLogout = async () => {
try {
const response = await axios.post('http://localhost:5000/logout', {}, { withCredentials: true });
if (response.status === 200) {
navigate('/');
}
} catch (error) {
console.error('Logout failed:', error);
}
};
const coins = [
{ name: 'BTC', price: '$66,844.30', change: '-1.71%', icon: <AttachMoneyIcon />, balance: '0' },
{ name: 'ETH', price: '$2,451.09', change: '-2.87%', icon: <AttachMoneyIcon />, balance: '0' },
{ name: 'TRX', price: '$0.16', change: '-1.25%', icon: <AttachMoneyIcon />, balance: '0' },
{ name: 'BNB', price: '$578.05', change: '-2.95%', icon: <AttachMoneyIcon />, balance: '0' },
];
return (
<div className="wallet-screen">
{/* Top Balance Section */}
<div className="balance-section">
<h2>$0.00</h2>
<p>Main Wallet</p>
{user && (
<div>
<p>Welcome, {user.email}</p>
<Button variant="contained" color="secondary" onClick={handleLogout}>
Logout
</Button>
</div>
)}
</div>
{/* Action Buttons */}
<div className="action-buttons">
{[
{ action: 'Send', icon: <SendIcon /> },
{ action: 'Receive', icon: <DownloadIcon /> },
{ action: 'Buy', icon: <ShoppingCartIcon /> },
{ action: 'Earn', icon: <AttachMoneyIcon /> },
].map((item, index) => (
<div key={index} className="action-button">
<IconButton style={{ backgroundColor: '#007bff', color: '#fff' }}>
{item.icon}
</IconButton>
<p>{item.action}</p>
</div>
))}
</div>
{/* Coins/NFTs Toggle */}
<div className="toggle-section">
<button className="toggle-button active">Coins</button>
<button className="toggle-button">NFTs</button>
</div>
{/* Coin List */}
<div className="coin-list">
{coins.map((coin, index) => (
<div key={index} className="coin-item">
<div className="coin-icon">
{coin.icon}
</div>
<div className="coin-details">
<span className="coin-name">{coin.name}</span>
<span className={`coin-price ${coin.change.startsWith('-') ? 'negative' : 'positive'}`}>
{coin.price} {coin.change}
</span>
</div>
<span className="coin-balance">{coin.balance}</span>
</div>
))}
</div>
{/* Bottom Navigation */}
<div className="bottom-navigation">
{[
{ label: 'Wallet', icon: <AccountBalanceWalletIcon />, path: '/' },
{ label: 'Swap', icon: <SwapHorizIcon />, path: '/swap' },
{ label: 'Discover', icon: <ExploreIcon />, path: '/discover' },
{ label: 'Browser', icon: <LanguageIcon />, path: '/browser' },
{ label: 'Settings', icon: <SettingsIcon />, path: '/settings' },
].map((tab, index) => (
<NavLink
to={tab.path}
key={index}
className="nav-item"
activeClassName="active-link"
>
{tab.icon}
<p>{tab.label}</p>
</NavLink>
))}
</div>
</div>
);
};
export default WalletScreen;
Step 5: Styling and Icons
The styling is managed through CSS files (LoginPage.css
, WalletScreen.css
), with color consistency, intuitive layouts, and interactive icons (react-icons
). Icons provide a familiar and user-friendly interface, particularly helpful for new users.
Summary
In this guide, we’ve completed the front-end of our authentication system, focusing on:
Registration and Login: Secure sign-up and login forms with real-time feedback.
Password Management: User-friendly forgot and reset password flows.
Wallet Interface: An interactive, easy-to-navigate wallet page with cryptocurrency balances and action buttons.
This full-stack project provides a solid foundation for any authentication flow in a modern web application, complete with the best practices for usability, security, and responsive design. The next steps could include adding email verification, multi-factor authentication, and integrating OAuth for even stronger security and user convenience.