By the end of this tutorial you will have a working full-stack e-commerce application with:
- A Flask REST API (products, auth, orders, admin)
- A React + Vite frontend (shop, cart, account, orders, admin dashboard)
- bcrypt password hashing
- SQLite database (zero config, runs locally)
- A cart persisted in
localStorage
No prior Flask or React experience is assumed, but you should be comfortable running commands in a terminal.
Table of Contents
- Project Structure
- Backend — Flask
- Frontend — React + Vite
- Running the app
- Security checklist before going live
Project Structure
ecommerce/
├── app.py ← entire Flask backend
├── shop.db ← SQLite DB (auto-created on first run)
└── frontend/
├── index.html
└── src/
├── api.js
├── main.jsx
├── App.jsx
└── pages/
├── Home.jsx
├── Shop.jsx
├── Cart.jsx
├── Account.jsx
├── Orders.jsx
└── Admin.jsx
Backend — Flask
Install & bootstrap
pip install flask flask-cors bcrypt
Create app.py and add the imports and app object:
from flask import Flask, request, jsonify
from flask_cors import CORS
import sqlite3
import bcrypt
from datetime import datetime
app = Flask(__name__)
CORS(app) # allows the React dev server (different port) to call the API
DB = "shop.db"
flask-cors is required because the React dev server runs on localhost:5173 while Flask runs on localhost:5000 — the browser blocks cross-origin requests unless the server sends the right headers.
Database setup
SQLite creates the file automatically. The helper db() returns a connection with row_factory set so rows behave like dictionaries.
def db():
conn = sqlite3.connect(DB)
conn.row_factory = sqlite3.Row
return conn
def init_db():
conn = db()
cur = conn.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT,
email TEXT UNIQUE,
password TEXT,
first_name TEXT,
last_name TEXT,
role TEXT DEFAULT 'user'
)""")
cur.execute("""
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
price REAL,
stock INTEGER,
image TEXT
)""")
cur.execute("""
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
total REAL,
status TEXT DEFAULT 'pending',
created_at TEXT
)""")
cur.execute("""
CREATE TABLE IF NOT EXISTS order_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER,
product_id INTEGER,
qty INTEGER,
price REAL
)""")
conn.commit()
conn.close()
init_db()
| Table | What it stores |
|---|---|
users |
Accounts with hashed passwords and a role field (user or admin) |
products |
Catalogue — name, price, stock count, image URL |
orders |
One row per placed order, linked to a user |
order_items |
Line items (product, quantity, price snapshot at time of order) |
Auth routes
POST /register
The password is hashed with bcrypt before it ever reaches the database. Parameterised queries (?) protect against SQL injection.
@app.route("/register", methods=["POST"])
def register():
d = request.json
hashed = bcrypt.hashpw(d["password"].encode(), bcrypt.gensalt())
conn = db()
conn.execute("""
INSERT INTO users(username, email, password, first_name, last_name)
VALUES (?, ?, ?, ?, ?)
""", (d["username"], d["email"], hashed.decode(), d["first_name"], d["last_name"]))
conn.commit()
conn.close()
return jsonify({"msg": "registered"})
POST /login
bcrypt.checkpw compares the plain-text input against the stored hash. The API returns user_id, username, and role — the frontend stores this in localStorage.
@app.route("/login", methods=["POST"])
def login():
d = request.json
conn = db()
user = conn.execute("SELECT * FROM users WHERE email=?", (d["email"],)).fetchone()
conn.close()
if not user:
return jsonify({"error": "not found"}), 404
if not bcrypt.checkpw(d["password"].encode(), user["password"].encode()):
return jsonify({"error": "wrong password"}), 401
return jsonify({
"user_id": user["id"],
"username": user["username"],
"role": user["role"]
})
Product routes
# List all products
@app.route("/products", methods=["GET"])
def get_products():
conn = db()
rows = conn.execute("SELECT * FROM products").fetchall()
conn.close()
return jsonify([dict(r) for r in rows])
# Add a product (should be admin-only in production)
@app.route("/products", methods=["POST"])
def add_product():
d = request.json
conn = db()
conn.execute("""
INSERT INTO products(name, price, stock, image)
VALUES (?, ?, ?, ?)
""", (d["name"], d["price"], d["stock"], d["image"]))
conn.commit()
conn.close()
return jsonify({"msg": "added"})
# Delete a product (should be admin-only in production)
@app.route("/products/<int:pid>", methods=["DELETE"])
def delete_product(pid):
conn = db()
conn.execute("DELETE FROM products WHERE id=?", (pid,))
conn.commit()
conn.close()
return jsonify({"msg": "deleted"})
Checkout & orders
The checkout endpoint validates stock before writing anything, then creates the order, writes line items, and decrements stock in one commit.
@app.route("/checkout", methods=["POST"])
def checkout():
d = request.json
user_id = d["user_id"]
cart = d["cart"]
conn = db()
total = 0
# Validate stock first — fail before writing anything
for item in cart:
p = conn.execute("SELECT * FROM products WHERE id=?", (item["id"],)).fetchone()
if p["stock"] < item["qty"]:
return jsonify({"error": "out of stock"}), 400
total += p["price"] * item["qty"]
cur = conn.cursor()
cur.execute("""
INSERT INTO orders(user_id, total, created_at)
VALUES (?, ?, ?)
""", (user_id, total, datetime.now().isoformat()))
order_id = cur.lastrowid
for item in cart:
conn.execute("""
INSERT INTO order_items(order_id, product_id, qty, price)
VALUES (?, ?, ?, ?)
""", (order_id, item["id"], item["qty"], item["price"]))
conn.execute("""
UPDATE products SET stock = stock - ? WHERE id=?
""", (item["qty"], item["id"]))
conn.commit()
conn.close()
return jsonify({"msg": "order created"})
# Order history for a single user
@app.route("/orders/<int:user_id>")
def user_orders(user_id):
conn = db()
orders = conn.execute("SELECT * FROM orders WHERE user_id=?", (user_id,)).fetchall()
result = []
for o in orders:
items = conn.execute(
"SELECT * FROM order_items WHERE order_id=?", (o["id"],)
).fetchall()
result.append({"order": dict(o), "items": [dict(i) for i in items]})
conn.close()
return jsonify(result)
Admin routes
# All orders
@app.route("/admin/orders")
def admin_orders():
conn = db()
orders = conn.execute("SELECT * FROM orders").fetchall()
result = []
for o in orders:
items = conn.execute(
"SELECT * FROM order_items WHERE order_id=?", (o["id"],)
).fetchall()
result.append({"order": dict(o), "items": [dict(i) for i in items]})
conn.close()
return jsonify(result)
# Update order status
@app.route("/admin/orders/<int:oid>", methods=["PUT"])
def update_order(oid):
d = request.json
conn = db()
conn.execute("UPDATE orders SET status=? WHERE id=?", (d["status"], oid))
conn.commit()
conn.close()
return jsonify({"msg": "updated"})
# Delete an order and its line items
@app.route("/admin/orders/<int:oid>", methods=["DELETE"])
def delete_order(oid):
conn = db()
conn.execute("DELETE FROM order_items WHERE order_id=?", (oid,))
conn.execute("DELETE FROM orders WHERE id=?", (oid,))
conn.commit()
conn.close()
return jsonify({"msg": "deleted"})
Full backend code
Here is app.py in its entirety — copy this into a new file and run python app.py.
from flask import Flask, request, jsonify
from flask_cors import CORS
import sqlite3
import bcrypt
from datetime import datetime
app = Flask(__name__)
CORS(app)
DB = "shop.db"
# ── Database ──────────────────────────────────────────────────────────────────
def db():
conn = sqlite3.connect(DB)
conn.row_factory = sqlite3.Row
return conn
def init_db():
conn = db()
cur = conn.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT,
email TEXT UNIQUE,
password TEXT,
first_name TEXT,
last_name TEXT,
role TEXT DEFAULT 'user'
)""")
cur.execute("""
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
price REAL,
stock INTEGER,
image TEXT
)""")
cur.execute("""
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
total REAL,
status TEXT DEFAULT 'pending',
created_at TEXT
)""")
cur.execute("""
CREATE TABLE IF NOT EXISTS order_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER,
product_id INTEGER,
qty INTEGER,
price REAL
)""")
conn.commit()
conn.close()
init_db()
# ── Auth ──────────────────────────────────────────────────────────────────────
@app.route("/register", methods=["POST"])
def register():
d = request.json
hashed = bcrypt.hashpw(d["password"].encode(), bcrypt.gensalt())
conn = db()
conn.execute("""
INSERT INTO users(username, email, password, first_name, last_name)
VALUES (?, ?, ?, ?, ?)
""", (d["username"], d["email"], hashed.decode(), d["first_name"], d["last_name"]))
conn.commit()
conn.close()
return jsonify({"msg": "registered"})
@app.route("/login", methods=["POST"])
def login():
d = request.json
conn = db()
user = conn.execute("SELECT * FROM users WHERE email=?", (d["email"],)).fetchone()
conn.close()
if not user:
return jsonify({"error": "not found"}), 404
if not bcrypt.checkpw(d["password"].encode(), user["password"].encode()):
return jsonify({"error": "wrong password"}), 401
return jsonify({
"user_id": user["id"],
"username": user["username"],
"role": user["role"]
})
# ── Products ──────────────────────────────────────────────────────────────────
@app.route("/products", methods=["GET"])
def get_products():
conn = db()
rows = conn.execute("SELECT * FROM products").fetchall()
conn.close()
return jsonify([dict(r) for r in rows])
@app.route("/products", methods=["POST"])
def add_product():
d = request.json
conn = db()
conn.execute("""
INSERT INTO products(name, price, stock, image)
VALUES (?, ?, ?, ?)
""", (d["name"], d["price"], d["stock"], d["image"]))
conn.commit()
conn.close()
return jsonify({"msg": "added"})
@app.route("/products/<int:pid>", methods=["DELETE"])
def delete_product(pid):
conn = db()
conn.execute("DELETE FROM products WHERE id=?", (pid,))
conn.commit()
conn.close()
return jsonify({"msg": "deleted"})
# ── Checkout & orders ─────────────────────────────────────────────────────────
@app.route("/checkout", methods=["POST"])
def checkout():
d = request.json
user_id = d["user_id"]
cart = d["cart"]
conn = db()
total = 0
for item in cart:
p = conn.execute("SELECT * FROM products WHERE id=?", (item["id"],)).fetchone()
if p["stock"] < item["qty"]:
return jsonify({"error": "out of stock"}), 400
total += p["price"] * item["qty"]
cur = conn.cursor()
cur.execute("""
INSERT INTO orders(user_id, total, created_at)
VALUES (?, ?, ?)
""", (user_id, total, datetime.now().isoformat()))
order_id = cur.lastrowid
for item in cart:
conn.execute("""
INSERT INTO order_items(order_id, product_id, qty, price)
VALUES (?, ?, ?, ?)
""", (order_id, item["id"], item["qty"], item["price"]))
conn.execute("""
UPDATE products SET stock = stock - ? WHERE id=?
""", (item["qty"], item["id"]))
conn.commit()
conn.close()
return jsonify({"msg": "order created"})
@app.route("/orders/<int:user_id>")
def user_orders(user_id):
conn = db()
orders = conn.execute("SELECT * FROM orders WHERE user_id=?", (user_id,)).fetchall()
result = []
for o in orders:
items = conn.execute(
"SELECT * FROM order_items WHERE order_id=?", (o["id"],)
).fetchall()
result.append({"order": dict(o), "items": [dict(i) for i in items]})
conn.close()
return jsonify(result)
# ── Admin ─────────────────────────────────────────────────────────────────────
@app.route("/admin/orders")
def admin_orders():
conn = db()
orders = conn.execute("SELECT * FROM orders").fetchall()
result = []
for o in orders:
items = conn.execute(
"SELECT * FROM order_items WHERE order_id=?", (o["id"],)
).fetchall()
result.append({"order": dict(o), "items": [dict(i) for i in items]})
conn.close()
return jsonify(result)
@app.route("/admin/orders/<int:oid>", methods=["PUT"])
def update_order(oid):
d = request.json
conn = db()
conn.execute("UPDATE orders SET status=? WHERE id=?", (d["status"], oid))
conn.commit()
conn.close()
return jsonify({"msg": "updated"})
@app.route("/admin/orders/<int:oid>", methods=["DELETE"])
def delete_order(oid):
conn = db()
conn.execute("DELETE FROM order_items WHERE order_id=?", (oid,))
conn.execute("DELETE FROM orders WHERE id=?", (oid,))
conn.commit()
conn.close()
return jsonify({"msg": "deleted"})
# ── Run ───────────────────────────────────────────────────────────────────────
app.run(debug=True)
Frontend — React + Vite
Create the project
npm create vite@latest frontend
cd frontend
npm install
npm install react-router-dom
npm run dev
The dev server starts on http://localhost:5173.
App shell
src/api.js
One place to change if you deploy the backend to a real server.
export const API = "http://127.0.0.1:5000";
src/main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
src/App.jsx
Sets up the router. The Admin link is only rendered when the stored user has role === "admin".
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
import Home from "./pages/Home.jsx";
import Shop from "./pages/Shop.jsx";
import Cart from "./pages/Cart.jsx";
import Account from "./pages/Account.jsx";
import Orders from "./pages/Orders.jsx";
import Admin from "./pages/Admin.jsx";
export default function App() {
const user = JSON.parse(localStorage.getItem("user") || "{}");
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link>{" | "}
<Link to="/shop">Shop</Link>{" | "}
<Link to="/cart">Cart</Link>{" | "}
<Link to="/account">Account</Link>{" | "}
<Link to="/orders">Orders</Link>{" | "}
{user.role === "admin" && <Link to="/admin">Admin</Link>}
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/shop" element={<Shop />} />
<Route path="/cart" element={<Cart />} />
<Route path="/account" element={<Account />} />
<Route path="/orders" element={<Orders />} />
<Route path="/admin" element={<Admin />} />
</Routes>
</BrowserRouter>
);
}
Pages
src/pages/Home.jsx
export default function Home() {
return (
<div>
<h1>Welcome Store</h1>
<p>Best products online</p>
</div>
);
}
src/pages/Shop.jsx
Fetches products on mount. "Add to cart" pushes into localStorage, incrementing qty if the item already exists.
import { useEffect, useState } from "react";
import { API } from "../api";
export default function Shop() {
const [products, setProducts] = useState([]);
useEffect(() => {
fetch(API + "/products")
.then(r => r.json())
.then(setProducts);
}, []);
const add = (p) => {
const cart = JSON.parse(localStorage.getItem("cart") || "[]");
const found = cart.find(i => i.id === p.id);
if (found) found.qty++;
else cart.push({ ...p, qty: 1 });
localStorage.setItem("cart", JSON.stringify(cart));
};
return (
<div>
{products.map(p => (
<div key={p.id}>
<h3>{p.name} — £{p.price}</h3>
<p>In stock: {p.stock}</p>
<button onClick={() => add(p)}>Add to cart</button>
</div>
))}
</div>
);
}
src/pages/Cart.jsx
Reads the cart from localStorage, displays items, and sends the full cart to POST /checkout. The cart is wiped from storage after a successful order.
import { API } from "../api";
export default function Cart() {
const cart = JSON.parse(localStorage.getItem("cart") || "[]");
const user = JSON.parse(localStorage.getItem("user") || "{}");
const checkout = async () => {
const res = await fetch(API + "/checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user_id: user.user_id, cart })
});
const data = await res.json();
if (res.ok) {
localStorage.removeItem("cart");
alert("Order placed!");
} else {
alert(data.error);
}
};
if (cart.length === 0) return <p>Your cart is empty.</p>;
return (
<div>
{cart.map(i => (
<div key={i.id}>
{i.name} x{i.qty} — £{(i.price * i.qty).toFixed(2)}
</div>
))}
<button onClick={checkout}>Checkout</button>
</div>
);
}
src/pages/Account.jsx
Handles login. The API response is stored in localStorage so user_id and role persist across page loads.
import { useState } from "react";
import { API } from "../api";
export default function Account() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [msg, setMsg] = useState("");
const login = async () => {
const res = await fetch(API + "/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password })
});
const data = await res.json();
if (res.ok) {
localStorage.setItem("user", JSON.stringify(data));
setMsg("Logged in as " + data.username);
} else {
setMsg(data.error);
}
};
return (
<div>
<h2>Login</h2>
<input
type="email"
placeholder="Email"
onChange={e => setEmail(e.target.value)}
/>
<input
type="password"
placeholder="Password"
onChange={e => setPassword(e.target.value)}
/>
<button onClick={login}>Login</button>
{msg && <p>{msg}</p>}
</div>
);
}
src/pages/Orders.jsx
Fetches the logged-in user's order history using their stored user_id.
import { useEffect, useState } from "react";
import { API } from "../api";
export default function Orders() {
const [orders, setOrders] = useState([]);
useEffect(() => {
const user = JSON.parse(localStorage.getItem("user") || "{}");
if (!user.user_id) return;
fetch(API + "/orders/" + user.user_id)
.then(r => r.json())
.then(setOrders);
}, []);
if (orders.length === 0) return <p>No orders yet.</p>;
return (
<div>
{orders.map(o => (
<div key={o.order.id}>
<strong>Order #{o.order.id}</strong> — £{o.order.total.toFixed(2)} — {o.order.status}
<ul>
{o.items.map(i => (
<li key={i.id}>Product #{i.product_id} × {i.qty}</li>
))}
</ul>
</div>
))}
</div>
);
}
src/pages/Admin.jsx
Fetches all orders. Admins can mark an order as "shipped" or delete it. In production this page should be behind a route guard that checks role.
import { useEffect, useState } from "react";
import { API } from "../api";
export default function Admin() {
const [orders, setOrders] = useState([]);
const load = () =>
fetch(API + "/admin/orders")
.then(r => r.json())
.then(setOrders);
useEffect(() => { load(); }, []);
const update = async (id, status) => {
await fetch(API + "/admin/orders/" + id, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status })
});
load();
};
const del = async (id) => {
await fetch(API + "/admin/orders/" + id, { method: "DELETE" });
load();
};
return (
<div>
<h2>All Orders</h2>
{orders.map(o => (
<div key={o.order.id} style={{ marginBottom: "1rem", borderBottom: "1px solid #ccc" }}>
<strong>Order #{o.order.id}</strong> — £{o.order.total.toFixed(2)} — {o.order.status}
<ul>
{o.items.map(i => (
<li key={i.id}>Product #{i.product_id} × {i.qty}</li>
))}
</ul>
<button onClick={() => update(o.order.id, "shipped")}>Mark shipped</button>{" "}
<button onClick={() => del(o.order.id)}>Delete</button>
</div>
))}
</div>
);
}
Full frontend code
All files together for easy copying.
src/api.js
export const API = "http://127.0.0.1:5000";
src/main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
src/App.jsx
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
import Home from "./pages/Home.jsx";
import Shop from "./pages/Shop.jsx";
import Cart from "./pages/Cart.jsx";
import Account from "./pages/Account.jsx";
import Orders from "./pages/Orders.jsx";
import Admin from "./pages/Admin.jsx";
export default function App() {
const user = JSON.parse(localStorage.getItem("user") || "{}");
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link>{" | "}
<Link to="/shop">Shop</Link>{" | "}
<Link to="/cart">Cart</Link>{" | "}
<Link to="/account">Account</Link>{" | "}
<Link to="/orders">Orders</Link>{" | "}
{user.role === "admin" && <Link to="/admin">Admin</Link>}
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/shop" element={<Shop />} />
<Route path="/cart" element={<Cart />} />
<Route path="/account" element={<Account />} />
<Route path="/orders" element={<Orders />} />
<Route path="/admin" element={<Admin />} />
</Routes>
</BrowserRouter>
);
}
src/pages/Home.jsx
export default function Home() {
return (
<div>
<h1>Welcome Store</h1>
<p>Best products online</p>
</div>
);
}
src/pages/Shop.jsx
import { useEffect, useState } from "react";
import { API } from "../api";
export default function Shop() {
const [products, setProducts] = useState([]);
useEffect(() => {
fetch(API + "/products").then(r => r.json()).then(setProducts);
}, []);
const add = (p) => {
const cart = JSON.parse(localStorage.getItem("cart") || "[]");
const found = cart.find(i => i.id === p.id);
if (found) found.qty++;
else cart.push({ ...p, qty: 1 });
localStorage.setItem("cart", JSON.stringify(cart));
};
return (
<div>
{products.map(p => (
<div key={p.id}>
<h3>{p.name} — £{p.price}</h3>
<p>In stock: {p.stock}</p>
<button onClick={() => add(p)}>Add to cart</button>
</div>
))}
</div>
);
}
src/pages/Cart.jsx
import { API } from "../api";
export default function Cart() {
const cart = JSON.parse(localStorage.getItem("cart") || "[]");
const user = JSON.parse(localStorage.getItem("user") || "{}");
const checkout = async () => {
const res = await fetch(API + "/checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user_id: user.user_id, cart })
});
const data = await res.json();
if (res.ok) { localStorage.removeItem("cart"); alert("Order placed!"); }
else alert(data.error);
};
if (cart.length === 0) return <p>Your cart is empty.</p>;
return (
<div>
{cart.map(i => (
<div key={i.id}>{i.name} x{i.qty} — £{(i.price * i.qty).toFixed(2)}</div>
))}
<button onClick={checkout}>Checkout</button>
</div>
);
}
src/pages/Account.jsx
import { useState } from "react";
import { API } from "../api";
export default function Account() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [msg, setMsg] = useState("");
const login = async () => {
const res = await fetch(API + "/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password })
});
const data = await res.json();
if (res.ok) { localStorage.setItem("user", JSON.stringify(data)); setMsg("Logged in as " + data.username); }
else setMsg(data.error);
};
return (
<div>
<h2>Login</h2>
<input type="email" placeholder="Email" onChange={e => setEmail(e.target.value)} />
<input type="password" placeholder="Password" onChange={e => setPassword(e.target.value)} />
<button onClick={login}>Login</button>
{msg && <p>{msg}</p>}
</div>
);
}
src/pages/Orders.jsx
import { useEffect, useState } from "react";
import { API } from "../api";
export default function Orders() {
const [orders, setOrders] = useState([]);
useEffect(() => {
const user = JSON.parse(localStorage.getItem("user") || "{}");
if (!user.user_id) return;
fetch(API + "/orders/" + user.user_id).then(r => r.json()).then(setOrders);
}, []);
if (orders.length === 0) return <p>No orders yet.</p>;
return (
<div>
{orders.map(o => (
<div key={o.order.id}>
<strong>Order #{o.order.id}</strong> — £{o.order.total.toFixed(2)} — {o.order.status}
<ul>{o.items.map(i => <li key={i.id}>Product #{i.product_id} × {i.qty}</li>)}</ul>
</div>
))}
</div>
);
}
src/pages/Admin.jsx
import { useEffect, useState } from "react";
import { API } from "../api";
export default function Admin() {
const [orders, setOrders] = useState([]);
const load = () => fetch(API + "/admin/orders").then(r => r.json()).then(setOrders);
useEffect(() => { load(); }, []);
const update = async (id, status) => {
await fetch(API + "/admin/orders/" + id, {
method: "PUT", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status })
});
load();
};
const del = async (id) => {
await fetch(API + "/admin/orders/" + id, { method: "DELETE" });
load();
};
return (
<div>
<h2>All Orders</h2>
{orders.map(o => (
<div key={o.order.id} style={{ marginBottom: "1rem", borderBottom: "1px solid #ccc" }}>
<strong>Order #{o.order.id}</strong> — £{o.order.total.toFixed(2)} — {o.order.status}
<ul>{o.items.map(i => <li key={i.id}>Product #{i.product_id} × {i.qty}</li>)}</ul>
<button onClick={() => update(o.order.id, "shipped")}>Mark shipped</button>{" "}
<button onClick={() => del(o.order.id)}>Delete</button>
</div>
))}
</div>
);
}
Running the app
Open two terminals:
# Terminal 1 — backend
python app.py
# → Running on http://127.0.0.1:5000
# Terminal 2 — frontend
cd frontend
npm run dev
# → Local: http://localhost:5173
To seed a test product, call the API directly:
curl -X POST http://127.0.0.1:5000/products \
-H "Content-Type: application/json" \
-d '{"name":"Widget","price":9.99,"stock":50,"image":"https://placehold.co/200"}'
To create an admin user, register normally then open the SQLite database and set the role:
sqlite3 shop.db "UPDATE users SET role='admin' WHERE email='you@example.com';"
Security checklist before going live
This tutorial is a learning scaffold. Here are the issues to fix before any real deployment:
| Issue | Fix |
|---|---|
| Admin routes are unprotected | Verify a JWT or session token on every /admin/* request and check role === "admin"
|
| Product write routes are unprotected | Same — admin check on POST /products and DELETE /products/<id>
|
user_id comes from the client at checkout |
Derive it server-side from the session token, not the request body |
localStorage for auth state |
Use httpOnly cookies to prevent XSS from reading the token |
| No input validation | Validate all JSON fields with a library like marshmallow or pydantic before writing to the DB |
| SQLite in production | Switch to PostgreSQL or MySQL; SQLite doesn't handle concurrent writes well |
debug=True in production |
Set debug=False and run behind Gunicorn: gunicorn app:app
|
Found a bug or want to add registration, search, or Stripe payments? Drop a comment below.
Top comments (0)