Building a Full-Stack User Authentication & Profile System with React and Flask
In this tutorial, we will build a complete user authentication system with a profile page using React on the frontend and Flask with MySQL on the backend. We will cover registration, login, and a dynamic profile page with a product carousel and loyalty card.
Prerequisites
- Node.js and npm installed
- Python 3 and pip installed
- MySQL installed and running
- Basic knowledge of React and Python
Project Structure
project/
├── frontend/
│ └── src/
│ ├── pages/
│ │ ├── login/
│ │ │ ├── login.jsx
│ │ │ └── login.css
│ │ ├── signUp/
│ │ │ ├── signUp.jsx
│ │ │ └── signUp.css
│ │ └── profile/
│ │ ├── profile.jsx
│ │ └── profile.css
│ ├── components/
│ │ └── productCard/
│ │ └── productCard.jsx
│ ├── data/
│ │ └── products.js
│ └── assets/
│ ├── chicken.png
│ ├── bag.png
│ └── cow.svg
└── backend/
└── app.py
Step 1 — Setting Up the Database
First, create your MySQL database and tables:
-- Drop existing database if it exists
DROP DATABASE IF EXISTS task_db;
-- Create a fresh database
CREATE DATABASE task_db;
USE task_db;
-- Role table
CREATE TABLE IF NOT EXISTS role (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE,
description VARCHAR(255)
);
-- Insert default roles
INSERT INTO role (name, description) VALUES
('admin', 'Full system access'),
('customer', 'Can browse and place orders'),
('staff', 'Can manage orders and products'),
('vendor', 'Supplier with limited access')
ON DUPLICATE KEY UPDATE name = name;
-- Users table (renamed from user)
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
role_id INT NOT NULL,
firstName VARCHAR(100) NOT NULL,
lastName VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
phone VARCHAR(20),
userPoints INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (role_id) REFERENCES role(id)
);
-- Producer table
CREATE TABLE IF NOT EXISTS producer (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL UNIQUE,
bio TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Category table
CREATE TABLE IF NOT EXISTS category (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE
);
-- Insert default categories
INSERT INTO category (name) VALUES
('Vegetables'),
('Fruits'),
('Dairy'),
('Honey & Preserves'),
('Meat & Poultry'),
('Drinks'),
('Bakery'),
('Herbs & Flowers')
ON DUPLICATE KEY UPDATE name = name;
-- Product table
CREATE TABLE IF NOT EXISTS product (
id INT AUTO_INCREMENT PRIMARY KEY,
producer_id INT NOT NULL,
category_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10, 2) NOT NULL,
stock_qty INT NOT NULL DEFAULT 0,
unit VARCHAR(50) NOT NULL DEFAULT 'item',
is_organic BOOLEAN DEFAULT FALSE,
is_available BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (producer_id) REFERENCES producer(id) ON DELETE CASCADE,
FOREIGN KEY (category_id) REFERENCES category(id)
);
-- Address table
CREATE TABLE IF NOT EXISTS address (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
street VARCHAR(255) NOT NULL,
city VARCHAR(100) NOT NULL,
postcode VARCHAR(20) NOT NULL,
country VARCHAR(100) NOT NULL DEFAULT 'United Kingdom',
is_default BOOLEAN DEFAULT FALSE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Orders table (renamed from `order`)
CREATE TABLE IF NOT EXISTS orders (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
address_id INT NOT NULL,
status ENUM('pending', 'confirmed', 'processing', 'shipped', 'delivered', 'cancelled') DEFAULT 'pending',
total_price DECIMAL(10, 2) NOT NULL,
payment_status ENUM('unpaid', 'paid', 'refunded') DEFAULT 'unpaid',
payment_method VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (address_id) REFERENCES address(id)
);
-- Order item table
CREATE TABLE IF NOT EXISTS order_item (
id INT AUTO_INCREMENT PRIMARY KEY,
order_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL DEFAULT 1,
unit_price DECIMAL(10, 2) NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES product(id)
);
Step 2 — Setting Up the Flask Backend
Install the required Python packages:
bashpip install flask flask-cors mysql-connector-python werkzeug python-dotenv
from flask import Flask, jsonify, request
from flask_cors import CORS
from werkzeug.security import generate_password_hash, check_password_hash
import mysql.connector
import os
from dotenv import load_dotenv
load_dotenv()
app = Flask(__name__)
CORS(app)
# ---------- DB CONNECTION ----------
def get_cursor():
conn = mysql.connector.connect(
host=os.getenv("DB_HOST", "localhost"),
port=int(os.getenv("DB_PORT", 3306)),
user=os.getenv("DB_USER"),
password=os.getenv("DB_PASSWORD"),
database=os.getenv("DB_NAME", "task_db")
)
return conn, conn.cursor(dictionary=True)
# ---------- REGISTER ----------
@app.post("/api/register")
def register():
conn, cursor = get_cursor()
try:
data = request.json or {}
firstName = data.get("firstName", "").strip()
lastName = data.get("lastName", "").strip()
email = data.get("email", "").strip()
password = data.get("password", "").strip()
confirmPassword = data.get("confirmPassword", "").strip()
# Validation
if not firstName:
return jsonify({"success": False, "message": "First name is required."}), 400
if not lastName:
return jsonify({"success": False, "message": "Last name is required."}), 400
if not email:
return jsonify({"success": False, "message": "Email is required."}), 400
if not password:
return jsonify({"success": False, "message": "Password is required."}), 400
if len(password) < 8:
return jsonify({"success": False, "message": "Password must be at least 8 characters."}), 400
if password != confirmPassword:
return jsonify({"success": False, "message": "Passwords do not match."}), 400
# Check duplicate email
cursor.execute("SELECT id FROM users WHERE email = %s", (email,))
if cursor.fetchone():
return jsonify({"success": False, "message": "Email already registered."}), 400
# Hash password before storing
hashed_password = generate_password_hash(password)
# Insert new user (role_id 2 = customer)
cursor.execute("""
INSERT INTO users (role_id, firstName, lastName, email, password_hash)
VALUES (%s, %s, %s, %s, %s)
""", (2, firstName, lastName, email, hashed_password))
conn.commit()
# Get new user ID
cursor.execute("SELECT LAST_INSERT_ID() AS id")
new_user = cursor.fetchone()
return jsonify({
"success": True,
"message": "User registered successfully!",
"user": {"id": new_user["id"]}
}), 200
except Exception as e:
conn.rollback()
print("REGISTER ERROR:", e)
return jsonify({"success": False, "message": "Server error"}), 500
finally:
cursor.close()
conn.close()
# ---------- LOGIN ----------
@app.post("/api/login")
def login():
conn, cursor = get_cursor()
try:
data = request.json or {}
email = data.get("email", "").strip()
password = data.get("password", "").strip()
# Validation
if not email:
return jsonify({"success": False, "message": "Email is required."}), 400
if not password:
return jsonify({"success": False, "message": "Password is required."}), 400
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))
user = cursor.fetchone()
if not user:
return jsonify({"success": False, "message": "Email not found."}), 400
# Check hashed password
if not check_password_hash(user["password_hash"], password):
return jsonify({"success": False, "message": "Incorrect password."}), 400
return jsonify({
"success": True,
"message": "Login successful.",
"user": {
"id": user["id"],
"firstName": user["firstName"],
"lastName": user["lastName"],
"email": user["email"],
"phone": user.get("phone"),
}
}), 200
except Exception as e:
print("LOGIN ERROR:", e)
return jsonify({"success": False, "message": "Server error"}), 500
finally:
cursor.close()
conn.close()
# ---------- GET PROFILE ----------
@app.get("/api/user/profile")
def get_profile():
conn, cursor = get_cursor()
try:
user_id = request.args.get("userId")
if not user_id:
return jsonify({"success": False, "message": "Missing userId"}), 400
cursor.execute("""
SELECT id, firstName, lastName, email, phone
FROM users
WHERE id = %s
""", (user_id,))
user = cursor.fetchone()
if not user:
return jsonify({"success": False, "message": "User not found"}), 404
return jsonify({"success": True, "user": user}), 200
except Exception as e:
print("PROFILE ERROR:", e)
return jsonify({"success": False, "message": "Server error"}), 500
finally:
cursor.close()
conn.close()
# ---------- UPDATE PROFILE ----------
@app.post("/api/user/update")
def update_profile():
conn, cursor = get_cursor()
try:
data = request.json or {}
user_id = data.get("userId")
if not user_id:
return jsonify({"success": False, "message": "Missing userId"}), 400
firstName = data.get("firstName", "").strip()
lastName = data.get("lastName", "").strip()
email = data.get("email", "").strip()
phone = data.get("phone", "").strip()
if not firstName:
return jsonify({"success": False, "message": "First name is required."}), 400
if not lastName:
return jsonify({"success": False, "message": "Last name is required."}), 400
if not email:
return jsonify({"success": False, "message": "Email is required."}), 400
# Check if new email is taken by another user
cursor.execute("SELECT id FROM users WHERE email = %s AND id != %s", (email, user_id))
if cursor.fetchone():
return jsonify({"success": False, "message": "Email already in use."}), 400
cursor.execute("""
UPDATE users
SET firstName=%s, lastName=%s, email=%s, phone=%s
WHERE id=%s
""", (firstName, lastName, email, phone, user_id))
conn.commit()
return jsonify({"success": True, "message": "Profile updated"}), 200
except Exception as e:
conn.rollback()
print("UPDATE ERROR:", e)
return jsonify({"success": False, "message": "Server error"}), 500
finally:
cursor.close()
conn.close()
if __name__ == "__main__":
app.run(debug=True)
Create a .env file:
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=yourpassword
DB_NAME=task_db
Create app.py:
Step 3 — Setting Up the React Frontend
Install the required packages:
bashnpm install react-router-dom
Step 4 — Login Page
The login page handles user authentication, stores the userId in localStorage, and displays Flask error messages directly in the UI.
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import "./login.css";
import Chicken from "../../assets/chicken.png";
export default function Login() {
const navigate = useNavigate();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const handleLogin = async () => {
try {
const res = await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password })
});
const data = await res.json();
if (!data.success) {
setError(data.message);
return;
}
localStorage.setItem("userId", data.user.id);
navigate("/profile");
} catch (err) {
setError("Something went wrong.");
}
};
return (
<div className="body">
<div className="loginContainer">
<div className="mainLoginContainer">
<h1 className="loginHeader">Login</h1>
<div className="loginTop">
<input
className="email"
placeholder="Email"
value={email}
type="email"
onChange={(e) => setEmail(e.target.value)}
/>
<input
className="password"
placeholder="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
{error && <div className="errorPopup">{error}</div>}
<div className="loginBottom">
<button className="loginSubmit" onClick={handleLogin}>
Login
</button>
</div>
</div>
</div>
<div className="imgContainer">
<img src={Chicken} alt="chicken" className="chickenImg" />
</div>
</div>
);
}
Key points:
- Uses a pattern with e.preventDefault() to handle submission.
- All validation (password length, matching passwords, duplicate email) is handled server-side in Flask and returned as error messages.
- On success, userId is saved to localStorage and the user is redirected to /profile.
Step 6 — Profile Page
The profile page fetches the logged-in user's data using the stored userId, displays a product carousel, and shows a loyalty card.
import { useState, useEffect } from "react";
import { products } from "../../data/products.js";
import { useNavigate } from "react-router-dom";
import "../profile/profile.css";
import ProductCard from "../../components/productCard/productCard.jsx";
import Cow from "../../assets/cow.svg";
export default function Profile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [currentIndex, setCurrentIndex] = useState(0);
const navigate = useNavigate();
const nextSlide = () => {
setCurrentIndex((prev) => (prev + 2 >= products.length ? 0 : prev + 2));
};
const prevSlide = () => {
setCurrentIndex((prev) => {
if (prev === 0) {
return products.length % 2 === 0 ? products.length - 2 : products.length - 1;
}
return prev - 2;
});
};
const handleAddToBasket = (product) => {
console.log("Added to basket:", product.name);
};
useEffect(() => {
const storedUserId = localStorage.getItem("userId");
if (!storedUserId) {
setLoading(false);
return;
}
fetch(`/api/user/profile?userId=${storedUserId}`)
.then((res) => res.json())
.then((data) => {
if (data.success) {
setUser(data.user);
}
})
.catch((err) => console.error("Error:", err))
.finally(() => setLoading(false));
}, []);
if (loading) return <p>Loading...</p>;
if (!user) return <p>Please log in.</p>;
return (
<div className="profilePage">
<div className="topSection">
<div className="welcomeBox">
<h2>Welcome {user.firstName}!</h2>
<p className="welcomeDesc">Recommended products for you</p>
<div className="carouselContainer">
<button className="carouselArrow" onClick={prevSlide}>❮</button>
<div className="carouselWrapper" key={currentIndex}>
{products.slice(currentIndex, currentIndex + 2).map((product) => (
<ProductCard
key={product.id}
product={product}
onAdd={handleAddToBasket}
/>
))}
</div>
<button className="carouselArrow" onClick={nextSlide}>❯</button>
</div>
</div>
<div className="mainLoyalBox">
<h3 className="loyalTitle">Loyalty Card</h3>
<div className="loyaltyBox">
<div className="loyalLeft">
<h2 className="points">{user.userPoints || 400} Points</h2>
<p className="loyalDesc">Points are added when you shop online</p>
<div className="progressWrapper">
<h3 className="rewardText">Next Reward:</h3>
<p className="progressText">1000 points: 50% Discount</p>
<button className="continueShopBtn" onClick={() => navigate("/products")}>
Continue Shopping
</button>
</div>
</div>
<div className="loyalRight">
<img src={Cow} alt="Loyalty Mascot" />
</div>
</div>
</div>
</div>
<div className="bottomSection">
<div className="banner">
<div className="bannerRight">
<div className="bannerRightTop">
<h2>Shop with our best deals</h2>
</div>
<div className="bannerRightBottom">
<img />
</div>
</div>
<div className="bannerLeft"></div>
</div>
</div>
</div>
);
}
Example of CSS for profile
.profilePage{
display: flex;
flex-direction: column;
}
.topSection{
display: flex;
flex-direction: row;
justify-content: space-evenly;
margin:2rem 1rem;
}
.welcomeBox{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: var(--primary);
border-radius: 15px;
}
.welcomeBox h2{
color: var(--text-h);
font-size: 3rem;
}
.welcomeDesc{
margin:0px;
font-size: 1.3rem;
}
.carouselContainer {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
margin-top: 20px;
padding: 20px;
}
.carouselArrow {
background: var(--primary);
color: var(--text);
border: none;
border-radius: 50%;
width: 35px;
height: 35px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
transition: background 0.2s;
}
.carouselArrow:hover {
background: var(--accent);
}
.carouselWrapper {
display: flex;
gap: 20px;
justify-content: center;
align-items: flex-start;
min-width: 450px;
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.carouselWrapper {
animation: slideInRight 0.3s ease-in-out;
}
.mainLoyalBox{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.loyalTitle{
font-size: 2rem;
margin: 1rem;
color: var(--text-h-2);
}
.loyaltyBox{
display: flex;
flex-direction: row;
background-color: var(--secondary);
border-radius: 15px;
padding: 20px;
padding-right: 0px;
}
.points{
margin:20px;
font-size: 3.5rem;
color: var(--text-h-2);
}
.loyalDesc{
margin:20px;
}
.rewardText, .progressText{
font-size: 1.5rem;
margin: 10px;
}
.continueShopBtn{
background-color: #D8EBED;
color: var(--text-h-2);
font-family: var(--main-font);
font-size: 1.2rem;
border: none;
border-radius: 7px;
padding:10px;
margin-top: 1.5rem;
margin-left: 8rem;
}
.loyalRight img{
width: 10rem;
}
.banner{
display: flex;
align-items: center;
justify-content: space-evenly;
background-color: var(--accent);
}
Key points:
- localStorage.getItem("userId") retrieves the stored ID on mount.
- The useEffect runs once on mount ([] dependency array) and fetches the user's profile.
- loading state prevents rendering before the fetch completes.
- user.userPoints || 400 displays a default of 400 points if the field is not yet in the database.
- The carousel uses currentIndex to slice 2 products at a time from the products array.
How It All Connects
User fills Sign Up / Login form
↓
React sends POST to Flask (/api/register or /api/login)
↓
Flask validates → queries MySQL → returns { success, user }
↓
React saves userId to localStorage
↓
Profile page reads userId from localStorage
↓
React sends GET to Flask (/api/user/profile?userId=...)
↓
Flask queries MySQL → returns user object
↓
React renders Welcome, Carousel, Loyalty Card
Running the App
Backend:
bashcd backend
python app.py
Frontend:
bashcd frontend
npm run dev
Make sure your Vite config proxies API calls to Flask:
Conclusion
In this tutorial we built a full-stack authentication system using React and Flask. We covered secure password hashing with Werkzeug, MySQL database integration, localStorage for session persistence, and a dynamic profile page with a product carousel and loyalty card.
Top comments (0)