A full-stack e-commerce webstore template built with a Flask (Python) backend and a React + Vite frontend. It ships with product/service browsing, a localStorage cart, authentication, an admin panel, a contact form, and PayPal checkout — all wired together out of the box.
⚠️ Template status. This is a working starting point, not a production-hardened shop. See Security & known limitations andSECURITY.mdbefore deploying.
- Tech stack
- Repository layout
- Prerequisites
- Backend setup
- Frontend setup
- One-command start
- Environment variables
- Database
- API reference
- Frontend routes
- Features
- Scripts
- Security & known limitations
| Layer | Technology |
|---|---|
| Backend | Python 3.10+, Flask 3, Flask-Cors, SQLite (stdlib sqlite3), python-dotenv, colorama |
| Images | Pillow — required: uploads are re-encoded and metadata-stripped |
| Frontend | React 19, Vite 7, Tailwind CSS 4 (+ typography plugin), React Router 7, Axios, lucide-react |
| Content | react-markdown + remark-gfm, sanitized with rehype-sanitize (front/src/lib/markdown.js) |
| Auth | PBKDF2-HMAC-SHA256 (200 000 iterations, 16-byte salt), HMAC-SHA256 signed bearer tokens |
| Payments | PayPal Orders API v2 (raw urllib, no SDK), sandbox or live |
Webstore-Template/
├── back/ # Flask backend
│ ├── server.py # App entry point + every API route
│ ├── init_db.py # Schema creation/migration + demo-catalog seeding
│ ├── requirements.txt # Python dependencies
│ ├── .env.example # Template for the required environment variables
│ ├── shop.db # created at runtime (gitignored)
│ ├── server.log # created at runtime (gitignored)
│ └── static/uploads/ # uploaded images (gitignored, created at runtime)
├── front/ # React (Vite) frontend
│ ├── src/
│ │ ├── components/ # Navbar
│ │ ├── pages/ # One component per route (Home, Products, Admin, …)
│ │ ├── lib/markdown.js # Hardened rehype-sanitize schema
│ │ └── api/axios.js # Shared Axios instance (not imported anywhere yet)
│ ├── README.md # Frontend-specific notes
│ ├── index.html
│ ├── vite.config.js
│ └── package.json
├── start.cmd # Windows helper: starts backend + frontend
├── SECURITY.md # Audit report, remediations and re-tests
└── .gitignore
- Node.js 18+ (developed against Node 24) and npm
- Python 3.10+ (developed against Python 3.14) — 3.10 is the floor because the code uses
X | Nonetype unions - Optional: a PayPal developer account for live checkout testing
Commands are shown for Windows PowerShell (on macOS/Linux activate the venv with source .venv/bin/activate):
# 1. Create and activate a virtual environment (from the repository root)
python -m venv .venv
.\.venv\Scripts\Activate.ps1
# 2. Install dependencies
pip install -r back/requirements.txt
# 3. Configure the environment
Copy-Item back\.env.example back\.env
# Generate a SECRET_KEY and paste it into back\.env (SECRET_KEY=...):
python -c "import secrets; print(secrets.token_hex(32))"
# 4. Start the API server, from the back/ directory
cd back
python server.pyThe backend listens on http://127.0.0.1:5000. It refuses to start if SECRET_KEY or MAX_UPLOAD_MB is missing, and prints a message instead.
📁 Working directory matters.
shop.db,server.logandstatic/uploads/are created relative to the current working directory, so run the server fromback/(asstart.cmddoes).back/.envis found either way, because python-dotenv resolves it relative toserver.py.
💡 First launch: the first run
- seeds a temporary admin account (
admin/admin) — log in once with it and you are redirected to/setup, where you create your own admin account (the temporary one is deleted in that step), and- seeds a demo catalog (8 products with 15 images + 5 services with 6 images, using
picsum.photosplaceholders and Markdown descriptions) so the shop never looks empty. Delete or edit these in the Admin panel to start with your own content — real data is never overwritten, and the catalog is only ever seeded into a brand-new database file.
From the repository root:
cd front
npm install
npm run devThe Vite dev server runs on http://localhost:5173 and calls the API at http://127.0.0.1:5000/api. The API base URL is currently a hard-coded const in each page (Login.jsx, Admin.jsx, Checkout.jsx, …) — front/src/api/axios.js holds a shared instance for that purpose but no page imports it yet.
On Windows you can start both servers at once:
.\start.cmdThis launches the backend (py server.py from back/), the frontend (npm run dev from front/) in separate windows, and opens the site in Brave (the browser is hard-coded, and the py launcher must be on PATH).
All backend configuration lives in back/.env (see back/.env.example).
| Variable | Required | Default | Description |
|---|---|---|---|
SECRET_KEY |
✅ | – | Secret used to sign auth tokens (use a long random value) |
MAX_UPLOAD_MB |
✅ | – | Max image upload size in megabytes |
ALLOWED_ORIGINS |
❌ | http://localhost:5173,http://127.0.0.1:5173 |
Comma-separated CORS allow-list for the API |
ENABLE_DEV_DISCOUNT_CODES |
❌ | 0 |
Set 1 to enable the DEV10/STUDENT15/SAVE5 dev codes server-side |
PAYPAL_CLIENT_ID |
❌ | (empty) | PayPal client ID; empty disables PayPal |
PAYPAL_CLIENT_SECRET |
❌ | (empty) | PayPal client secret |
PAYPAL_ENV |
❌ | sandbox |
sandbox or live |
CURRENCY |
❌ | EUR |
Three-letter ISO currency code |
ADMIN_USERNAME |
❌ | LeonBoussen |
Legacy: username promoted to admin only when migrating a pre-role database |
ADMIN_PASSWORD |
❌ | (empty) | Legacy variable, read at startup but unused |
ALLOW_UPLOADS_WITHOUT_EXIF_REMOVED is not an environment variable — it is a hard-coded False constant in server.py (see Security & known limitations).
Auth model. Passwords are PBKDF2-hashed with a per-user random salt and must be 8–128 characters (enforced server-side). Users log in with their email or username. Tokens are base64url(payload).base64url(HMAC-SHA256 signature) over {user_id, role, exp}, valid for 7 days, and are sent as Authorization: Bearer <token>. Admin rights come from the account's role column — never from its username.
First launch. A database with no admin at all is seeded with a temporary admin account (admin / admin, flagged is_setup_admin=1). Logging in with it returns setup_pending: true; POST /api/auth/setup then creates your own admin account and deletes the temporary one in the same transaction, so admin/admin stops working immediately. Regular signups always create role user. ADMIN_USERNAME is only used as a one-time migration for databases created before roles existed.
SQLite (shop.db), created and migrated by init_db.py on every start — adding missing columns and moving legacy products.image_path / services.image_path rows into the image tables.
| Table | Used by the API | Notes |
|---|---|---|
products |
✅ | almost_sold_out exists in the schema but is never read or written |
services |
✅ | Only rows with active=1 are returned by GET /api/services |
product_images |
✅ | Ordered by sort_order, then id; cascade-deleted with its product |
service_images |
✅ | Same shape as product_images |
users |
✅ | role (user/admin), is_setup_admin, phone, address, preferred_payment |
contact_messages |
✅ | Contact-form submissions with a created_at timestamp |
orders |
❌ | Scaffolded only — no endpoint writes here yet |
order_items |
❌ | Scaffolded only |
discount_codes |
❌ | Scaffolded only; checkout uses the hard-coded dev codes instead |
All API endpoints are prefixed with /api; uploaded images are served from /static/uploads/. Admin routes require a bearer token whose account has the admin role; user routes require any valid token. Rate limits are per client IP and in-memory (per process).
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/catchphrase |
– | Random marketing tagline |
| GET | /api/products |
– | List products (first image as image_url) |
| GET | /api/products/<id> |
– | Single product + images[] gallery |
| POST | /api/products |
admin | Create product (name, price required; images[] optional) |
| PUT | /api/products/<id> |
admin | Update product fields and/or replace images[] |
| DELETE | /api/products/<id> |
admin | Delete product (images cascade) |
| GET | /api/services |
– | List active services |
| POST | /api/services |
admin | Create service |
| PUT | /api/services/<id> |
admin | Update service fields and/or replace images[] |
| DELETE | /api/services/<id> |
admin | Delete service (images cascade) |
| POST | /api/auth/signup |
– | Register (always role user); 10 req / 15 min |
| POST | /api/auth/login |
– | Log in with email or username; 10 req / 15 min |
| GET | /api/auth/me |
user | Current user (id, email, username, role, setup_pending) |
| GET | /api/auth/setup-status |
– | Whether the first-launch setup is still pending |
| POST | /api/auth/setup |
setup admin | Create the owner's admin account & delete the temporary one |
| GET | /api/user/profile |
user | Get profile (id, email, username, address) |
| PUT | /api/user/profile |
user | Update email/address and/or change password (current_password required) |
| POST | /api/upload/image |
admin | Upload a product/service image (multipart field image) |
| POST | /api/contact |
– | Submit a contact-form message; 5 req / hour |
| GET | /api/paypal/config |
– | PayPal client config for the JS SDK (500 when unconfigured) |
| POST | /api/paypal/create-order |
– | Create a PayPal order from {items:[{id,kind,qty}], discount_code} |
| POST | /api/paypal/capture-order |
– | Capture an approved order ({order_id}) |
| GET | /static/uploads/<filename> |
– | Serve uploaded images |
Authentication endpoints return { token, user_id, role } (login adds setup_pending), and the token must be sent as Authorization: Bearer <token>. Validations: password 8–128 chars, username 3–32, email format + ≤254 chars, address ≤2000 chars, contact name ≤100 / message ≤5000. Requests above a rate limit get 429; wrong credentials always get a generic 401 {"error":"invalid credentials"}, and duplicate signups a generic 409.
POST /api/upload/image accepts .png, .jpg, .jpeg, .webp, rejects files above MAX_UPLOAD_MB with 413, and returns 503 if Pillow is unavailable on the server (never storing an unsanitized file). Otherwise the image is re-encoded with Pillow — EXIF/metadata dropped, downscaled to at most 1600 px wide — written as <unix-timestamp>_<sanitized-name> into static/uploads/, and returned as a /static/uploads/<filename> URL.
| Path | Page |
|---|---|
/ |
Home (fetches a random catchphrase) |
/products |
Product/service catalog + cart drawer |
/product/:id |
Product details (image gallery, sanitized Markdown) |
/about |
About |
/contact |
Contact form |
/addproduct |
Legacy quick single-product form (one image URL per line; the API still enforces the admin role) |
/login |
Login (redirects to /setup when setup is pending) |
/signup |
Sign up |
/setup |
First-launch wizard (create your admin account) |
/admin |
Admin panel (products/services CRUD, multi-image upload) |
/account |
Profile & password management |
/checkout |
Checkout (PayPal buttons + simulated payment UI for other methods) |
* |
Built-in 404 page |
- Catalog — products and services with multi-image galleries, sanitized Markdown descriptions, discounted prices, "limited edition" and "sold out" badges, plus search/filtering in the admin panel.
- Cart — client-side cart persisted in
localStorage(keycart); items carry{id, kind, qty}wherekindisproductorservice. - Auth — signup/login with email or username, PBKDF2-hashed passwords (8+ chars, server-enforced), signed 7-day bearer tokens, per-IP rate limiting on login/signup/contact. First launch seeds a temporary
admin/adminaccount that you replace via/setup. - Admin — role-based CRUD for products and services with image reordering (move up/down), multi-image upload, and a role check against
/api/auth/me(the temporary setup admin is redirected to/setup). - Checkout — server-side amount calculation (prices are re-read from the database, so the client cannot set them) and PayPal Orders API v2 integration. Dev discount codes
DEV10(10%),STUDENT15(15%),SAVE5(€5) are honoured by the server only whenENABLE_DEV_DISCOUNT_CODES=1. - Contact — validated, rate-limited form submissions stored in the
contact_messagestable.
Frontend (front/): there is no backend build step, and no automated test suite in either half.
| Command | Description |
|---|---|
npm run dev |
Start the Vite dev server |
npm run build |
Production build to dist/ |
npm run preview |
Preview the production build |
npm run lint |
Run ESLint |
The repo ships with the following hardening (see also SECURITY.md for the full audit report with before/after tests):
- Role-based admin (
users.role); the only bootstrap is the temporary setup account, and the legacy username-based promotion runs once, when a pre-role database is migrated. - No credentials, tokens or password hashes in
server.log; per-IP rate limits on login, signup and contact. - CORS restricted to the
ALLOWED_ORIGINSallow-list;X-Content-Type-Options,X-Frame-OptionsandReferrer-Policyon all API responses. - Server-side validation: password length, email format, and length caps on auth, profile and contact payloads.
- Markdown product/service descriptions are sanitized (
rehype-sanitize) before rendering. - Image uploads are refused outright unless Pillow is installed, so files are always re-encoded and metadata-stripped.
- Dev discount codes are off by default (
ENABLE_DEV_DISCOUNT_CODES).
Known limitations (accepted for a template, revisit before going live):
- Non-PayPal payment methods (Stripe, iDEAL, Crypto) are UI placeholders — "Place order" only logs the payload and simulates success. No order is persisted anywhere; the
orderstable is unused. - The checkout discount box is client-side.
Checkout.jsxappliesDEV10/STUDENT15/SAVE5for display regardless ofENABLE_DEV_DISCOUNT_CODES, so with the flag off the summary can show a lower total than PayPal actually charges (the server recomputes and ignores the code). Wire the box to the server before using it in production. - Auth token is stored client-side (
localStorage, keyuserToken); it is neither refreshed nor revocable server-side. An HttpOnly-session design would further reduce XSS impact. - Rate limits are in-memory (per process) — fine for a single dev server; a shared store (e.g. Redis) is needed when scaling.
- The Flask server binds to
127.0.0.1— expose it behind a reverse proxy with TLS for anything public. - The API base URL is duplicated as a literal in the frontend pages instead of using
front/src/api/axios.js, andALLOW_UPLOADS_WITHOUT_EXIF_REMOVEDis a hard-coded constant rather than a setting. - The "restart" loop around
app.run()inserver.pynever actually loops (itsfinallyblock breaks immediately), which Python flags with aSyntaxWarning.