From ab5d1586243fe5d7387b547b091da4046f4a2c21 Mon Sep 17 00:00:00 2001 From: Gus Date: Sun, 7 Jan 2024 23:54:06 -0600 Subject: [PATCH 01/30] feat(api): add crud operations for category --- apps/api/src/main.ts | 101 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 2 deletions(-) diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index e5fad103..86dd5db2 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -5,8 +5,105 @@ const port = process.env.PORT ? Number(process.env.PORT) : 3000; const app = express(); -app.get('/', (req, res) => { - res.send({ message: 'Hello MFEE!' }); +app.use(express.json()); + +// Initialize categories array to save data in memory +const categories = []; + +// Get all categories +app.get('/api/categories', (req, res) => { + // Return all the categories with a 200 status code + res.status(200).json(categories); +}); + +// Get category by id +app.get('/api/categories/:id', (req, res) => { + // Retrieve the id from the route params + const { id } = req.params; + // Check if we have a category with that id + const category = categories.find((p) => p.id === id); + + if (!category) { + // If we don't find the category return a 404 status code with a message + return res.status(404).json({ message: 'Category not found' }); + // Note: Remember that json method doesn't interrupt the workflow + // therefore is important to add a "return" to break the process + } + + // Return the category with a 200 status code + res.status(200).json(category); +}); + +// Create category +app.post('/api/categories', (req, res) => { + // Retrieve the name from the request body + const { name } = req.body; + + if (!name) { + // If name is empty or undefined return a 400 status code with a message + return res.status(400).json({ message: 'The name is required.' }); + } + + // Generate a new category + const newCategory = { + id: Date.now().toString(), // Convert id to string to match the value in get by id endpoint + name + }; + // Add the new category to our array + categories.push(newCategory); + + // Return the created category with a 201 status code + res.status(201).json(newCategory); +}); + +// Update category +app.patch('/api/categories/:id', (req, res) => { + // Retrieve the id from the route params + const { id } = req.params; + // Retrieve the index of the category in the array + const categoryIndex = categories.findIndex((p) => p.id === id); + + // "findIndex" will return -1 if there is no match + if (categoryIndex === -1) { + // If we don't find the category return a 404 status code with a message + return res.status(404).json({ message: 'Category not found' }); + } + + // Generate a copy of our cateogory + const updatedCategory = { ...categories[categoryIndex] }; + // Retrieve the name from the request body + const { name } = req.body; + + // Check if we have a name, if so update the property + if (name) { + updatedCategory.name = name; + } + + // Update the category in our array + categories[categoryIndex] = updatedCategory; + + // Return the updated category with a 200 status code + res.status(200).json(updatedCategory); +}); + +// Delete category +app.delete('/api/categories/:id', (req, res) => { + // Retrieve the id from the route params + const { id } = req.params; + // Retrieve the index of the category in the array + const categoryIndex = categories.findIndex((p) => p.id === id); + + // "findIndex" will return -1 if there is no match + if (categoryIndex === -1) { + // If we don't find the category return a 404 status code with a message + return res.status(404).json({ message: 'Category not found' }); + } + + // Remove the category from the array + categories.splice(categoryIndex, 1); + + // Return a 204 status code + res.status(204).send(); }); app.listen(port, host, () => { From 4f3b7352ed1bbacb51792ee04d42ba2c868c5741 Mon Sep 17 00:00:00 2001 From: Gus Date: Mon, 8 Jan 2024 00:14:35 -0600 Subject: [PATCH 02/30] feat(api): add router --- apps/api/src/main.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 86dd5db2..99e3de71 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -4,6 +4,7 @@ const host = process.env.HOST ?? 'localhost'; const port = process.env.PORT ? Number(process.env.PORT) : 3000; const app = express(); +const router = express.Router(); app.use(express.json()); @@ -11,13 +12,13 @@ app.use(express.json()); const categories = []; // Get all categories -app.get('/api/categories', (req, res) => { +router.get('/', (req, res) => { // Return all the categories with a 200 status code res.status(200).json(categories); }); // Get category by id -app.get('/api/categories/:id', (req, res) => { +router.get('/:id', (req, res) => { // Retrieve the id from the route params const { id } = req.params; // Check if we have a category with that id @@ -35,7 +36,7 @@ app.get('/api/categories/:id', (req, res) => { }); // Create category -app.post('/api/categories', (req, res) => { +router.post('/', (req, res) => { // Retrieve the name from the request body const { name } = req.body; @@ -57,7 +58,7 @@ app.post('/api/categories', (req, res) => { }); // Update category -app.patch('/api/categories/:id', (req, res) => { +router.patch('/:id', (req, res) => { // Retrieve the id from the route params const { id } = req.params; // Retrieve the index of the category in the array @@ -87,7 +88,7 @@ app.patch('/api/categories/:id', (req, res) => { }); // Delete category -app.delete('/api/categories/:id', (req, res) => { +router.delete('/:id', (req, res) => { // Retrieve the id from the route params const { id } = req.params; // Retrieve the index of the category in the array @@ -106,6 +107,8 @@ app.delete('/api/categories/:id', (req, res) => { res.status(204).send(); }); +app.use('/api/categories', router); + app.listen(port, host, () => { console.log(`[ ready ] http://${host}:${port}`); }); From 1ed66331d725002b9400e8b20f14b9d8868ba7af Mon Sep 17 00:00:00 2001 From: Gus Date: Mon, 8 Jan 2024 00:15:52 -0600 Subject: [PATCH 03/30] feat(api): refactor crud operations to new file --- apps/api/src/main.ts | 105 +----------------------------- apps/api/src/routes/.gitkeep | 0 apps/api/src/routes/categories.ts | 103 +++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 102 deletions(-) delete mode 100644 apps/api/src/routes/.gitkeep create mode 100644 apps/api/src/routes/categories.ts diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 99e3de71..a59003ab 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -1,113 +1,14 @@ import express from 'express'; +import categories from './routes/categories'; + const host = process.env.HOST ?? 'localhost'; const port = process.env.PORT ? Number(process.env.PORT) : 3000; const app = express(); -const router = express.Router(); app.use(express.json()); - -// Initialize categories array to save data in memory -const categories = []; - -// Get all categories -router.get('/', (req, res) => { - // Return all the categories with a 200 status code - res.status(200).json(categories); -}); - -// Get category by id -router.get('/:id', (req, res) => { - // Retrieve the id from the route params - const { id } = req.params; - // Check if we have a category with that id - const category = categories.find((p) => p.id === id); - - if (!category) { - // If we don't find the category return a 404 status code with a message - return res.status(404).json({ message: 'Category not found' }); - // Note: Remember that json method doesn't interrupt the workflow - // therefore is important to add a "return" to break the process - } - - // Return the category with a 200 status code - res.status(200).json(category); -}); - -// Create category -router.post('/', (req, res) => { - // Retrieve the name from the request body - const { name } = req.body; - - if (!name) { - // If name is empty or undefined return a 400 status code with a message - return res.status(400).json({ message: 'The name is required.' }); - } - - // Generate a new category - const newCategory = { - id: Date.now().toString(), // Convert id to string to match the value in get by id endpoint - name - }; - // Add the new category to our array - categories.push(newCategory); - - // Return the created category with a 201 status code - res.status(201).json(newCategory); -}); - -// Update category -router.patch('/:id', (req, res) => { - // Retrieve the id from the route params - const { id } = req.params; - // Retrieve the index of the category in the array - const categoryIndex = categories.findIndex((p) => p.id === id); - - // "findIndex" will return -1 if there is no match - if (categoryIndex === -1) { - // If we don't find the category return a 404 status code with a message - return res.status(404).json({ message: 'Category not found' }); - } - - // Generate a copy of our cateogory - const updatedCategory = { ...categories[categoryIndex] }; - // Retrieve the name from the request body - const { name } = req.body; - - // Check if we have a name, if so update the property - if (name) { - updatedCategory.name = name; - } - - // Update the category in our array - categories[categoryIndex] = updatedCategory; - - // Return the updated category with a 200 status code - res.status(200).json(updatedCategory); -}); - -// Delete category -router.delete('/:id', (req, res) => { - // Retrieve the id from the route params - const { id } = req.params; - // Retrieve the index of the category in the array - const categoryIndex = categories.findIndex((p) => p.id === id); - - // "findIndex" will return -1 if there is no match - if (categoryIndex === -1) { - // If we don't find the category return a 404 status code with a message - return res.status(404).json({ message: 'Category not found' }); - } - - // Remove the category from the array - categories.splice(categoryIndex, 1); - - // Return a 204 status code - res.status(204).send(); -}); - -app.use('/api/categories', router); +app.use('/api/categories', categories); app.listen(port, host, () => { console.log(`[ ready ] http://${host}:${port}`); diff --git a/apps/api/src/routes/.gitkeep b/apps/api/src/routes/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/api/src/routes/categories.ts b/apps/api/src/routes/categories.ts new file mode 100644 index 00000000..f14d6e29 --- /dev/null +++ b/apps/api/src/routes/categories.ts @@ -0,0 +1,103 @@ +import express from 'express'; + +const router = express.Router(); +// Initialize categories array to save data in memory +const categories = []; + +// Get all categories +router.get('/', (req, res) => { + // Return all the categories with a 200 status code + res.status(200).json(categories); +}); + +// Get category by id +router.get('/:id', (req, res) => { + // Retrieve the id from the route params + const { id } = req.params; + // Check if we have a category with that id + const category = categories.find((p) => p.id === id); + + if (!category) { + // If we don't find the category return a 404 status code with a message + return res.status(404).json({ message: 'Category not found' }); + // Note: Remember that json method doesn't interrupt the workflow + // therefore is important to add a "return" to break the process + } + + // Return the category with a 200 status code + res.status(200).json(category); +}); + +// Create category +router.post('/', (req, res) => { + // Retrieve the name from the request body + const { name } = req.body; + + if (!name) { + // If name is empty or undefined return a 400 status code with a message + return res.status(400).json({ message: 'The name is required.' }); + } + + // Generate a new category + const newCategory = { + id: Date.now().toString(), // Convert id to string to match the value in get by id endpoint + name + }; + // Add the new category to our array + categories.push(newCategory); + + // Return the created category with a 201 status code + res.status(201).json(newCategory); +}); + +// Update category +router.patch('/:id', (req, res) => { + // Retrieve the id from the route params + const { id } = req.params; + // Retrieve the index of the category in the array + const categoryIndex = categories.findIndex((p) => p.id === id); + + // "findIndex" will return -1 if there is no match + if (categoryIndex === -1) { + // If we don't find the category return a 404 status code with a message + return res.status(404).json({ message: 'Category not found' }); + } + + // Generate a copy of our cateogory + const updatedCategory = { ...categories[categoryIndex] }; + // Retrieve the name from the request body + const { name } = req.body; + + // Check if we have a name, if so update the property + if (name) { + updatedCategory.name = name; + } + + // Update the category in our array + categories[categoryIndex] = updatedCategory; + + // Return the updated category with a 200 status code + res.status(200).json(updatedCategory); +}); + +// Delete category +router.delete('/:id', (req, res) => { + // Retrieve the id from the route params + const { id } = req.params; + // Retrieve the index of the category in the array + const categoryIndex = categories.findIndex((p) => p.id === id); + + // "findIndex" will return -1 if there is no match + if (categoryIndex === -1) { + // If we don't find the category return a 404 status code with a message + return res.status(404).json({ message: 'Category not found' }); + } + + // Remove the category from the array + categories.splice(categoryIndex, 1); + + // Return a 204 status code + res.status(204).send(); +}); + +export default router; From 41fcc8fca6e270141bd8eb7628cbe1bb9ae25242 Mon Sep 17 00:00:00 2001 From: Gus Date: Mon, 8 Jan 2024 08:40:53 -0600 Subject: [PATCH 04/30] refactor(api): move logic from route to controller --- apps/api/src/controllers/category.ts | 106 +++++++++++++++++++++++++++ apps/api/src/routes/categories.ts | 95 ++---------------------- 2 files changed, 113 insertions(+), 88 deletions(-) create mode 100644 apps/api/src/controllers/category.ts diff --git a/apps/api/src/controllers/category.ts b/apps/api/src/controllers/category.ts new file mode 100644 index 00000000..e94d41cb --- /dev/null +++ b/apps/api/src/controllers/category.ts @@ -0,0 +1,106 @@ +// Initialize categories array to save data in memory +const categories = []; + +// Get all categories +const getCategories = (req, res) => { + // Return all the categories with a 200 status code + res.status(200).json(categories); +}; + +// Get category by id +const getCategoryById = (req, res) => { + // Retrieve the id from the route params + const { id } = req.params; + // Check if we have a category with that id + const category = categories.find((p) => p.id === id); + + if (!category) { + // If we don't find the category return a 404 status code with a message + return res.status(404).json({ message: 'Category not found' }); + // Note: Remember that json method doesn't interrupt the workflow + // therefore is important to add a "return" to break the process + } + + // Return the category with a 200 status code + res.status(200).json(category); +}; + +// Create category +const createCategory = (req, res) => { + // Retrieve the name from the request body + const { name } = req.body; + + if (!name) { + // If name is empty or undefined return a 400 status code with a message + return res.status(400).json({ message: 'The name is required.' }); + } + + // Generate a new category + const newCategory = { + id: Date.now().toString(), // Convert id to string to match the value in get by id endpoint + name + }; + // Add the new category to our array + categories.push(newCategory); + + // Return the created category with a 201 status code + res.status(201).json(newCategory); +}; + +// Update category +const updateCategory = (req, res) => { + // Retrieve the id from the route params + const { id } = req.params; + // Retrieve the index of the category in the array + const categoryIndex = categories.findIndex((p) => p.id === id); + + // "findIndex" will return -1 if there is no match + if (categoryIndex === -1) { + // If we don't find the category return a 404 status code with a message + return res.status(404).json({ message: 'Category not found' }); + } + + // Generate a copy of our cateogory + const updatedCategory = { ...categories[categoryIndex] }; + // Retrieve the name from the request body + const { name } = req.body; + + // Check if we have a name, if so update the property + if (name) { + updatedCategory.name = name; + } + + // Update the category in our array + categories[categoryIndex] = updatedCategory; + + // Return the updated category with a 200 status code + res.status(200).json(updatedCategory); +}; + +// Delete category +const deleteCategory = (req, res) => { + // Retrieve the id from the route params + const { id } = req.params; + // Retrieve the index of the category in the array + const categoryIndex = categories.findIndex((p) => p.id === id); + + // "findIndex" will return -1 if there is no match + if (categoryIndex === -1) { + // If we don't find the category return a 404 status code with a message + return res.status(404).json({ message: 'Category not found' }); + } + + // Remove the category from the array + categories.splice(categoryIndex, 1); + + // Return a 204 status code + res.status(204).send(); +}; + +export default { + getCategories, + getCategoryById, + createCategory, + updateCategory, + deleteCategory +}; diff --git a/apps/api/src/routes/categories.ts b/apps/api/src/routes/categories.ts index f14d6e29..78e120a1 100644 --- a/apps/api/src/routes/categories.ts +++ b/apps/api/src/routes/categories.ts @@ -1,103 +1,22 @@ import express from 'express'; +import categoryController from '../controllers/category'; + const router = express.Router(); -// Initialize categories array to save data in memory -const categories = []; // Get all categories -router.get('/', (req, res) => { - // Return all the categories with a 200 status code - res.status(200).json(categories); -}); +router.get('/', categoryController.getCategories); // Get category by id -router.get('/:id', (req, res) => { - // Retrieve the id from the route params - const { id } = req.params; - // Check if we have a category with that id - const category = categories.find((p) => p.id === id); - - if (!category) { - // If we don't find the category return a 404 status code with a message - return res.status(404).json({ message: 'Category not found' }); - // Note: Remember that json method doesn't interrupt the workflow - // therefore is important to add a "return" to break the process - } - - // Return the category with a 200 status code - res.status(200).json(category); -}); +router.get('/:id', categoryController.getCategoryById); // Create category -router.post('/', (req, res) => { - // Retrieve the name from the request body - const { name } = req.body; - - if (!name) { - // If name is empty or undefined return a 400 status code with a message - return res.status(400).json({ message: 'The name is required.' }); - } - - // Generate a new category - const newCategory = { - id: Date.now().toString(), // Convert id to string to match the value in get by id endpoint - name - }; - // Add the new category to our array - categories.push(newCategory); - - // Return the created category with a 201 status code - res.status(201).json(newCategory); -}); +router.post('/', categoryController.createCategory); // Update category -router.patch('/:id', (req, res) => { - // Retrieve the id from the route params - const { id } = req.params; - // Retrieve the index of the category in the array - const categoryIndex = categories.findIndex((p) => p.id === id); - - // "findIndex" will return -1 if there is no match - if (categoryIndex === -1) { - // If we don't find the category return a 404 status code with a message - return res.status(404).json({ message: 'Category not found' }); - } - - // Generate a copy of our cateogory - const updatedCategory = { ...categories[categoryIndex] }; - // Retrieve the name from the request body - const { name } = req.body; - - // Check if we have a name, if so update the property - if (name) { - updatedCategory.name = name; - } - - // Update the category in our array - categories[categoryIndex] = updatedCategory; - - // Return the updated category with a 200 status code - res.status(200).json(updatedCategory); -}); +router.patch('/:id', categoryController.updateCategory); // Delete category -router.delete('/:id', (req, res) => { - // Retrieve the id from the route params - const { id } = req.params; - // Retrieve the index of the category in the array - const categoryIndex = categories.findIndex((p) => p.id === id); - - // "findIndex" will return -1 if there is no match - if (categoryIndex === -1) { - // If we don't find the category return a 404 status code with a message - return res.status(404).json({ message: 'Category not found' }); - } - - // Remove the category from the array - categories.splice(categoryIndex, 1); - - // Return a 204 status code - res.status(204).send(); -}); +router.delete('/:id', categoryController.deleteCategory); export default router; From b8016470851601ea2010bf3a34a6ab269cd60183 Mon Sep 17 00:00:00 2001 From: Gus Date: Mon, 8 Jan 2024 11:42:13 -0600 Subject: [PATCH 05/30] chore(api): remove .gitkeep from controllers --- apps/api/src/controllers/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 apps/api/src/controllers/.gitkeep diff --git a/apps/api/src/controllers/.gitkeep b/apps/api/src/controllers/.gitkeep deleted file mode 100644 index e69de29b..00000000 From 081c58a75bfdcdf52c5547c4e48ec0e255d3b4ff Mon Sep 17 00:00:00 2001 From: Gus Date: Mon, 8 Jan 2024 11:49:19 -0600 Subject: [PATCH 06/30] feat(api): create user model --- apps/api/src/models/.gitkeep | 0 apps/api/src/models/user.ts | 4 ++++ 2 files changed, 4 insertions(+) delete mode 100644 apps/api/src/models/.gitkeep create mode 100644 apps/api/src/models/user.ts diff --git a/apps/api/src/models/.gitkeep b/apps/api/src/models/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/api/src/models/user.ts b/apps/api/src/models/user.ts new file mode 100644 index 00000000..6ac7896e --- /dev/null +++ b/apps/api/src/models/user.ts @@ -0,0 +1,4 @@ +export type User = { + username: string; + password: string; +}; From 26140e1c605f869b5bef0a9c62fcdd5f68fa51f3 Mon Sep 17 00:00:00 2001 From: Gus Date: Mon, 8 Jan 2024 11:52:50 -0600 Subject: [PATCH 07/30] feat(api): create auth controller --- apps/api/src/controllers/auth.ts | 97 ++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 apps/api/src/controllers/auth.ts diff --git a/apps/api/src/controllers/auth.ts b/apps/api/src/controllers/auth.ts new file mode 100644 index 00000000..c5b90d02 --- /dev/null +++ b/apps/api/src/controllers/auth.ts @@ -0,0 +1,97 @@ +import bcrypt from 'bcrypt'; +import jwt from 'jsonwebtoken'; + +import { User } from '../models/user'; + +const users: User[] = []; + +const register = async (req, res) => { + const { username, password } = req.body; + + // Check that we have the correct payload + if (!username || !password) { + return res.status(400).json({ + message: 'Username and password are required' + }); + } + + // Check that we don't have duplicates + const duplicate = users.find((u) => u.username === username); + if (duplicate) { + return res.sendStatus(409); + } + + try { + // Encrypt the password + const hashedPassword = await bcrypt.hash(password, 10); + + // Store new user + users.push({ username, password: hashedPassword }); + + res.status(201).json({ message: 'User registered successfully' }); + } catch (e) { + res.status(500).json({ message: e.message }); + } +}; + +const login = async (req, res) => { + const { username, password } = req.body; + + // Check that we have the correct payload + if (!username || !password) { + return res.status(400).json({ + message: 'Username and password are required' + }); + } + + // Retrieve user + const user = users.find((u) => u.username === username); + + // Check if we found the user and the password matches + if (!user || !(await bcrypt.compare(password, user.password))) { + return res.status(401).json({ message: 'Invalid credentials' }); + } + + // Generate access token and refresh token + const accessToken = jwt.sign({ username }, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '15m' }); + const refreshToken = jwt.sign({ username }, process.env.REFRESH_TOKEN_SECRET, { expiresIn: '7d' }); + + // Save refresh token + res.cookie('refreshToken', refreshToken, { + httpOnly: true, + maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days in milliseconds + }); + + res.json({ accessToken }); +}; + +const refresh = (req, res) => { + // Get refresh token from cookies + const refreshToken = req.cookies.refreshToken; + + if (!refreshToken) { + return res.status(401).json({ message: 'Unauthorized' }); + } + + jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (err, { username }) => { + if (err) { + // Invalid token + return res.status(403).json({ message: 'Forbidden' }); + } + + const accessToken = jwt.sign({ username }, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '15m' }); + res.json({ accessToken }); + }); +}; + +const logout = (req, res) => { + res.clearCookie('refreshToken'); + res.json({ message: 'Logged out successfully' }); +}; + +export default { + register, + login, + refresh, + logout +}; From 18cc352656c4b6f6effa94a194762b3dcea612cc Mon Sep 17 00:00:00 2001 From: Gus Date: Mon, 8 Jan 2024 11:54:05 -0600 Subject: [PATCH 08/30] feat(api): create auth route --- apps/api/src/routes/auth.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 apps/api/src/routes/auth.ts diff --git a/apps/api/src/routes/auth.ts b/apps/api/src/routes/auth.ts new file mode 100644 index 00000000..062e9bc9 --- /dev/null +++ b/apps/api/src/routes/auth.ts @@ -0,0 +1,15 @@ +import express from 'express'; + +import authController from '../controllers/auth'; + +const router = express.Router(); + +router.post('/register', authController.register); + +router.post('/login', authController.login); + +router.post('/refresh', authController.refresh); + +router.post('/logout', authController.logout); + +export default router; From 505f33416da05e67fc551bd6e4ffd1c74e1bf75e Mon Sep 17 00:00:00 2001 From: Gus Date: Mon, 8 Jan 2024 11:55:57 -0600 Subject: [PATCH 09/30] feat(api): add route to main file --- apps/api/src/main.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index a59003ab..dc2ee434 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -1,5 +1,6 @@ import express from 'express'; +import auth from './routes/auth'; import categories from './routes/categories'; const host = process.env.HOST ?? 'localhost'; @@ -8,6 +9,7 @@ const port = process.env.PORT ? Number(process.env.PORT) : 3000; const app = express(); app.use(express.json()); +app.use('/api/auth', auth); app.use('/api/categories', categories); app.listen(port, host, () => { From d67968b7ab93cc90069269d580223651dd6306f7 Mon Sep 17 00:00:00 2001 From: Gus Date: Mon, 8 Jan 2024 15:24:29 -0600 Subject: [PATCH 10/30] feat(api): add basic configuration --- apps/api/config/corsConfig.ts | 14 ++++++++++++++ apps/api/src/main.ts | 8 ++++++++ 2 files changed, 22 insertions(+) create mode 100644 apps/api/config/corsConfig.ts diff --git a/apps/api/config/corsConfig.ts b/apps/api/config/corsConfig.ts new file mode 100644 index 00000000..bd444ca9 --- /dev/null +++ b/apps/api/config/corsConfig.ts @@ -0,0 +1,14 @@ +const allowedOrigins = ['http://localhost:4200']; + +export const corsOptions = { + origin: (origin, callback) => { + if (allowedOrigins.indexOf(origin) !== -1) { + callback(null, true); + } else { + callback(new Error('Not allowed by CORS')); + } + }, + optionsSuccessStatus: 200 +}; + +export default { corsOptions }; diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index e5fad103..d6b88d7e 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -1,10 +1,18 @@ +import cors from 'cors'; import express from 'express'; +import helmet from 'helmet'; + +import { corsOptions } from '../config/corsConfig'; const host = process.env.HOST ?? 'localhost'; const port = process.env.PORT ? Number(process.env.PORT) : 3000; const app = express(); +app.use(express.json()); +app.use(helmet()); +app.use(cors(corsOptions)); + app.get('/', (req, res) => { res.send({ message: 'Hello MFEE!' }); }); From 7c1b48f46a406640936dd24be1f0984f6a83892c Mon Sep 17 00:00:00 2001 From: Gus Date: Mon, 8 Jan 2024 21:41:58 -0600 Subject: [PATCH 11/30] feat(api): add verify token middleware --- apps/api/src/main.ts | 4 +++- apps/api/src/middleware/.gitkeep | 0 apps/api/src/middleware/auth.ts | 24 ++++++++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) delete mode 100644 apps/api/src/middleware/.gitkeep create mode 100644 apps/api/src/middleware/auth.ts diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 6a099eb7..3ed56658 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -3,7 +3,7 @@ import express from 'express'; import helmet from 'helmet'; import { corsOptions } from '../config/corsConfig'; - +import { verifyToken } from './middleware/auth'; import auth from './routes/auth'; import categories from './routes/categories'; @@ -17,6 +17,8 @@ app.use(helmet()); app.use(cors(corsOptions)); app.use('/api/auth', auth); + +app.use(verifyToken); app.use('/api/categories', categories); app.listen(port, host, () => { diff --git a/apps/api/src/middleware/.gitkeep b/apps/api/src/middleware/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/api/src/middleware/auth.ts b/apps/api/src/middleware/auth.ts new file mode 100644 index 00000000..193a5a9e --- /dev/null +++ b/apps/api/src/middleware/auth.ts @@ -0,0 +1,24 @@ +import jwt from 'jsonwebtoken'; + +export const verifyToken = (req, res, next) => { + const authHeader = req.headers['authorization']; + + if (!authHeader) { + return res.status(401).json({ message: 'Unauthorized' }); + } + + const token = authHeader.split(' ')[1]; + jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => { + if (err) { + // Invalid token + return res.status(403).json({ message: 'Forbidden' }); + } + + req.user = user; + next(); + }); +}; + +export default { + verifyToken +}; From d329c841fc7b84283c94ebff49ec7f38fc48d1fb Mon Sep 17 00:00:00 2001 From: Gus Date: Mon, 8 Jan 2024 22:09:53 -0600 Subject: [PATCH 12/30] fix(api): relocate cors config --- apps/api/{ => src}/config/corsConfig.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename apps/api/{ => src}/config/corsConfig.ts (100%) diff --git a/apps/api/config/corsConfig.ts b/apps/api/src/config/corsConfig.ts similarity index 100% rename from apps/api/config/corsConfig.ts rename to apps/api/src/config/corsConfig.ts From 65ac083de9ffca3b2166b45d01a7ed9184f7db37 Mon Sep 17 00:00:00 2001 From: Gus Date: Mon, 8 Jan 2024 22:27:14 -0600 Subject: [PATCH 13/30] fix(api): cors issue for local development --- apps/api/src/config/corsConfig.ts | 5 +++-- apps/api/src/main.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/api/src/config/corsConfig.ts b/apps/api/src/config/corsConfig.ts index bd444ca9..045cf517 100644 --- a/apps/api/src/config/corsConfig.ts +++ b/apps/api/src/config/corsConfig.ts @@ -1,8 +1,9 @@ -const allowedOrigins = ['http://localhost:4200']; +const allowedOrigins = ['http://localhost:4200', 'http://localhost:3000']; export const corsOptions = { origin: (origin, callback) => { - if (allowedOrigins.indexOf(origin) !== -1) { + // Note: origin will be undefined from same route in local development + if (allowedOrigins.indexOf(origin) !== -1 || !origin) { callback(null, true); } else { callback(new Error('Not allowed by CORS')); diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index d6b88d7e..bb421d83 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -2,7 +2,7 @@ import cors from 'cors'; import express from 'express'; import helmet from 'helmet'; -import { corsOptions } from '../config/corsConfig'; +import { corsOptions } from './config/corsConfig'; const host = process.env.HOST ?? 'localhost'; const port = process.env.PORT ? Number(process.env.PORT) : 3000; From 0aec3673e2164139fa9d47ed295e28c9cf944adb Mon Sep 17 00:00:00 2001 From: Gus Date: Mon, 8 Jan 2024 22:52:46 -0600 Subject: [PATCH 14/30] feat(api): add error handler middleware --- apps/api/src/main.ts | 3 +++ apps/api/src/middleware/errorHandler.ts | 9 +++++++++ 2 files changed, 12 insertions(+) create mode 100644 apps/api/src/middleware/errorHandler.ts diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index a7c0793d..7964793b 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -4,6 +4,7 @@ import helmet from 'helmet'; import { corsOptions } from './config/corsConfig'; import { verifyToken } from './middleware/auth'; +import { errorHandler } from './middleware/errorHandler'; import auth from './routes/auth'; import categories from './routes/categories'; @@ -21,6 +22,8 @@ app.use('/api/auth', auth); app.use(verifyToken); app.use('/api/categories', categories); +app.use(errorHandler); + app.listen(port, host, () => { console.log(`[ ready ] http://${host}:${port}`); }); diff --git a/apps/api/src/middleware/errorHandler.ts b/apps/api/src/middleware/errorHandler.ts new file mode 100644 index 00000000..90969d56 --- /dev/null +++ b/apps/api/src/middleware/errorHandler.ts @@ -0,0 +1,9 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const errorHandler = (err, req, res, next) => { + // Log this for debug purposes + console.error(err.stack); + // Return custom error to user + res.status(500).json({ error: 'Internal Server Error' }); +}; + +export default { errorHandler }; From ca4222e69fb9d0edaedbf870dcb2c837f9705140 Mon Sep 17 00:00:00 2001 From: Gus Date: Mon, 8 Jan 2024 22:54:00 -0600 Subject: [PATCH 15/30] chore(api): remove .gitkeep file from config --- apps/api/src/config/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 apps/api/src/config/.gitkeep diff --git a/apps/api/src/config/.gitkeep b/apps/api/src/config/.gitkeep deleted file mode 100644 index e69de29b..00000000 From 05c4fbd10b7d04467033b5a510f0431155cd51b9 Mon Sep 17 00:00:00 2001 From: Gus Date: Mon, 8 Jan 2024 23:29:19 -0600 Subject: [PATCH 16/30] feat(api): add connection to MongoDB --- apps/api/src/main.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 7964793b..daf9dc2e 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -1,6 +1,7 @@ import cors from 'cors'; import express from 'express'; import helmet from 'helmet'; +import mongoose from 'mongoose'; import { corsOptions } from './config/corsConfig'; import { verifyToken } from './middleware/auth'; @@ -24,6 +25,15 @@ app.use('/api/categories', categories); app.use(errorHandler); -app.listen(port, host, () => { - console.log(`[ ready ] http://${host}:${port}`); -}); +mongoose + .connect(process.env.MONGO_URL) + .then(() => { + console.log('Connected to MongoDB'); + + app.listen(port, host, () => { + console.log(`[ ready ] http://${host}:${port}`); + }); + }) + .catch((e) => { + console.error(e); + }); From 837ac4c11006dd876392d8328064c614c7d4e9dc Mon Sep 17 00:00:00 2001 From: Gus Date: Mon, 8 Jan 2024 23:34:05 -0600 Subject: [PATCH 17/30] feat(api): create category model --- apps/api/src/models/category.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 apps/api/src/models/category.ts diff --git a/apps/api/src/models/category.ts b/apps/api/src/models/category.ts new file mode 100644 index 00000000..e6775d12 --- /dev/null +++ b/apps/api/src/models/category.ts @@ -0,0 +1,18 @@ +import mongoose, { Document, Schema } from 'mongoose'; + +interface ICategory extends Document { + name: string; +} + +export const categorySchema = new Schema( + { + name: String + }, + { + timestamps: true + } +); + +const Category = mongoose.model('Category', categorySchema); + +export default Category; From 2eec543dec1a2fb2bc70c3e7d8e27c3cc07d3d36 Mon Sep 17 00:00:00 2001 From: Gus Date: Mon, 8 Jan 2024 23:41:24 -0600 Subject: [PATCH 18/30] refactor(api): update required property in model --- apps/api/src/models/category.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/api/src/models/category.ts b/apps/api/src/models/category.ts index e6775d12..c69bbc87 100644 --- a/apps/api/src/models/category.ts +++ b/apps/api/src/models/category.ts @@ -6,7 +6,10 @@ interface ICategory extends Document { export const categorySchema = new Schema( { - name: String + name: { + type: String, + required: [true, 'Property is required'] + } }, { timestamps: true From 78a1a00757eecf9e846721e59765c768c1e281ae Mon Sep 17 00:00:00 2001 From: Gus Date: Tue, 9 Jan 2024 00:25:42 -0600 Subject: [PATCH 19/30] refactor(api): connect controller with model --- apps/api/src/controllers/category.ts | 133 +++++++++++++-------------- 1 file changed, 65 insertions(+), 68 deletions(-) diff --git a/apps/api/src/controllers/category.ts b/apps/api/src/controllers/category.ts index e94d41cb..edb7e409 100644 --- a/apps/api/src/controllers/category.ts +++ b/apps/api/src/controllers/category.ts @@ -1,100 +1,97 @@ -// Initialize categories array to save data in memory -const categories = []; +import Category from '../models/category'; // Get all categories -const getCategories = (req, res) => { - // Return all the categories with a 200 status code - res.status(200).json(categories); +const getCategories = async (req, res) => { + try { + const categories = await Category.find(); + // Return all the categories with a 200 status code + res.status(200).json(categories); + } catch (error) { + const { message } = error; + res.status(500).json({ message }); + } }; // Get category by id -const getCategoryById = (req, res) => { +const getCategoryById = async (req, res) => { // Retrieve the id from the route params const { id } = req.params; - // Check if we have a category with that id - const category = categories.find((p) => p.id === id); - if (!category) { - // If we don't find the category return a 404 status code with a message - return res.status(404).json({ message: 'Category not found' }); - // Note: Remember that json method doesn't interrupt the workflow - // therefore is important to add a "return" to break the process + try { + // Check if we have a category with that id + const category = await Category.findById(id); + + if (!category) { + // If we don't find the category return a 404 status code with a message + return res.status(404).json({ message: 'Category not found' }); + // Note: Remember that json method doesn't interrupt the workflow + // therefore is important to add a "return" to break the process + } + + // Return the category with a 200 status code + res.status(200).json(category); + } catch (error) { + const { message } = error; + res.status(500).json({ message }); } - - // Return the category with a 200 status code - res.status(200).json(category); }; // Create category -const createCategory = (req, res) => { - // Retrieve the name from the request body - const { name } = req.body; - - if (!name) { - // If name is empty or undefined return a 400 status code with a message - return res.status(400).json({ message: 'The name is required.' }); +const createCategory = async (req, res) => { + try { + const category = await Category.create(req.body); + // Return the created category with a 201 status code + res.status(201).json(category); + } catch (error) { + const { message } = error; + res.status(500).json({ message }); } - - // Generate a new category - const newCategory = { - id: Date.now().toString(), // Convert id to string to match the value in get by id endpoint - name - }; - // Add the new category to our array - categories.push(newCategory); - - // Return the created category with a 201 status code - res.status(201).json(newCategory); }; // Update category -const updateCategory = (req, res) => { +const updateCategory = async (req, res) => { // Retrieve the id from the route params const { id } = req.params; - // Retrieve the index of the category in the array - const categoryIndex = categories.findIndex((p) => p.id === id); - // "findIndex" will return -1 if there is no match - if (categoryIndex === -1) { - // If we don't find the category return a 404 status code with a message - return res.status(404).json({ message: 'Category not found' }); - } + try { + // Check and update if we have a category with that id + const category = await Category.findByIdAndUpdate(id, req.body); - // Generate a copy of our cateogory - const updatedCategory = { ...categories[categoryIndex] }; - // Retrieve the name from the request body - const { name } = req.body; - - // Check if we have a name, if so update the property - if (name) { - updatedCategory.name = name; + // If we don't find the category return a 404 status code with a message + if (!category) { + return res.status(404).json({ message: 'Category not found' }); + } + + // Get the new values of the category + const updatedCategory = await Category.findById(id); + // Return the updated category with a 200 status code + res.status(200).json(updatedCategory); + } catch (error) { + const { message } = error; + res.status(500).json({ message }); } - - // Update the category in our array - categories[categoryIndex] = updatedCategory; - - // Return the updated category with a 200 status code - res.status(200).json(updatedCategory); }; // Delete category -const deleteCategory = (req, res) => { +const deleteCategory = async (req, res) => { // Retrieve the id from the route params const { id } = req.params; - // Retrieve the index of the category in the array - const categoryIndex = categories.findIndex((p) => p.id === id); - // "findIndex" will return -1 if there is no match - if (categoryIndex === -1) { + try { + // Check and delete if we have a category with that id + const category = await Category.findByIdAndDelete(id); + // If we don't find the category return a 404 status code with a message - return res.status(404).json({ message: 'Category not found' }); + if (!category) { + return res.status(404).json({ message: 'Category not found' }); + } + + // Return a 200 status code + res.status(200).json(category); + } catch (error) { + const { message } = error; + res.status(500).json({ message }); } - - // Remove the category from the array - categories.splice(categoryIndex, 1); - - // Return a 204 status code - res.status(204).send(); }; export default { From 4e2e2b7a04860d9a210d5d1bca02254f2d9f163e Mon Sep 17 00:00:00 2001 From: Gus Date: Tue, 9 Jan 2024 12:05:36 -0600 Subject: [PATCH 20/30] chore(api): add session 01 challenges --- apps/api/README.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/apps/api/README.md b/apps/api/README.md index 6066cc0e..ba28da48 100644 --- a/apps/api/README.md +++ b/apps/api/README.md @@ -21,7 +21,30 @@ ## Challenges -### Session * +### Session 01 + +- Create `route` for `posts` endpoint with the following methods: + - `GET /posts` Return an array of all the posts with status code 200 + - `GET /posts/category/:category` Return an array of all the posts by category with status code 200 + - `GET /posts/:id` Return a post by id with category object and each comment object in the array with status code 200 + - `POST /posts` Create a new post and return the created post with status code 201 + - `POST /posts/:id/comments` Create a comment inside the post and return the comment with status code 201 + - `PATCH /posts/:id` Update post information and return the updated post with status code 200 + - `DELETE /posts/:id` Delete the post and return the deleted post with status code 200 or 204 if you decide to not return anything + * *Add 404 validation where needed* + +- Post model + - id: string + - title: string + - image: string + - description: string + - category: string *Id of the category* + - comments: array *Array of comment ids* + +- Comment model + - id: string + - author: string + - content: string ## How to From bedf0596a58648b9411a2b02caeb6b963b9327b4 Mon Sep 17 00:00:00 2001 From: Gus Date: Tue, 9 Jan 2024 12:08:59 -0600 Subject: [PATCH 21/30] chore(api): add session 02 challenges --- apps/api/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/api/README.md b/apps/api/README.md index ba28da48..f172a610 100644 --- a/apps/api/README.md +++ b/apps/api/README.md @@ -46,6 +46,10 @@ - author: string - content: string +### Session 02 + +- Refactor the code from last session to add a post controller + ## How to ### Run postman collection From ee6fae4cdc252a4b78ea5c34c8fe7b180bca4bf4 Mon Sep 17 00:00:00 2001 From: Gus Date: Tue, 9 Jan 2024 12:12:08 -0600 Subject: [PATCH 22/30] chore(api): add session 03 challenges --- apps/api/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/api/README.md b/apps/api/README.md index f172a610..c428f832 100644 --- a/apps/api/README.md +++ b/apps/api/README.md @@ -50,6 +50,10 @@ - Refactor the code from last session to add a post controller +### Session 03 + +- N/A + ## How to ### Run postman collection From 072672f7be407cd23fc83fbf74cc0eb2240bfaee Mon Sep 17 00:00:00 2001 From: Gus Date: Tue, 9 Jan 2024 12:13:36 -0600 Subject: [PATCH 23/30] chore(api): add session 04 challenges --- apps/api/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/api/README.md b/apps/api/README.md index c428f832..416f7e97 100644 --- a/apps/api/README.md +++ b/apps/api/README.md @@ -54,6 +54,10 @@ - N/A +### Session 04 + +- N/A + ## How to ### Run postman collection From 230782f8f4fcc1e69e195384cfbcdd593fe4d445 Mon Sep 17 00:00:00 2001 From: Gus Date: Tue, 9 Jan 2024 12:22:31 -0600 Subject: [PATCH 24/30] chore(api): add session 05 challenges --- apps/api/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/api/README.md b/apps/api/README.md index 416f7e97..658178b6 100644 --- a/apps/api/README.md +++ b/apps/api/README.md @@ -58,6 +58,16 @@ - N/A +### Session 05 + +- Create MongoDB database +- Connect to MongoDB database using mongoose +- Create models for Post and Comment +- Refactor the controller to retrieve information from database + - *Tip: Use `populate` method to get data from reference id* +- **Extra** + - Remove post comments from database when you delete the post + ## How to ### Run postman collection From 901ea2497ca95fb123d22b2555f7fd0a9418561b Mon Sep 17 00:00:00 2001 From: Gus Date: Wed, 10 Jan 2024 20:12:17 -0600 Subject: [PATCH 25/30] fix(api): refactor getCategory for future use --- apps/api/src/routes/categories.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/api/src/routes/categories.ts b/apps/api/src/routes/categories.ts index f14d6e29..929c5552 100644 --- a/apps/api/src/routes/categories.ts +++ b/apps/api/src/routes/categories.ts @@ -1,5 +1,9 @@ import express from 'express'; +export const getCategory = (id: string) => { + return categories.find((p) => p.id === id); +}; + const router = express.Router(); // Initialize categories array to save data in memory const categories = []; @@ -15,7 +19,7 @@ router.get('/:id', (req, res) => { // Retrieve the id from the route params const { id } = req.params; // Check if we have a category with that id - const category = categories.find((p) => p.id === id); + const category = getCategory(id); if (!category) { // If we don't find the category return a 404 status code with a message From 1d0c66299561005cc3172531dce48a8bedaab6a7 Mon Sep 17 00:00:00 2001 From: Gus Date: Wed, 10 Jan 2024 22:45:51 -0600 Subject: [PATCH 26/30] refactor(api): update findByIdAndUpdate options --- apps/api/src/controllers/category.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/api/src/controllers/category.ts b/apps/api/src/controllers/category.ts index edb7e409..cf881549 100644 --- a/apps/api/src/controllers/category.ts +++ b/apps/api/src/controllers/category.ts @@ -55,17 +55,15 @@ const updateCategory = async (req, res) => { try { // Check and update if we have a category with that id - const category = await Category.findByIdAndUpdate(id, req.body); + const category = await Category.findByIdAndUpdate(id, req.body, { new: true }); // If we don't find the category return a 404 status code with a message if (!category) { return res.status(404).json({ message: 'Category not found' }); } - // Get the new values of the category - const updatedCategory = await Category.findById(id); // Return the updated category with a 200 status code - res.status(200).json(updatedCategory); + res.status(200).json(category); } catch (error) { const { message } = error; res.status(500).json({ message }); From 5eb29eb5b3605c3ff0c574f9648300d8b65d281d Mon Sep 17 00:00:00 2001 From: Gus Date: Wed, 10 Jan 2024 23:29:35 -0600 Subject: [PATCH 27/30] fix(api): add message to 409 error in auth register --- apps/api/src/controllers/auth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/controllers/auth.ts b/apps/api/src/controllers/auth.ts index c5b90d02..96bb74b5 100644 --- a/apps/api/src/controllers/auth.ts +++ b/apps/api/src/controllers/auth.ts @@ -18,7 +18,7 @@ const register = async (req, res) => { // Check that we don't have duplicates const duplicate = users.find((u) => u.username === username); if (duplicate) { - return res.sendStatus(409); + return res.status(409).json({ message: 'User already exist' }); } try { From fb234cb65cf44de2ad1bb1fc019a976ebd4c6c07 Mon Sep 17 00:00:00 2001 From: NiverMtz Date: Fri, 30 Aug 2024 02:06:11 -0600 Subject: [PATCH 28/30] feat(session-01): create posts routes --- apps/api/src/main.ts | 2 + apps/api/src/routes/posts.ts | 106 +++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 apps/api/src/routes/posts.ts diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index daf9dc2e..96888e1e 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -8,6 +8,7 @@ import { verifyToken } from './middleware/auth'; import { errorHandler } from './middleware/errorHandler'; import auth from './routes/auth'; import categories from './routes/categories'; +import posts from './routes/posts'; const host = process.env.HOST ?? 'localhost'; const port = process.env.PORT ? Number(process.env.PORT) : 3000; @@ -22,6 +23,7 @@ app.use('/api/auth', auth); app.use(verifyToken); app.use('/api/categories', categories); +app.use('/api/posts', posts); app.use(errorHandler); diff --git a/apps/api/src/routes/posts.ts b/apps/api/src/routes/posts.ts new file mode 100644 index 00000000..299b44c8 --- /dev/null +++ b/apps/api/src/routes/posts.ts @@ -0,0 +1,106 @@ +import express from 'express'; + +const router = express.Router(); +// Initialize categories array to save data in memory +const posts = []; + +// Get all posts +router.get('/', (req, res) => { + res.status(200).json(posts); +}); + +// Get posts by category +router.get('/category/:category', (req, res) => { + const { category } = req.params; + + const postByCategory = posts.filter((p) => p.category === category); + + res.status(200).json(postByCategory); +}); + +// Get post by id +router.get('/:id', (req, res) => { + const { id } = req.params; + const post = posts.find((p) => p.id === id); + + if (!post) { + return res.status(404).json({ message: 'Post not found' }); + } + res.status(200).json(post); +}); + +// Create post +router.post('/', (req, res) => { + const { title, image, description, category } = req.body; + + if (!title || !image || !description || !category) { + return res.status(400).json({ message: 'All fields are required.' }); + } + + const newPost = { + id: Date.now().toString(), + ...req.body + }; + + posts.push(newPost); + + res.status(201).json(newPost); +}); + +// Create post comment +router.post('/:id/comments', (req, res) => { + const { id } = req.params; + const { author, content } = req.body; + + const post = posts.find((p) => p.id === id); + if (!post) { + return res.status(404).json({ message: 'Post not found' }); + } + + const newComment = { + id: new Date().toString(), + author, + content + }; + + if (!post.comments) post.comments = []; + + post.comments.push(newComment); + + res.status(201).json(newComment); +}); + +// Update post +router.patch('/:id', (req, res) => { + const { id } = req.params; + const categoryIndex = posts.findIndex((p) => p.id === id); + + if (categoryIndex === -1) { + return res.status(404).json({ message: 'Post not found' }); + } + + const updatedCategory = { ...posts[categoryIndex] }; + for (const key in updatedCategory) { + if (req.body[key]) updatedCategory[key] = req.body[key]; + } + + posts[categoryIndex] = { ...updatedCategory }; + + res.status(200).json(updatedCategory); +}); + +// Delete post +router.delete('/:id', (req, res) => { + const { id } = req.params; + const categoryIndex = posts.findIndex((p) => p.id === id); + + if (categoryIndex === -1) { + return res.status(404).json({ message: 'Post not found' }); + } + + posts.splice(categoryIndex, 1); + + res.status(204).send(); +}); + +export default router; From 145f5f8fc8e71b67b50958da1c6925b416f694a5 Mon Sep 17 00:00:00 2001 From: NiverMtz Date: Sat, 31 Aug 2024 16:48:26 -0600 Subject: [PATCH 29/30] feat(session-02): refact router for controllers modularization --- apps/api/src/controllers/post.ts | 112 +++++++++++++++++++++++++++++++ apps/api/src/models/post.ts | 14 ++++ apps/api/src/routes/posts.ts | 97 +++----------------------- 3 files changed, 135 insertions(+), 88 deletions(-) create mode 100644 apps/api/src/controllers/post.ts create mode 100644 apps/api/src/models/post.ts diff --git a/apps/api/src/controllers/post.ts b/apps/api/src/controllers/post.ts new file mode 100644 index 00000000..2842e1d2 --- /dev/null +++ b/apps/api/src/controllers/post.ts @@ -0,0 +1,112 @@ +// Initialize categories array to save data in memory +const posts = []; + +// Get all posts +const getAllPosts = (req, res) => { + res.status(200).json(posts); +}; + +// Get post by category +const getPostByCategory = (req, res) => { + const { category } = req.params; + + const postByCategory = posts.filter((p) => p.category === category); + + res.status(200).json(postByCategory); +}; + +// Get post by id +const getPostById = (req, res) => { + const { id } = req.params; + const post = posts.find((p) => p.id === id); + + if (!post) { + return res.status(404).json({ message: 'Post not found' }); + } + res.status(200).json(post); +}; + +// Create post +const createPost = (req, res) => { + const { title, image, description, category } = req.body; + + if (!title || !image || !description || !category) { + return res.status(400).json({ message: 'All fields are required.' }); + } + + const newPost = { + id: Date.now().toString(), + ...req.body, + comments: [] + }; + + posts.push(newPost); + + res.status(201).json(newPost); +}; + +// Create post comment +const createPostComment = (req, res) => { + const { id } = req.params; + const { author, content } = req.body; + + const post = posts.find((p) => p.id === id); + if (!post) { + return res.status(404).json({ message: 'Post not found' }); + } + + const newComment = { + id: Date.now().toString(), + author, + content + }; + + if (!post.comments) post.comments = []; + + post.comments.push(newComment); + + res.status(201).json(newComment); +}; + +// Update post +const updatePost = (req, res) => { + const { id } = req.params; + const categoryIndex = posts.findIndex((p) => p.id === id); + + if (categoryIndex === -1) { + return res.status(404).json({ message: 'Post not found' }); + } + + const updatedCategory = { ...posts[categoryIndex] }; + for (const key in updatedCategory) { + if (req.body[key]) updatedCategory[key] = req.body[key]; + } + + posts[categoryIndex] = { ...updatedCategory }; + + res.status(200).json(updatedCategory); +}; + +// Delete post +const deletePost = (req, res) => { + const { id } = req.params; + const categoryIndex = posts.findIndex((p) => p.id === id); + + if (categoryIndex === -1) { + return res.status(404).json({ message: 'Post not found' }); + } + + posts.splice(categoryIndex, 1); + + res.status(204).send(); +}; + +export default { + getAllPosts, + getPostByCategory, + getPostById, + createPost, + createPostComment, + updatePost, + deletePost +}; diff --git a/apps/api/src/models/post.ts b/apps/api/src/models/post.ts new file mode 100644 index 00000000..6ff57786 --- /dev/null +++ b/apps/api/src/models/post.ts @@ -0,0 +1,14 @@ +export type Post = { + id: string; + title: string; + image: string; + description: string; + category: string; + comments: Array; +}; + +export type Comment = { + id: string; + author: string; + content: string; +}; diff --git a/apps/api/src/routes/posts.ts b/apps/api/src/routes/posts.ts index 299b44c8..c10cf9f1 100644 --- a/apps/api/src/routes/posts.ts +++ b/apps/api/src/routes/posts.ts @@ -1,106 +1,27 @@ import express from 'express'; +import postController from '../controllers/post'; const router = express.Router(); -// Initialize categories array to save data in memory -const posts = []; // Get all posts -router.get('/', (req, res) => { - res.status(200).json(posts); -}); +router.get('/', postController.getAllPosts); -// Get posts by category -router.get('/category/:category', (req, res) => { - const { category } = req.params; - - const postByCategory = posts.filter((p) => p.category === category); - - res.status(200).json(postByCategory); -}); +// Get post by category +router.get('/category/:category', postController.getPostByCategory); // Get post by id -router.get('/:id', (req, res) => { - const { id } = req.params; - const post = posts.find((p) => p.id === id); - - if (!post) { - return res.status(404).json({ message: 'Post not found' }); - } - res.status(200).json(post); -}); +router.get('/:id', postController.getPostById); // Create post -router.post('/', (req, res) => { - const { title, image, description, category } = req.body; - - if (!title || !image || !description || !category) { - return res.status(400).json({ message: 'All fields are required.' }); - } - - const newPost = { - id: Date.now().toString(), - ...req.body - }; - - posts.push(newPost); - - res.status(201).json(newPost); -}); +router.post('/', postController.createPost); // Create post comment -router.post('/:id/comments', (req, res) => { - const { id } = req.params; - const { author, content } = req.body; - - const post = posts.find((p) => p.id === id); - if (!post) { - return res.status(404).json({ message: 'Post not found' }); - } - - const newComment = { - id: new Date().toString(), - author, - content - }; - - if (!post.comments) post.comments = []; - - post.comments.push(newComment); - - res.status(201).json(newComment); -}); +router.post('/:id/comments', postController.createPostComment); // Update post -router.patch('/:id', (req, res) => { - const { id } = req.params; - const categoryIndex = posts.findIndex((p) => p.id === id); - - if (categoryIndex === -1) { - return res.status(404).json({ message: 'Post not found' }); - } - - const updatedCategory = { ...posts[categoryIndex] }; - for (const key in updatedCategory) { - if (req.body[key]) updatedCategory[key] = req.body[key]; - } - - posts[categoryIndex] = { ...updatedCategory }; - - res.status(200).json(updatedCategory); -}); +router.patch('/:id', postController.updatePost); // Delete post -router.delete('/:id', (req, res) => { - const { id } = req.params; - const categoryIndex = posts.findIndex((p) => p.id === id); - - if (categoryIndex === -1) { - return res.status(404).json({ message: 'Post not found' }); - } - - posts.splice(categoryIndex, 1); - - res.status(204).send(); -}); +router.delete('/:id', postController.deletePost); export default router; From cde7597eeeecd1845519e122e338033b10aa9521 Mon Sep 17 00:00:00 2001 From: NiverMtz Date: Sat, 31 Aug 2024 21:53:31 -0600 Subject: [PATCH 30/30] feat(session-05): mongodb integration --- apps/api/.env | 6 +- apps/api/src/controllers/category.ts | 1 - apps/api/src/controllers/post.ts | 156 +++++++++++++++------------ apps/api/src/main.ts | 1 - apps/api/src/models/comment.ts | 26 +++++ apps/api/src/models/post.ts | 53 +++++++-- apps/api/src/routes/posts.ts | 10 +- 7 files changed, 166 insertions(+), 87 deletions(-) create mode 100644 apps/api/src/models/comment.ts diff --git a/apps/api/.env b/apps/api/.env index ecaf5a7c..7fdedf23 100644 --- a/apps/api/.env +++ b/apps/api/.env @@ -1,3 +1,3 @@ -ACCESS_TOKEN_SECRET=[YOUR_ACCESS_TOKEN_SECRET_HERE] -REFRESH_TOKEN_SECRET=[YOUR_REFRESH_TOKEN_SECRET_HERE] -MONGO_URL=[YOUR_MONGO_CONNECTION_STRING_HERE] \ No newline at end of file +ACCESS_TOKEN_SECRET=62f7814ecb7bd8eed882d0c25403dd0b390dc07673ba3798ed22fc6db3e480750b8f9f6ee845e9e138c7047ecfd7d13c95d081821c1dd85333b53cedac0c7b24 +REFRESH_TOKEN_SECRET=3bcad700ce9c447d3fd5f1af746d5b597ed01ddb6884693ed38a2c7913b2bf3dd69b6c1e974a6d1760c076b55b6ff0c3d92b218faac728e3db31d0474b6888e0 +MONGO_URL=mongodb+srv://nivermartinez96:tSzduB9XoRg2oq1P@cluster0.bhfou.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0 diff --git a/apps/api/src/controllers/category.ts b/apps/api/src/controllers/category.ts index cf881549..a056adad 100644 --- a/apps/api/src/controllers/category.ts +++ b/apps/api/src/controllers/category.ts @@ -27,7 +27,6 @@ const getCategoryById = async (req, res) => { // Note: Remember that json method doesn't interrupt the workflow // therefore is important to add a "return" to break the process } - // Return the category with a 200 status code res.status(200).json(category); } catch (error) { diff --git a/apps/api/src/controllers/post.ts b/apps/api/src/controllers/post.ts index 2842e1d2..51893daa 100644 --- a/apps/api/src/controllers/post.ts +++ b/apps/api/src/controllers/post.ts @@ -1,104 +1,128 @@ -// Initialize categories array to save data in memory -const posts = []; +import Post from '../models/post'; +import Comment from '../models/comment'; // Get all posts -const getAllPosts = (req, res) => { - res.status(200).json(posts); +const getAllPosts = async (req, res) => { + try { + const posts = await Post.find(); + res.status(200).json(posts); + } catch (error) { + const { message } = error; + res.status(500).json({ message }); + } }; // Get post by category -const getPostByCategory = (req, res) => { +const getPostByCategory = async (req, res) => { const { category } = req.params; + try { + const posts = await Post.find({ category }); - const postByCategory = posts.filter((p) => p.category === category); + if (!posts) { + return res.status(404).json({ message: 'No posts found for this category' }); + } - res.status(200).json(postByCategory); + res.status(200).json(posts); + } catch (error) { + const { message } = error; + res.status(500).json({ message }); + } }; -// Get post by id -const getPostById = (req, res) => { +// // Get post by id +const getPostById = async (req, res) => { const { id } = req.params; - const post = posts.find((p) => p.id === id); + try { + const post = await Post.findById(id).populate('comments'); + + if (!post) { + return res.status(404).json({ message: 'Post not found' }); + } - if (!post) { - return res.status(404).json({ message: 'Post not found' }); + res.status(200).json(post); + } catch (error) { + const { message } = error; + res.status(500).json({ message }); } - res.status(200).json(post); }; -// Create post -const createPost = (req, res) => { - const { title, image, description, category } = req.body; - - if (!title || !image || !description || !category) { - return res.status(400).json({ message: 'All fields are required.' }); +// // Create post +const createPost = async (req, res) => { + try { + const post = await Post.create(req.body); + res.status(201).json(post); + } catch (error) { + const { message } = error; + res.status(500).json({ message }); } - - const newPost = { - id: Date.now().toString(), - ...req.body, - comments: [] - }; - - posts.push(newPost); - - res.status(201).json(newPost); }; -// Create post comment -const createPostComment = (req, res) => { - const { id } = req.params; +// Crear un comentario y agregarlo al post +const createPostComment = async (req, res) => { + const postId = req.params.id; const { author, content } = req.body; - const post = posts.find((p) => p.id === id); - if (!post) { - return res.status(404).json({ message: 'Post not found' }); - } + try { + const newComment = new Comment({ author, content }); + await newComment.save(); - const newComment = { - id: Date.now().toString(), - author, - content - }; + const post = await Post.findById(postId); - if (!post.comments) post.comments = []; + if (!post) { + return res.status(404).json({ message: 'Post not found' }); + } - post.comments.push(newComment); + post.comments.push(newComment._id); + await post.save(); - res.status(201).json(newComment); -}; + const populatedComment = await Comment.findById(newComment._id); -// Update post -const updatePost = (req, res) => { - const { id } = req.params; - const categoryIndex = posts.findIndex((p) => p.id === id); + if (!populatedComment) { + return res.status(404).json({ message: 'Comment not found' }); + } - if (categoryIndex === -1) { - return res.status(404).json({ message: 'Post not found' }); + res.status(201).json(populatedComment); + } catch (error) { + const { message } = error; + res.status(500).json({ message }); } +}; - const updatedCategory = { ...posts[categoryIndex] }; - for (const key in updatedCategory) { - if (req.body[key]) updatedCategory[key] = req.body[key]; +// // Update post +const updatePost = async (req, res) => { + const { id } = req.params; + try { + const post = await Post.findByIdAndUpdate(id, req.body, { new: true }); + if (!post) { + return res.status(404).json({ message: 'Post not found' }); + } + + res.status(200).json(post); + } catch (error) { + const { message } = error; + res.status(500).json({ message }); } - - posts[categoryIndex] = { ...updatedCategory }; - - res.status(200).json(updatedCategory); }; -// Delete post -const deletePost = (req, res) => { +// // Delete post +const deletePost = async (req, res) => { const { id } = req.params; - const categoryIndex = posts.findIndex((p) => p.id === id); + try { + const post = await Post.findById(id); - if (categoryIndex === -1) { - return res.status(404).json({ message: 'Post not found' }); - } + if (!post) { + return res.status(404).json({ message: 'Post not found' }); + } - posts.splice(categoryIndex, 1); + await Comment.deleteMany({ _id: { $in: post.comments } }); - res.status(204).send(); + await Post.findByIdAndDelete(id); + + res.status(204).json({ message: 'Post and related comments deleted successfully' }); + } catch (error) { + const { message } = error; + res.status(500).json({ message }); + } }; export default { diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 96888e1e..7fd7feb7 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -2,7 +2,6 @@ import cors from 'cors'; import express from 'express'; import helmet from 'helmet'; import mongoose from 'mongoose'; - import { corsOptions } from './config/corsConfig'; import { verifyToken } from './middleware/auth'; import { errorHandler } from './middleware/errorHandler'; diff --git a/apps/api/src/models/comment.ts b/apps/api/src/models/comment.ts new file mode 100644 index 00000000..ac2cbdf8 --- /dev/null +++ b/apps/api/src/models/comment.ts @@ -0,0 +1,26 @@ +import mongoose, { Document, Schema } from 'mongoose'; + +interface IComment extends Document { + author: string; + content: string; +} + +export const commentSchema = new Schema( + { + author: { + type: String, + required: [true, 'Property is required'] + }, + content: { + type: String, + required: [true, 'Property is required'] + } + }, + { + timestamps: true + } +); + +const Comment = mongoose.model('Comment', commentSchema); + +export default Comment; diff --git a/apps/api/src/models/post.ts b/apps/api/src/models/post.ts index 6ff57786..658948a7 100644 --- a/apps/api/src/models/post.ts +++ b/apps/api/src/models/post.ts @@ -1,14 +1,45 @@ -export type Post = { - id: string; +import mongoose, { Document, Schema } from 'mongoose'; + +interface IPost extends Document { title: string; image: string; description: string; - category: string; - comments: Array; -}; - -export type Comment = { - id: string; - author: string; - content: string; -}; + category: Schema.Types.ObjectId; + comments: Schema.Types.ObjectId[]; +} + +export const postSchema = new Schema( + { + title: { + type: String, + required: [true, 'Property is required'] + }, + image: { + type: String, + required: [true, 'Property is required'] + }, + description: { + type: String, + required: [true, 'Property is required'] + }, + category: { + type: Schema.Types.ObjectId, + ref: 'Category', + required: [true, 'Property is required'] + }, + comments: [ + { + type: Schema.Types.ObjectId, + ref: 'Comment', + default: [] + } + ] + }, + { + timestamps: true + } +); + +const Post = mongoose.model('Post', postSchema); + +export default Post; diff --git a/apps/api/src/routes/posts.ts b/apps/api/src/routes/posts.ts index c10cf9f1..b420a607 100644 --- a/apps/api/src/routes/posts.ts +++ b/apps/api/src/routes/posts.ts @@ -9,19 +9,19 @@ router.get('/', postController.getAllPosts); // Get post by category router.get('/category/:category', postController.getPostByCategory); -// Get post by id +// // Get post by id router.get('/:id', postController.getPostById); -// Create post +// // Create post router.post('/', postController.createPost); -// Create post comment +// // Create post comment router.post('/:id/comments', postController.createPostComment); -// Update post +// // Update post router.patch('/:id', postController.updatePost); -// Delete post +// // Delete post router.delete('/:id', postController.deletePost); export default router;