feat: agregar profile y aplanar submódulo
This commit is contained in:
parent
8906d7a970
commit
d6c48759db
22 changed files with 1664 additions and 5 deletions
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -1,3 +0,0 @@
|
|||
[submodule "backend"]
|
||||
path = backend
|
||||
url = ssh://git@git.silvertke.net:2222/SilverTke/backend-pizza-dela.git
|
||||
1
backend
1
backend
|
|
@ -1 +0,0 @@
|
|||
Subproject commit d86ee9ce553a0421211a48bacce8a2ea49b69c5d
|
||||
1
backend/.env
Normal file
1
backend/.env
Normal file
|
|
@ -0,0 +1 @@
|
|||
JWT_SECRET=increiblementeSecreto
|
||||
1
backend/.gitignore
vendored
Normal file
1
backend/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
node_modules
|
||||
94
backend/README.md
Normal file
94
backend/README.md
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# Simple API JWT
|
||||
|
||||
API para consumir un servicio de Auth con JWT.
|
||||
|
||||
## Instalación
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
## Uso
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Endpoints
|
||||
|
||||
### Pizzas
|
||||
|
||||
```sh
|
||||
GET /api/pizzas
|
||||
```
|
||||
|
||||
### Pizza (única)
|
||||
|
||||
```sh
|
||||
GET /api/pizzas/:id
|
||||
```
|
||||
|
||||
### Auth
|
||||
|
||||
```sh
|
||||
POST /api/auth/login
|
||||
POST /api/auth/register
|
||||
```
|
||||
|
||||
body:
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "test@example.com",
|
||||
"password": "123123"
|
||||
}
|
||||
```
|
||||
|
||||
### Checkout & Profile
|
||||
|
||||
Esta ruta requiere un token JWT en el header, el token se obtiene en el endpoint de Auth explicado en el item siguiente (JWT).
|
||||
|
||||
Además puedes enviar un carrito con los productos a comprar, esto es solo una simulación, no se guarda en la base de datos.
|
||||
|
||||
```sh
|
||||
POST /api/checkouts
|
||||
```
|
||||
|
||||
body:
|
||||
|
||||
```json
|
||||
{
|
||||
"cart": [...]
|
||||
}
|
||||
```
|
||||
|
||||
Endpoint para obtener el perfil del usuario autenticado. Necesitas enviar el token JWT en el header.
|
||||
|
||||
```sh
|
||||
GET /api/auth/me
|
||||
```
|
||||
|
||||
## JWT
|
||||
|
||||
Para obtener el token JWT, se debe hacer una petición a `/api/auth/login` o a `/api/auth/register` con el body correspondiente.
|
||||
|
||||
El token JWT se debe enviar en el header `Authorization` de la siguiente manera:
|
||||
|
||||
```sh
|
||||
Authorization Bearer token_jwt
|
||||
```
|
||||
|
||||
Ejemplo con fetch:
|
||||
|
||||
```js
|
||||
await fetch("http://localhost:5000/api/checkout", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer token_jwt`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
cart: carrito,
|
||||
}),
|
||||
});
|
||||
```
|
||||
95
backend/controllers/auth.controller.js
Normal file
95
backend/controllers/auth.controller.js
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import "dotenv/config";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { nanoid } from "nanoid";
|
||||
import { authModel } from "../models/auth.model.js";
|
||||
import { isValidEmail } from "../utils/validators/email.validate.js";
|
||||
|
||||
const login = async (req, res) => {
|
||||
try {
|
||||
const { email = "", password = "" } = req.body;
|
||||
|
||||
if (!email.trim() || !password.trim()) {
|
||||
return res.status(400).json({ error: "Email and password are required" });
|
||||
}
|
||||
|
||||
if (!isValidEmail(email)) {
|
||||
return res.status(400).json({ error: "Invalid email" });
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "Password must be at least 6 characters" });
|
||||
}
|
||||
|
||||
const user = await authModel.getUserByEmail(email);
|
||||
|
||||
if (!user) {
|
||||
return res.status(400).json({ error: "User not found" });
|
||||
}
|
||||
|
||||
if (user.password !== password) {
|
||||
return res.status(400).json({ error: "Invalid password" });
|
||||
}
|
||||
|
||||
const payload = { email, id: user.id };
|
||||
const token = jwt.sign(payload, process.env.JWT_SECRET);
|
||||
|
||||
return res.json({ email, token });
|
||||
} catch (error) {
|
||||
// console.log(error);
|
||||
return res.status(500).json({ error: "Server error" });
|
||||
}
|
||||
};
|
||||
|
||||
const register = async (req, res) => {
|
||||
try {
|
||||
const { email = "", password = "" } = req.body;
|
||||
|
||||
if (!email.trim() || !password.trim()) {
|
||||
return res.status(400).json({ error: "Email and password are required" });
|
||||
}
|
||||
|
||||
if (!isValidEmail(email)) {
|
||||
return res.status(400).json({ error: "Invalid email" });
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "Password must be at least 6 characters" });
|
||||
}
|
||||
|
||||
const user = await authModel.getUserByEmail(email);
|
||||
if (user) {
|
||||
return res.status(400).json({ error: "User already exists" });
|
||||
}
|
||||
const newUser = { email, password, id: nanoid() };
|
||||
await authModel.addUser(newUser);
|
||||
|
||||
const payload = { email, id: newUser.id };
|
||||
const token = jwt.sign(payload, process.env.JWT_SECRET);
|
||||
|
||||
return res.json({ email, token });
|
||||
} catch (error) {
|
||||
// console.log(error);
|
||||
return res.status(500).json({ error: "Server error" });
|
||||
}
|
||||
};
|
||||
|
||||
const me = async (req, res) => {
|
||||
try {
|
||||
const { email } = req.user;
|
||||
const user = await authModel.getUserByEmail(email);
|
||||
return res.json({ email, id: user.id });
|
||||
} catch (error) {
|
||||
// console.log(error);
|
||||
return res.status(500).json({ error: "Server error" });
|
||||
}
|
||||
};
|
||||
|
||||
export const authController = {
|
||||
login,
|
||||
register,
|
||||
me,
|
||||
};
|
||||
16
backend/controllers/checkout.controller.js
Normal file
16
backend/controllers/checkout.controller.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
const create = async (req, res) => {
|
||||
try {
|
||||
return res.json({
|
||||
message: "Checkout successful",
|
||||
cart: req.body,
|
||||
user: req.user,
|
||||
});
|
||||
} catch (error) {
|
||||
// console.log(error);
|
||||
return res.status(500).json({ error: "Server error" });
|
||||
}
|
||||
};
|
||||
|
||||
export const checkoutController = {
|
||||
create,
|
||||
};
|
||||
20
backend/controllers/pizza.controller.js
Normal file
20
backend/controllers/pizza.controller.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { pizzaModel } from "../models/pizza.model.js";
|
||||
|
||||
const readPizzas = async (req, res) => {
|
||||
const pizzas = await pizzaModel.getPizzas();
|
||||
res.json(pizzas);
|
||||
};
|
||||
|
||||
const readPizza = async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const pizza = await pizzaModel.getPizza(id.toLowerCase());
|
||||
if (!pizza) {
|
||||
return res.status(404).json({ message: "Pizza not found" });
|
||||
}
|
||||
res.json(pizza);
|
||||
};
|
||||
|
||||
export const pizzaController = {
|
||||
readPizzas,
|
||||
readPizza,
|
||||
};
|
||||
50
backend/db/pizzas.json
Normal file
50
backend/db/pizzas.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
[
|
||||
{
|
||||
"desc": "La pizza napolitana, de masa tierna y delgada pero bordes altos, es la versión propia de la cocina napolitana de la pizza redonda. El término pizza napoletana, por su importancia histórica o regional, se emplea en algunas zonas como sinónimo de pizza tonda.",
|
||||
"id": "p001",
|
||||
"img": "https://firebasestorage.googleapis.com/v0/b/apis-varias-mias.appspot.com/o/pizzeria%2Fpizza-1239077_640_cl.jpg?alt=media&token=6a9a33da-5c00-49d4-9080-784dcc87ec2c",
|
||||
"ingredients": ["mozzarella", "tomates", "jamón", "orégano"],
|
||||
"name": "napolitana",
|
||||
"price": 5950
|
||||
},
|
||||
{
|
||||
"desc": "La pizza es una preparación culinaria que consiste en un pan plano, habitualmente de forma circular, elaborado con harina de trigo, levadura, agua y sal (a veces aceite de oliva) que comúnmente se cubre con salsa de tomate, queso y otros muchos ingredientes, y que se hornea a alta temperatura, tradicionalmente en un horno de leña.",
|
||||
"id": "p002",
|
||||
"img": "https://firebasestorage.googleapis.com/v0/b/apis-varias-mias.appspot.com/o/pizzeria%2Fcheese-164872_640_com.jpg?alt=media&token=18b2b821-4d0d-43f2-a1c6-8c57bc388fab",
|
||||
"ingredients": ["mozzarella", "tomates", "jamón", "choricillo"],
|
||||
"name": "española",
|
||||
"price": 7250
|
||||
},
|
||||
{
|
||||
"desc": "La pizza es una preparación culinaria que consiste en un pan plano, habitualmente de forma circular, elaborado con harina de trigo, levadura, agua y sal (a veces aceite de oliva) que comúnmente se cubre con salsa de tomate, queso y otros muchos ingredientes, y que se hornea a alta temperatura, tradicionalmente en un horno de leña.",
|
||||
"id": "p003",
|
||||
"img": "https://firebasestorage.googleapis.com/v0/b/apis-varias-mias.appspot.com/o/pizzeria%2Fpizza-1239077_640_com.jpg?alt=media&token=e7cde87a-08d5-4040-ac54-90f6c31eb3e3",
|
||||
"ingredients": ["mozzarella", "tomates", "salame", "orégano"],
|
||||
"name": "salame",
|
||||
"price": 5990
|
||||
},
|
||||
{
|
||||
"desc": "La pizza es una preparación culinaria que consiste en un pan plano, habitualmente de forma circular, elaborado con harina de trigo, levadura, agua y sal (a veces aceite de oliva) que comúnmente se cubre con salsa de tomate, queso y otros muchos ingredientes, y que se hornea a alta temperatura, tradicionalmente en un horno de leña.",
|
||||
"id": "p004",
|
||||
"img": "https://firebasestorage.googleapis.com/v0/b/apis-varias-mias.appspot.com/o/pizzeria%2Fpizza-2000595_640_c.jpg?alt=media&token=61325b6e-a1e0-441e-b3b5-7335ba13e8be",
|
||||
"ingredients": ["mozzarella", "salame", "aceitunas", "champiñones"],
|
||||
"name": "cuatro estaciones",
|
||||
"price": 9590
|
||||
},
|
||||
{
|
||||
"desc": "La pizza es una preparación culinaria que consiste en un pan plano, habitualmente de forma circular, elaborado con harina de trigo, levadura, agua y sal (a veces aceite de oliva) que comúnmente se cubre con salsa de tomate, queso y otros muchos ingredientes, y que se hornea a alta temperatura, tradicionalmente en un horno de leña.",
|
||||
"id": "p005",
|
||||
"img": "https://firebasestorage.googleapis.com/v0/b/apis-varias-mias.appspot.com/o/pizzeria%2Fpizza-salame.jpg?alt=media&token=ab3d4bf8-01f2-4810-982b-bd7fb6b517b2",
|
||||
"ingredients": ["mozzarella", "tomates cherry", "bacon", "orégano"],
|
||||
"name": "bacon",
|
||||
"price": 6450
|
||||
},
|
||||
{
|
||||
"desc": "La pizza es una preparación culinaria que consiste en un pan plano, habitualmente de forma circular, elaborado con harina de trigo, levadura, agua y sal (a veces aceite de oliva) que comúnmente se cubre con salsa de tomate, queso y otros muchos ingredientes, y que se hornea a alta temperatura, tradicionalmente en un horno de leña.",
|
||||
"id": "p006",
|
||||
"img": "https://firebasestorage.googleapis.com/v0/b/apis-varias-mias.appspot.com/o/pizzeria%2Fpizza-2000595_640_c.jpg?alt=media&token=61325b6e-a1e0-441e-b3b5-7335ba13e8be",
|
||||
"ingredients": ["mozzarella", "pimientos", "pollo grillé", "orégano"],
|
||||
"name": "pollo picante",
|
||||
"price": 8500
|
||||
}
|
||||
]
|
||||
7
backend/db/users.json
Normal file
7
backend/db/users.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[
|
||||
{
|
||||
"email": "test@test.com",
|
||||
"password": "123123",
|
||||
"id": "UYz_2Vy9rNw7uELQ7AZ8D"
|
||||
}
|
||||
]
|
||||
24
backend/index.js
Normal file
24
backend/index.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import cors from "cors";
|
||||
import "dotenv/config";
|
||||
import express from "express";
|
||||
|
||||
import authRoute from "./routes/auth.route.js";
|
||||
import checkoutRoute from "./routes/checkout.route.js";
|
||||
import pizzaRoute from "./routes/pizza.route.js";
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(express.json());
|
||||
app.use(cors());
|
||||
|
||||
app.use("/api/auth", authRoute);
|
||||
app.use("/api/pizzas", pizzaRoute);
|
||||
app.use("/api/checkouts", checkoutRoute);
|
||||
app.use((_, res) => {
|
||||
res.status(404).json({ error: "Not Found" });
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 5000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server is running on port http://localhost:${PORT}`);
|
||||
});
|
||||
18
backend/middlewares/auth.middleware.js
Normal file
18
backend/middlewares/auth.middleware.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import "dotenv/config";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
export const authMiddleware = (req, res, next) => {
|
||||
const token = req.headers.authorization?.split(" ")[1];
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: "No token provided" });
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = jwt.verify(token, process.env.JWT_SECRET);
|
||||
req.user = payload;
|
||||
next();
|
||||
} catch (error) {
|
||||
// console.log(error);
|
||||
return res.status(401).send({ error: "Invalid token" });
|
||||
}
|
||||
};
|
||||
19
backend/models/auth.model.js
Normal file
19
backend/models/auth.model.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { readFile, writeFile } from "node:fs/promises";
|
||||
|
||||
const getUserByEmail = async (email) => {
|
||||
const data = await readFile("db/users.json", "utf-8");
|
||||
const users = JSON.parse(data);
|
||||
return users.find((user) => user.email === email);
|
||||
};
|
||||
|
||||
const addUser = async (newUser) => {
|
||||
const data = await readFile("db/users.json", "utf-8");
|
||||
const users = JSON.parse(data);
|
||||
users.push(newUser);
|
||||
await writeFile("db/users.json", JSON.stringify(users, null, 2));
|
||||
};
|
||||
|
||||
export const authModel = {
|
||||
getUserByEmail,
|
||||
addUser,
|
||||
};
|
||||
16
backend/models/pizza.model.js
Normal file
16
backend/models/pizza.model.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { readFile } from "node:fs/promises";
|
||||
|
||||
const getPizzas = async () => {
|
||||
const data = await readFile("db/pizzas.json", "utf-8");
|
||||
return JSON.parse(data);
|
||||
};
|
||||
|
||||
const getPizza = async (id) => {
|
||||
const pizzas = await getPizzas();
|
||||
return pizzas.find((pizza) => pizza.id === id);
|
||||
};
|
||||
|
||||
export const pizzaModel = {
|
||||
getPizzas,
|
||||
getPizza,
|
||||
};
|
||||
1227
backend/package-lock.json
generated
Normal file
1227
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
24
backend/package.json
Normal file
24
backend/package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "simple-api-jwt",
|
||||
"type": "module",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"dev": "nodemon index.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "bluuweb",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"nanoid": "^5.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.0"
|
||||
}
|
||||
}
|
||||
11
backend/routes/auth.route.js
Normal file
11
backend/routes/auth.route.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { Router } from "express";
|
||||
import { authController } from "../controllers/auth.controller.js";
|
||||
import { authMiddleware } from "../middlewares/auth.middleware.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post("/login", authController.login);
|
||||
router.post("/register", authController.register);
|
||||
router.get("/me", authMiddleware, authController.me);
|
||||
|
||||
export default router;
|
||||
10
backend/routes/checkout.route.js
Normal file
10
backend/routes/checkout.route.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Router } from "express";
|
||||
import { checkoutController } from "../controllers/checkout.controller.js";
|
||||
import { authMiddleware } from "../middlewares/auth.middleware.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authMiddleware);
|
||||
router.post("/", checkoutController.create);
|
||||
|
||||
export default router;
|
||||
9
backend/routes/pizza.route.js
Normal file
9
backend/routes/pizza.route.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { Router } from "express";
|
||||
import { pizzaController } from "../controllers/pizza.controller.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/", pizzaController.readPizzas);
|
||||
router.get("/:id", pizzaController.readPizza);
|
||||
|
||||
export default router;
|
||||
5
backend/utils/validators/email.validate.js
Normal file
5
backend/utils/validators/email.validate.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export const isValidEmail = (email) => {
|
||||
const regexEmail =
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
return regexEmail.test(email);
|
||||
};
|
||||
|
|
@ -13,7 +13,7 @@ const Navbar = () => {
|
|||
</Link>
|
||||
{token ? (
|
||||
<>
|
||||
<button>Profile</button>
|
||||
<Link to="/profile">Profile</Link>
|
||||
<button>Logout</button>
|
||||
</>
|
||||
) : (
|
||||
|
|
|
|||
16
src/pages/Profile.jsx
Normal file
16
src/pages/Profile.jsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
const Profile = () => {
|
||||
const email = "test@example.com";
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
<strong>Mail: </strong>
|
||||
{email}
|
||||
</p>
|
||||
<button className="bg-green-700 hover:bg-green-300 text-white hover:text-black">
|
||||
Cerrar sesión
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Profile;
|
||||
Loading…
Reference in a new issue