From d9dbcaa5123bd47ec28e1de435a516b7f80ff9b9 Mon Sep 17 00:00:00 2001 From: SebastianDev807 Date: Sat, 19 Oct 2024 23:27:34 -0500 Subject: [PATCH 1/3] validacion --- .env.template | 9 +++ app.js | 5 +- package.json | 4 +- readme.md | 72 +++++++++++++++++ src/controllers/task.controller.js | 8 +- src/helpers/generate-jwt.js | 6 +- src/middlewares/validate-jwt.js | 45 +++++++++++ src/models/task.model.js | 6 +- src/routes/task.routes.js | 7 +- src/routes/user.routes.js | 19 +++-- src/seed/task.seed.js | 77 ++++++++++++++++++ src/seed/user.seed.js | 121 +++++++++++++++++++++++++++++ 12 files changed, 366 insertions(+), 13 deletions(-) create mode 100644 .env.template create mode 100644 readme.md create mode 100644 src/middlewares/validate-jwt.js create mode 100644 src/seed/user.seed.js diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..819e3f0 --- /dev/null +++ b/.env.template @@ -0,0 +1,9 @@ +PORT=3000 + +#DATABASE +MONGO_USER= +MONGO_PASSWORD= +MONGO_CNN= + +#JWT +JWT_SECRET=x11E6xalIvOxEVq2Nf5Bc4m \ No newline at end of file diff --git a/app.js b/app.js index b5c4e34..6b99c6d 100644 --- a/app.js +++ b/app.js @@ -3,6 +3,7 @@ import express from 'express'; import dotenv from 'dotenv'; import dbConnect from './src/config/db.config.js'; import taskSeed from './src/seed/task.seed.js'; +import usersSeed from './src/seed/user.seed.js'; dotenv.config(); @@ -20,9 +21,9 @@ app.use('/api/v1/users', userRouter); app.use('/api/v1/auth', authRouter); -//Seed +//Seeds app.use('/api/v1/seed', taskSeed); - +app.use('/api/v1/seed', usersSeed) diff --git a/package.json b/package.json index c5543c0..bb0a7ad 100644 --- a/package.json +++ b/package.json @@ -15,5 +15,7 @@ "start": "node app", "dev": "nodemon --exec \"cls && node app\"" }, - "type": "module" + "type": "module", + "author": "Juan Sebastián Astudillo Ordoñez" } + diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..633bd04 --- /dev/null +++ b/readme.md @@ -0,0 +1,72 @@ +# To-Do API + +## Descripción +Esta es una API de tareas por hacer, en su primera versión. La aplicación cuenta con autenticación, protección de rutas con JWT, creación de usuarios y relaciones entre tareas y usuarios. + +## Estado del Proyecto +En desarrollo + +## Características +- Autenticación de usuarios. +- Protección de rutas utilizando JWT. +- Creación y gestión de tareas. +- Relación entre tareas y usuarios. +- **Futuras características:** + - Visualización de tareas relacionadas con el usuario. + - Validación de roles. + - Confirmación por correo electrónico. + - Ejecutar semillas en un solo endpoint + +## Tecnologías Usadas +- Node.js +- Express +- MongoDB + +## Instalación +1. Clonar el repositorio. +2. Instalar las dependencias + ``` + yarn install + ``` +3. Crear un cluster en MongoDB Atlas. +4. Renombrar `.env.template` a `.env` y rellenar las variables de entorno. +5. Ejecutar la aplicacion en desarrollo + ``` + yarn dev + ``` + +4. Ejecutar las semillas de usuario y tareas. + + ``` + http://localhost:3000/api/v1/seed/users + http://localhost:3000/api/v1/seed/tasks + ``` + +## Uso +### Endpoints + +| Método | Endpoint | Descripción | +|--------|-----------------------------------------------------|-------------------------------------------------------| +| POST | `http://localhost:3000/api/v1/users/register` | Registrar un usuario (requiere email y password en el body) | +| GET | `http://localhost:3000/api/v1/users/verify/:token` | Verificar un usuario con su token | +| POST | `http://localhost:3000/api/v1/auth/login` | Iniciar sesión, retorna un JWT | +| POST | `http://localhost:3000/api/v1/tasks` | Crear tarea, requiere un JWT en el header | +| GET | `http://localhost:3000/api/v1/tasks/:term` | Buscar una tarea específica por título o estado | +| PATCH | `http://localhost:3000/api/v1/tasks/:id` | Actualizar una tarea por su ID, requiere JWT | +| GET | `http://localhost:3000/api/v1/tasks?limit={number}&offset={number}` | Obtener todas las tareas paginadas | +| DELETE | `http://localhost:3000/api/v1/tasks/:id` | Borrar tarea, requiere JWT | + + + +## Licencia +Este proyecto está bajo la Licencia MIT. + +## Créditos +- Juan Sebastián Astudillo Ordoñez + +## Contacto +Puedes contactarme en [sebastian.dev0708@gmail.com](mailto:sebastian.dev0708@gmail.com). + +## Changelog +### v1.0.0 +- Primera versión lanzada. diff --git a/src/controllers/task.controller.js b/src/controllers/task.controller.js index d0b5d9a..510f73c 100644 --- a/src/controllers/task.controller.js +++ b/src/controllers/task.controller.js @@ -11,10 +11,13 @@ export const createTask = async (req = request, res = response) => { try { const task = await Task.create({ + //Aqui establezco el id del usuario autenticado + author: req.user._id, status: status ? status.toUpperCase() : undefined, ...data }); + return res.status(201).json({ task }); } catch (error) { @@ -45,7 +48,7 @@ export const findTaskByTerm = async (req = request, res = response) => { }); } - if (task.length === 0) + if (!task || task.length === 0) return res.status(404).json({ error: `Task with title, id, or status '${term}' not found` }); @@ -89,13 +92,14 @@ export const updateTask = async (req = request, res = response) => { } } -//TODO: Añadir Ordenamiento + export const findAllPaginated = async (req = request, res = response) => { const { limit = 5, offset = 0 } = req.query; try { const results = await Task.find() + .populate('author', 'username _id') .skip(offset) .limit(+limit) diff --git a/src/helpers/generate-jwt.js b/src/helpers/generate-jwt.js index 71cd8ce..3751d19 100644 --- a/src/helpers/generate-jwt.js +++ b/src/helpers/generate-jwt.js @@ -2,7 +2,11 @@ import jwt from 'jsonwebtoken'; export const generateJwt = async (uid) => { try { - return await jwt.sign({ uid }, process.env.JWT_SECRET); + return await jwt.sign( + { uid }, + process.env.JWT_SECRET, + { expiresIn: '4h' } + ); } catch (error) { console.error(error) } diff --git a/src/middlewares/validate-jwt.js b/src/middlewares/validate-jwt.js new file mode 100644 index 0000000..06d7c54 --- /dev/null +++ b/src/middlewares/validate-jwt.js @@ -0,0 +1,45 @@ +import jwt from 'jsonwebtoken'; +import User from '../models/user.model.js'; + +//middleware para validar JWT +export const validateJWT = async (req, res, next) => { + + const token = req.header('to-do-token'); + + if (!token) { + return res.status(401).json({ + error: 'No token provided' + }); + } + + try { + + //Extraer el payload del token + const { uid } = jwt.verify(token, process.env.JWT_SECRET); + + //leer el usuario correspondiente al uid + const user = await User.findById(uid); + + //Verificar si el usuario existe + if(!user || !user.status){ + return res.status(404).json({ + error:`User with id ${uid} doesn't exists` + }); + } + + // ??? + req.user = user; + + next(); + + + + } catch (error) { + console.error(error); + res.status(500).json({ + error: 'Something went broke.' + }) + + } + +} \ No newline at end of file diff --git a/src/models/task.model.js b/src/models/task.model.js index 8a2e97e..4725412 100644 --- a/src/models/task.model.js +++ b/src/models/task.model.js @@ -1,4 +1,4 @@ -import { Schema, model } from 'mongoose'; +import mongoose, { Schema, model } from 'mongoose'; const TaskSchema = Schema({ title: { @@ -18,6 +18,10 @@ const TaskSchema = Schema({ type: String, enum: ['PENDING', 'IN PROGRESS', 'COMPLETED'], default: 'PENDING' + }, + author: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Users' } }, { timestamps: true } diff --git a/src/routes/task.routes.js b/src/routes/task.routes.js index f5225d6..ac6bb36 100644 --- a/src/routes/task.routes.js +++ b/src/routes/task.routes.js @@ -3,10 +3,13 @@ import { createTask, deleteTask, findAllPaginated, findTaskByTerm, updateTask } import { checkValidationResult } from "../middlewares/check-validation-result.js"; import { check } from "express-validator"; import { isValidEnum } from "../helpers/check-enum.js"; +import { validateJWT } from "../middlewares/validate-jwt.js"; const taskRouter = Router(); +//TODO: Validar roles [ADMIN, USER] taskRouter.post('/', [ + validateJWT, check('title', 'Title must be more than 5 characters.') .isLength({ min: 5, max: 50 }), @@ -27,6 +30,7 @@ taskRouter.post('/', [ taskRouter.put('/:id', [ + validateJWT, check('id', 'Invalid Mongo ID').isMongoId(), check('title', 'Title must be more than 5 characters.') .optional() @@ -49,7 +53,8 @@ taskRouter.put('/:id', [ taskRouter.delete('/:id', [ - check('id','Invalid Mongo Id').isMongoId(), + validateJWT, + check('id', 'Invalid Mongo Id').isMongoId(), checkValidationResult ], deleteTask); diff --git a/src/routes/user.routes.js b/src/routes/user.routes.js index 90537de..59d9935 100644 --- a/src/routes/user.routes.js +++ b/src/routes/user.routes.js @@ -1,15 +1,24 @@ +import { check } from "express-validator"; import { confirmAccount, createAccount, desactiveUser } from "../controllers/user.controller.js"; import { Router } from "express"; const userRouter = Router(); -//TODO:AÑADIR VALIDACIONES -userRouter.post('/register', createAccount); -//TODO:AÑADIR VALIDACIONES +userRouter.post('/register', [ + check('username', 'username must be more than 5 characters') + .isString({ min: 5 }), + check('email', 'invalid email format').isEmail(), + check('password', 'password is required').isString({ min: 6 }) + +], createAccount); + + userRouter.get('/verify/:token', confirmAccount); -//TODO:AÑADIR VALIDACIONES -userRouter.delete('/:id', desactiveUser); + +userRouter.delete('/:id', [ + check('id', 'Invalid mongoId.').isMongoId() +], desactiveUser); export default userRouter; \ No newline at end of file diff --git a/src/seed/task.seed.js b/src/seed/task.seed.js index 4c97d43..2a2b800 100644 --- a/src/seed/task.seed.js +++ b/src/seed/task.seed.js @@ -1,5 +1,6 @@ import { Router } from "express"; import Task from "../models/task.model.js"; +import User from '../models/user.model.js' const taskSeed = Router(); @@ -99,6 +100,66 @@ const tasks = [ description: "Conocer a otros desarrolladores y aprender", status: "PENDING", deadLine: new Date("2024-10-30") + }, + { + title: "Estudiar un nuevo framework", + description: "Aprender sobre un nuevo framework de desarrollo web", + status: "PENDING", + deadLine: new Date("2024-11-15") + }, + { + title: "Crear un portafolio en línea", + description: "Diseñar y desarrollar un portafolio para mostrar mis proyectos", + status: "PENDING", + deadLine: new Date("2024-11-30") + }, + { + title: "Leer un libro de programación", + description: "Terminar de leer 'Clean Code' de Robert C. Martin", + status: "PENDING", + deadLine: new Date("2024-12-15") + }, + { + title: "Participar en un hackathon", + description: "Unirse a un hackathon local y colaborar con otros", + status: "PENDING", + deadLine: new Date("2024-11-20") + }, + { + title: "Actualizar el CV", + description: "Revisar y actualizar el currículum vitae", + status: "PENDING", + deadLine: new Date("2024-12-01") + }, + { + title: "Aprender sobre bases de datos NoSQL", + description: "Explorar MongoDB y otras bases de datos NoSQL", + status: "PENDING", + deadLine: new Date("2024-12-05") + }, + { + title: "Tomar un curso de JavaScript avanzado", + description: "Inscribirse en un curso en línea para mejorar mis habilidades", + status: "PENDING", + deadLine: new Date("2024-12-10") + }, + { + title: "Conectar con un mentor", + description: "Buscar y establecer contacto con un mentor en la industria", + status: "PENDING", + deadLine: new Date("2024-11-25") + }, + { + title: "Implementar un proyecto personal", + description: "Desarrollar un proyecto personal para aplicar lo aprendido", + status: "PENDING", + deadLine: new Date("2024-12-20") + }, + { + title: "Revisar y practicar algoritmos", + description: "Dedicar tiempo a resolver problemas de algoritmos y estructuras de datos", + status: "PENDING", + deadLine: new Date("2024-12-30") } ]; @@ -106,6 +167,22 @@ taskSeed.get('/tasks', async (req, res) => { try { + const users = await User.find(); + + //Extraer los ids para asignarselos a cada task + const ids = users.reduce((acc, user) => { + acc.push(user._id); + return acc; + }, []); + + //Asignar un id aleatorio a cada usuario + //Simulando que estos crearon dicha tarea + tasks.forEach(task => { + const randomIndex = Math.floor(Math.random() * ids.length); + task.author = ids[randomIndex]; + }); + + await Task.deleteMany(); await Task.insertMany(tasks); diff --git a/src/seed/user.seed.js b/src/seed/user.seed.js new file mode 100644 index 0000000..17b3969 --- /dev/null +++ b/src/seed/user.seed.js @@ -0,0 +1,121 @@ +import Users from '../models/user.model.js'; +import bcrypt from 'bcryptjs'; +import { Router } from 'express'; + +const users = [ + { + username: "Juan", + email: "juan.dev@example.com", + password: "Ju4nPassword!", + token: null, + confirmed: "true", + status: "true" + }, + { + username: "Ana", + email: "ana.dev@example.com", + password: "An@Password123", + token: null, + confirmed: "true", + status: "true" + }, + { + username: "Luis", + email: "luis.dev@example.com", + password: "Lu1sPassword#", + token: null, + confirmed: "true", + status: "true" + }, + { + username: "María", + email: "maria.dev@example.com", + password: "M@riaPassword456", + token: null, + confirmed: "true", + status: "true" + }, + { + username: "Carlos", + email: "carlos.dev@example.com", + password: "C@rlosPassword789", + token: null, + confirmed: "true", + status: "true" + }, + { + username: "Lucía", + email: "lucia.dev@example.com", + password: "Luc1aPassword%", + token: null, + confirmed: "true", + status: "true" + }, + { + username: "David", + email: "david.dev@example.com", + password: "D@vidPassword123", + token: null, + confirmed: "true", + status: "true" + }, + { + username: "Paola", + email: "paola.dev@example.com", + password: "P@olaPassword456", + token: null, + confirmed: "true", + status: "true" + }, + { + username: "Fernando", + email: "fernando.dev@example.com", + password: "F3rnandoPassword!", + token: null, + confirmed: "true", + status: "true" + }, + { + username: "Sofía", + email: "sofia.dev@example.com", + password: "S0fiaPassword#", + token: null, + confirmed: "true", + status: "true" + } +]; + + +const usersSeed = Router(); + +usersSeed.get('/users', async (req, res) => { + + try { + + users.forEach((user, uid) => { + + const { password } = user; + // user._id = uids[uid]; + user.password = bcrypt.hashSync(password, bcrypt.genSaltSync(10)); + }); + + + await Users.deleteMany(); + await Users.insertMany(users); + + res.status(201).json({ + message: 'Seed executed' + }); + + } catch (error) { + + console.error(error); + + res.status(500).json({ + error: 'Something went broke.' + }); + + } +}); + +export default usersSeed; \ No newline at end of file From 69253cfef26521f7f6dab140acad6e2d3c285f08 Mon Sep 17 00:00:00 2001 From: SebastianDev807 Date: Sat, 19 Oct 2024 23:31:22 -0500 Subject: [PATCH 2/3] readme actualizado --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 633bd04..a2502af 100644 --- a/readme.md +++ b/readme.md @@ -38,7 +38,7 @@ En desarrollo 4. Ejecutar las semillas de usuario y tareas. ``` - http://localhost:3000/api/v1/seed/users + http://localhost:3000/api/v1/seed/users http://localhost:3000/api/v1/seed/tasks ``` @@ -52,7 +52,7 @@ En desarrollo | POST | `http://localhost:3000/api/v1/auth/login` | Iniciar sesión, retorna un JWT | | POST | `http://localhost:3000/api/v1/tasks` | Crear tarea, requiere un JWT en el header | | GET | `http://localhost:3000/api/v1/tasks/:term` | Buscar una tarea específica por título o estado | -| PATCH | `http://localhost:3000/api/v1/tasks/:id` | Actualizar una tarea por su ID, requiere JWT | +| PUT | `http://localhost:3000/api/v1/tasks/:id` | Actualizar una tarea por su ID, requiere JWT | | GET | `http://localhost:3000/api/v1/tasks?limit={number}&offset={number}` | Obtener todas las tareas paginadas | | DELETE | `http://localhost:3000/api/v1/tasks/:id` | Borrar tarea, requiere JWT | From 9cb9f4d30ca07f49a94ef5a0c4c9c1627222166c Mon Sep 17 00:00:00 2001 From: SebastianDev807 Date: Sat, 19 Oct 2024 23:33:09 -0500 Subject: [PATCH 3/3] readme updated --- readme.md | 91 +++++++++++++++++++++++++++---------------------------- 1 file changed, 44 insertions(+), 47 deletions(-) diff --git a/readme.md b/readme.md index a2502af..e4a8b30 100644 --- a/readme.md +++ b/readme.md @@ -1,72 +1,69 @@ # To-Do API -## Descripción -Esta es una API de tareas por hacer, en su primera versión. La aplicación cuenta con autenticación, protección de rutas con JWT, creación de usuarios y relaciones entre tareas y usuarios. +## Description +This is a to-do task API, in its first version. The application includes authentication, route protection with JWT, user creation, and relationships between tasks and users. -## Estado del Proyecto -En desarrollo +## Project Status +In development -## Características -- Autenticación de usuarios. -- Protección de rutas utilizando JWT. -- Creación y gestión de tareas. -- Relación entre tareas y usuarios. -- **Futuras características:** - - Visualización de tareas relacionadas con el usuario. - - Validación de roles. - - Confirmación por correo electrónico. - - Ejecutar semillas en un solo endpoint +## Features +- User authentication. +- Route protection using JWT. +- Task creation and management. +- Relationship between tasks and users. +- **Future Features:** + - Viewing tasks related to the user. + - Role validation. + - Email confirmation. + - Execute seeds in a single endpoint. -## Tecnologías Usadas +## Technologies Used - Node.js - Express - MongoDB -## Instalación -1. Clonar el repositorio. -2. Instalar las dependencias - ``` +## Installation +1. Clone the repository. +2. Install dependencies: + ```bash yarn install ``` -3. Crear un cluster en MongoDB Atlas. -4. Renombrar `.env.template` a `.env` y rellenar las variables de entorno. -5. Ejecutar la aplicacion en desarrollo - ``` +3. Create a cluster in MongoDB Atlas. +4. Rename `.env.template` to `.env` and fill in the environment variables. +5. Run the application in development: + ```bash yarn dev ``` -4. Ejecutar las semillas de usuario y tareas. - - ``` - http://localhost:3000/api/v1/seed/users - http://localhost:3000/api/v1/seed/tasks - ``` +6. Run the user and task seeds: + ```bash + http://localhost:3000/api/v1/seed/users + http://localhost:3000/api/v1/seed/tasks + ``` -## Uso +## Usage ### Endpoints -| Método | Endpoint | Descripción | +| Method | Endpoint | Description | |--------|-----------------------------------------------------|-------------------------------------------------------| -| POST | `http://localhost:3000/api/v1/users/register` | Registrar un usuario (requiere email y password en el body) | -| GET | `http://localhost:3000/api/v1/users/verify/:token` | Verificar un usuario con su token | -| POST | `http://localhost:3000/api/v1/auth/login` | Iniciar sesión, retorna un JWT | -| POST | `http://localhost:3000/api/v1/tasks` | Crear tarea, requiere un JWT en el header | -| GET | `http://localhost:3000/api/v1/tasks/:term` | Buscar una tarea específica por título o estado | -| PUT | `http://localhost:3000/api/v1/tasks/:id` | Actualizar una tarea por su ID, requiere JWT | -| GET | `http://localhost:3000/api/v1/tasks?limit={number}&offset={number}` | Obtener todas las tareas paginadas | -| DELETE | `http://localhost:3000/api/v1/tasks/:id` | Borrar tarea, requiere JWT | - - +| POST | `http://localhost:3000/api/v1/users/register` | Register a user (requires email and password in the body) | +| GET | `http://localhost:3000/api/v1/users/verify/:token` | Verify a user with their token | +| POST | `http://localhost:3000/api/v1/auth/login` | Log in, returns a JWT | +| POST | `http://localhost:3000/api/v1/tasks` | Create a task, requires a JWT in the header | +| GET | `http://localhost:3000/api/v1/tasks/:term` | Search for a specific task by title or status | +| PUT | `http://localhost:3000/api/v1/tasks/:id` | Update a task by its ID, requires JWT | +| GET | `http://localhost:3000/api/v1/tasks?limit={number}&offset={number}` | Get all tasks with pagination | +| DELETE | `http://localhost:3000/api/v1/tasks/:id` | Delete a task, requires JWT | -## Licencia -Este proyecto está bajo la Licencia MIT. +## License +This project is licensed under the MIT License. -## Créditos +## Credits - Juan Sebastián Astudillo Ordoñez -## Contacto -Puedes contactarme en [sebastian.dev0708@gmail.com](mailto:sebastian.dev0708@gmail.com). +## Contact +You can contact me at [sebastian.dev0708@gmail.com](mailto:sebastian.dev0708@gmail.com). ## Changelog ### v1.0.0 -- Primera versión lanzada. +- First version released.