Ejemplo full stack de autenticación JWT con catálogo público y páginas privadas.
Este repositorio ya no es solo el boilerplate original: ahora implementa un flujo completo de sign_up, sign_in, me/profile, catálogo de productos, creación de órdenes y rutas protegidas en React. La referencia pedagógica usada para el enfoque fue day_28.
- Registro de usuario con contraseña hasheada.
- Login que retorna
access_tokeny datos del usuario. - Endpoint protegido
GET /api/mey aliasGET /api/profile. - Endpoint público
GET /api/products. - Endpoint protegido
GET /api/orderspara ver solo las órdenes del usuario autenticado. - Endpoint protegido
POST /api/orderspara crear compras autenticadas desde el front. - Nuevos modelos
ProductyOrder. - Seed de datos con usuarios demo, productos y órdenes iniciales.
- Página pública de catálogo en
/. - Páginas públicas
sign-inysign-up. - Páginas privadas
profileyorders. - Persistencia de sesión JWT en
localStorage. - Revalidación de la sesión al recargar la app.
PrivateRoutepara bloquear rutas si el usuario no está autenticado.- Compra desde la galería pública, pero creación real de la orden solo si existe sesión.
Visitante -> ve / y /api/products
Visitante -> sign-up o sign-in
Backend -> devuelve JWT + user
Frontend -> guarda token y user en localStorage
Usuario autenticado -> entra a /profile y /orders
Usuario autenticado -> compra producto desde /
Backend -> crea Order ligada al user_id del token
Usuario autenticado -> solo ve sus propias órdenes
| Método | Endpoint | Protección | Uso |
|---|---|---|---|
| GET | /api/hello |
Pública | Estado rápido de la API |
| POST | /api/sign-up |
Pública | Crear cuenta y devolver JWT |
| POST | /api/sign-in |
Pública | Iniciar sesión y devolver JWT |
| POST | /api/signup |
Pública | Alias del sign-up |
| POST | /api/signin |
Pública | Alias del sign-in |
| POST | /api/token |
Pública | Alias legacy del sign-in |
| GET | /api/me |
Protegida | Usuario autenticado |
| GET | /api/profile |
Protegida | Alias de /api/me |
| GET | /api/products |
Pública | Catálogo visible para cualquiera |
| GET | /api/orders |
Protegida | Órdenes del usuario autenticado |
| POST | /api/orders |
Protegida | Crear orden para un producto |
idemailpassword(guardada como hash)nameis_active
idnameslugdescriptioncategoryimage_urlpriceis_active
iduser_idproduct_idquantitystatusunit_pricecreated_at
Ejecuta estos comandos en este orden dentro de la raíz del proyecto:
python3 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
cp .env.example .envConfirma al menos estas variables:
FLASK_APP=src/app.py
FLASK_DEBUG=1
JWT_SECRET_KEY="super-secret-dev-key-change-me-please"
VITE_BACKEND_URL=http://127.0.0.1:3001flask --app src/app.py db upgradeEste comando limpia los datos demo anteriores y vuelve a insertar usuarios, productos y órdenes:
flask --app src/app.py insert-test-dataflask --app src/app.py run -p 3001npm install
npm run dev- Frontend:
http://127.0.0.1:3000 - Backend:
http://127.0.0.1:3001
Opción simple con venv:
python3 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
cp .env.example .envAsegúrate de definir en .env:
FLASK_APP=src/app.py
FLASK_DEBUG=1
JWT_SECRET_KEY="super-secret-dev-key-change-me-please"
VITE_BACKEND_URL=http://127.0.0.1:3001Si no defines DATABASE_URL, Flask usará SQLite en /tmp/test.db.
Luego:
flask --app src/app.py db upgrade
flask --app src/app.py insert-test-data
flask --app src/app.py run -p 3001npm install
npm run devlara@example.com / demo123diego@example.com / demo123
Puedes usar cualquiera de los dos, pero el recorrido más completo suele ser con:
lara@example.com / demo123
Ese usuario ya tiene órdenes cargadas en el seed, así que sirve para revisar inmediatamente:
/profile/orders- el comportamiento de compra desde
/
Si quieres comparar cómo cambia la experiencia entre usuarios, también puedes entrar con:
diego@example.com / demo123
- Abre
/y revisa la galería pública. - Intenta comprar sin sesión: el front redirige a
sign-in. - Entra con un usuario demo.
- Ve a
/profilepara comprobar la ruta protegida. - Ve a
/orderspara ver solo las órdenes del usuario autenticado. - Compra un producto desde
/y vuelve a/orderspara confirmar que se añadió.
- Abre DevTools en la pestaña Network.
- Observa
POST /api/sign-inoPOST /api/sign-up. - Verifica que luego
GET /api/meyGET /api/ordersllevanAuthorization: Bearer ....
Ver todas las rutas:
flask --app src/app.py routesResetear y reinsertar datos:
flask --app src/app.py insert-test-dataReaplicar migraciones en base limpia:
flask --app src/app.py db upgrade| Archivo | Qué mirar |
|---|---|
src/api/models.py |
Modelos User, Product, Order y serialización |
src/api/routes.py |
Auth, endpoints protegidos y creación de órdenes |
src/api/commands.py |
Seed de usuarios, productos y órdenes |
src/app.py |
Configuración de Flask, JWT, CORS y registro del blueprint |
migrations/versions/bd2f3a0b9b6d_add_products_and_orders.py |
Tablas nuevas de products y orders |
| Archivo | Qué mirar |
|---|---|
src/front/store.js |
Estado global, persistencia de sesión y reducer |
src/front/hooks/useGlobalReducer.jsx |
Context provider + sincronización con localStorage |
src/front/services/api.js |
Wrapper de fetch y headers de auth |
src/front/components/PrivateRoute.jsx |
Protección de rutas con redirect a sign-in |
src/front/pages/Layout.jsx |
Revalidación de sesión al montar la app |
src/front/routes.jsx |
Registro de rutas públicas y privadas |
src/front/pages/Home.jsx |
Catálogo público y compra autenticada |
src/front/pages/SignIn.jsx |
Login |
src/front/pages/SignUp.jsx |
Registro |
src/front/pages/Profile.jsx |
Vista privada del usuario |
src/front/pages/Orders.jsx |
Vista privada de órdenes |
Toda la API vive en src/api/routes.py bajo el blueprint api, luego se registra en src/app.py con prefijo /api.
El hash y la validación de contraseña se encapsulan en User.set_password() y User.check_password(). Esto evita repetir lógica de seguridad en cada endpoint.
El token guarda solo el user.id como identidad. Después, el backend reconstruye el usuario real leyendo get_jwt_identity() y consultando la base de datos.
Las órdenes no se consultan usando un user_id enviado por el cliente. El backend toma el usuario desde el JWT y filtra:
orders = Order.query.filter_by(user_id=user.id).all()Ese patrón evita que un usuario pueda pedir datos de otro usuario manipulando la request.
Order.unit_price guarda el precio del producto en el momento de la compra. Así, si el producto cambia de precio después, la orden mantiene el valor histórico.
La carga de datos vive en un comando Flask (insert-test-data) y no en código ad hoc. Esto hace el entorno reproducible y rápido de resetear.
La sesión, productos, órdenes, errores y estados de carga viven en un store global basado en useReducer.
Esto centraliza cambios como:
- guardar sesión
- cerrar sesión
- persistir JWT
- cargar catálogo
- cargar órdenes
- manejar mensajes y errores
Al iniciar la app se intenta reconstruir token y user desde localStorage. Después, Layout.jsx revalida esa sesión contra /api/me.
Esto permite:
- mantener la sesión entre recargas
- invalidarla correctamente si el token ya no sirve
El front usa un wrapper dedicado para separar:
- rutas públicas:
/,/sign-in,/sign-up - rutas privadas:
/profile,/orders
Si no hay token válido, el usuario es redirigido automáticamente a sign-in.
En vez de repetir fetch manual en cada página, existe un wrapper en src/front/services/api.js que:
- construye la URL base
- añade headers consistentes
- parsea JSON
- lanza errores con mensaje usable
La home es pública, pero la compra real es un buen ejemplo de “acción protegida sobre vista pública”:
- cualquiera ve productos
- solo un usuario autenticado puede crear la orden
Se validó el ejemplo con:
npm run lintnpm run buildpython3 -m compileall srcflask --app src/app.py db upgradeflask --app src/app.py insert-test-data- prueba end-to-end con
Flask test_clientsobreproducts,sign-up,me,create orderyorders
- Añadir
logoutautomático al expirar el token con manejo visual más explícito. - Crear
DELETE /api/orders/:idsi quieres practicar ownership de recursos. - Separar auth en un módulo propio si el proyecto crece.
- Añadir tests unitarios o de integración para el flujo JWT.