From 9082fc4af3390829f7fc046ad185faaee33a0d85 Mon Sep 17 00:00:00 2001 From: Joslyn Manzi Karenzi Date: Tue, 7 May 2024 20:25:43 +0200 Subject: [PATCH] * feat(rbac): Implement role based access control -define roles and permissions for vendors and buyers -assign roles and permissions to users during registration or profile update -enforce role-based access control throughout the application -write comprehensive unit tests [Delivers #34] * feat(rbac): integrate rbac into user registration -integrate role based access control into user registration [Delivers #34] * feat(rbac): integrate rbac into user registration -integrate role based access control into user registration [Delivers #34] --------- Co-authored-by: ambroisegithub Social Logins (#45) * squashing commits implementing routes for auth create passport callback function adding new user from Google creating new user check if user is exist in db implementing cookie session Fix error of TypeError: req.session.regenerate is not a function using Passport fix secret keys remove Google client secret keys working on facebook strategy get email from fb login and update the scope after verification save the user into db add profile image in db fixing minor bugs fix minor bugs in codes after rebasing & updating some fts link social login with userModel Addong Google client keys & FB client key into yml send confrim email after register a new user send email after register from facebook fix minor bugs * fix minor errors * remove lints errors user register register user test register user testing fix register user testing fix register user testing fix Authentication for User Added slint changes removed mocha added new features added new features Solved comflicts changed file added changes added new Test added new Test resolved test cases resolved test cases implemented two-factor authentication for enhanced security implemented two-factor authentication for enhanced security check whether the usertype is vendor to proceed with 2FA test the 2fa authentication Design the database schema for storing seller products and related information create endpoints for category and also database schema fix routes for products & categories implement CRUD operations for category entity create swagger docs for category routes adds delete documentation for category routes complete documentation for category routes implementing craate new product route and its controller implementing get product route and its controller working on update function Extend API endpoints store reference ID of vendor ft-Product-Docs This PR add the product swagger docs and some test category bug-documentation-fixes Fixing the errors on the doumentation add new tests for buyers login Design the database schema for storing seller products and related information create endpoints for category and also database schema fix routes for products & categories implement CRUD operations for category entity create swagger docs for category routes adds delete documentation for category routes complete documentation for category routes implementing craate new product route and its controller implementing get product route and its controller working on update function Extend API endpoints store reference ID of vendor ft-Product-Docs This PR add the product swagger docs and some test category bug-documentation-fixes Fixing the errors on the doumentation store reference ID of vendor replacing try & catch with errorHandler middleware remove all try & catch blocks in product controller validating routes fix minor errors in test categories Category-testing add category testing cases init commit for prouct test product controller test complete product test merging all commits delete product data after testing add --detectOpenHandles flag --- package.json | 2 +- src/__test__/category.test.ts | 183 ++++++++++++++++ src/__test__/product.test.ts | 263 +++++++++++++++++++++++ src/__test__/testSetup.ts | 79 ++++++- src/__test__/userController.test.ts | 207 +++++++++--------- src/app.ts | 12 +- src/controller/categoryController.ts | 128 ++++++++++++ src/controller/productController.ts | 290 ++++++++++++++++++++++++++ src/controller/userController.ts | 53 +++-- src/database/models/categoryEntity.ts | 36 ++++ src/database/models/index.ts | 1 + src/database/models/productEntity.ts | 65 ++++++ src/database/models/roleEntity.ts | 6 +- src/database/models/userModel.ts | 14 +- src/docs/2Fadocs.ts | 4 +- src/docs/categoryDocs.ts | 207 ++++++++++++++++++ src/docs/productDoc.ts | 186 +++++++++++++++++ src/docs/userAuth.ts | 4 +- src/docs/userRegisterDocs.ts | 41 +++- src/middlewares/authorize.ts | 35 +++- src/middlewares/errorHandler.ts | 24 ++- src/middlewares/isLoggedIn.ts | 33 +++ src/middlewares/passport-setup.ts | 2 +- src/routes/categoryRoutes.ts | 23 ++ src/routes/index.ts | 16 +- src/routes/productRoutes.ts | 26 +++ src/routes/userRoutes.ts | 22 +- 27 files changed, 1775 insertions(+), 187 deletions(-) create mode 100644 src/__test__/category.test.ts create mode 100644 src/__test__/product.test.ts create mode 100644 src/controller/categoryController.ts create mode 100644 src/controller/productController.ts create mode 100644 src/database/models/categoryEntity.ts create mode 100644 src/database/models/productEntity.ts create mode 100644 src/docs/categoryDocs.ts create mode 100644 src/docs/productDoc.ts create mode 100644 src/middlewares/isLoggedIn.ts create mode 100644 src/routes/categoryRoutes.ts create mode 100644 src/routes/productRoutes.ts diff --git a/package.json b/package.json index dc3f715f..c3b9ae67 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "build": "tsc", "lint": "eslint --config .eslintrc.json .", "format": "prettier --write .", - "test": "jest --no-cache", + "test": "jest --no-cache --detectOpenHandles", "test:ci": "jest --coverage --detectOpenHandles" }, "repository": { diff --git a/src/__test__/category.test.ts b/src/__test__/category.test.ts new file mode 100644 index 00000000..21b161bc --- /dev/null +++ b/src/__test__/category.test.ts @@ -0,0 +1,183 @@ +import request from 'supertest'; +import app from '../app'; +import { afterAllHook, beforeAllHook, getVendorToken } from './testSetup'; + +beforeAll(beforeAllHook); +afterAll(afterAllHook); + +describe('Category Creation Tests', () => { + beforeAll(async () => { + token = await getVendorToken(); + }); + let token: string; + let categoryId: number; + + it('should create a new category with valid data', async () => { + const categoryData = { + name: 'Test Category', + description: 'Test category description', + }; + + const response = await request(app) + .post('/api/v1/category') + .set('Authorization', `Bearer ${token}`) + .send(categoryData); + expect(response.status).toBe(201); + expect(response.body.message).toBe('Category successfully created'); + expect(response.body.data).toHaveProperty('id'); + expect(response.body.data).toHaveProperty('name', categoryData.name); + expect(response.body.data).toHaveProperty( + 'description', + categoryData.description + ); + categoryId = response.body.data.id; + }); + + it('should return a 400 status code if name is missing', async () => { + const invalidData = { + description: 'Test category description', + }; + + const response = await request(app) + .post('/api/v1/category') + .set('Authorization', `Bearer ${token}`) + .send(invalidData); + + expect(response.status).toBe(400); + expect(response.body.errors[0].msg).toBe('Category name is required'); + }); + + it('should return 400 if request data is invalid', async () => { + const invalidData = {}; + + const response = await request(app) + .put(`/api/v1/category/${categoryId}`) + .set('Authorization', `Bearer ${token}`) + .send(invalidData); + + expect(response.status).toBe(400); + expect(response.body.errors).toBeDefined(); + }); + + it('should return a 409 status code if category name already exists', async () => { + const existingCategoryData = { + name: 'Existing Category', + description: 'Existing category description', + }; + await request(app) + .post('/api/v1/category') + .set('Authorization', `Bearer ${token}`) + .send(existingCategoryData); + + const newCategoryData = { + name: 'Existing Category', + description: 'Existing category description', + }; + const response = await request(app) + .post('/api/v1/category') + .set('Authorization', `Bearer ${token}`) + .send(newCategoryData); + + expect(response.status).toBe(409); + expect(response.body.message).toBe('Category name already exists'); + }); + + it('should return all categories with status 200', async () => { + const response = await request(app).get('/api/v1/category'); + expect(response.status).toBe(200); + expect(response.body.message).toBe('Data retrieved successfully'); + expect(response.body.data).toBeDefined; + }); + + it('should return a category by ID with status 200', async () => { + const response = await request(app).get(`/api/v1/category/${categoryId}`); + + expect(response.status).toBe(200); + expect(response.body.message).toBe('Data retrieved successfully'); + expect(response.body.data).toBeDefined; + }); + + it('should return 404 if category is not found', async () => { + const nonExistentCategoryId = 9999; + + const response = await request(app).get( + `/api/v1/category/${nonExistentCategoryId}` + ); + + expect(response.status).toBe(404); + expect(response.body.message).toBe('Category Not Found'); + }); + + it('should update the category with status 200', async () => { + const updatedCategoryData = { + name: 'Updated Category Name', + description: 'Updated category description', + }; + + const response = await request(app) + .put(`/api/v1/category/${categoryId}`) + .set('Authorization', `Bearer ${token}`) + .send(updatedCategoryData); + + expect(response.status).toBe(200); + expect(response.body.message).toBe('Category successfully updated'); + expect(response.body.data.name).toBe(updatedCategoryData.name); + expect(response.body.data.description).toBe( + updatedCategoryData.description + ); + }); + + it('should return a 409 status code if category update name already exists', async () => { + const existingCategoryData = { + name: 'Existing Category', + description: 'Existing category description', + }; + await request(app) + .post('/api/v1/category') + .set('Authorization', `Bearer ${token}`) + .send(existingCategoryData); + + const updateCategoryData = { + name: 'Existing Category', + description: 'Existing category description', + }; + const response = await request(app) + .put(`/api/v1/category/${categoryId}`) + .set('Authorization', `Bearer ${token}`) + .send(updateCategoryData); + + expect(response.status).toBe(409); + expect(response.body.message).toBe('Category name already exists'); + }); + + it('should return 404 if category is not found', async () => { + const response = await request(app) + .put('/api/v1/category/9999') + .set('Authorization', `Bearer ${token}`) + .send({ + name: 'Updated Category Name', + description: 'Updated category description', + }); + + expect(response.status).toBe(404); + expect(response.body.message).toBe('Category Not Found'); + }); + + it('should delete the category with status 200', async () => { + const response = await request(app) + .delete(`/api/v1/category/${categoryId}`) + .set('Authorization', `Bearer ${token}`); + + expect(response.status).toBe(200); + expect(response.body.message).toBe('Category deleted successfully'); + }); + + it('should return 404 if category is not found', async () => { + const response = await request(app) + .delete('/api/v1/category/9999') + .set('Authorization', `Bearer ${token}`); + + expect(response.status).toBe(404); + expect(response.body.message).toBe('Category Not Found'); + }); +}); diff --git a/src/__test__/product.test.ts b/src/__test__/product.test.ts new file mode 100644 index 00000000..fc976ed8 --- /dev/null +++ b/src/__test__/product.test.ts @@ -0,0 +1,263 @@ +import request from 'supertest'; +import app from '../app'; +import { getVendorToken, afterAllHook, beforeAllHook } from './testSetup'; + +beforeAll(beforeAllHook); +afterAll(afterAllHook); + +describe('Product Controller Tests', () => { + let token: string; + let productId: number; + let categoryId: number; + + beforeAll(async () => { + token = await getVendorToken(); + }); + + it('should create a new product with valid data', async () => { + // create a category + const categoryData = { + name: 'Category', + description: 'category description', + }; + + const categoryResponse = await request(app) + .post('/api/v1/category') + .set('Authorization', `Bearer ${token}`) + .send(categoryData); + + categoryId = categoryResponse.body.data.id; + + const productData = { + name: 'New Product', + image: 'new_product.jpg', + gallery: [], + shortDesc: 'This is a new product', + longDesc: 'Detailed description of the new product', + categoryId: categoryId, + quantity: 10, + regularPrice: 5, + salesPrice: 4, + tags: ['tag1', 'tag2'], + type: 'Simple', + isAvailable: true, + }; + + const response = await request(app) + .post('/api/v1/product') + .set('Authorization', `Bearer ${token}`) + .send(productData); + expect(response.statusCode).toEqual(201); + expect(response.body.message).toEqual('Product successfully created'); + expect(response.body.data).toBeDefined(); + productId = response.body.data.id; + }); + + it('should return 409 if product name already exists', async () => { + const productData = { + name: 'New Product', + image: 'new_product.jpg', + gallery: [], + shortDesc: 'This is a new product', + longDesc: 'Detailed description of the new product', + categoryId: categoryId, + quantity: 10, + regularPrice: 5, + salesPrice: 4, + tags: ['tag1', 'tag2'], + type: 'Simple', + isAvailable: true, + }; + + const response = await request(app) + .post('/api/v1/product') + .set('Authorization', `Bearer ${token}`) + .send(productData); + + expect(response.statusCode).toEqual(409); + expect(response.body.message).toEqual('Product name already exists'); + }); + + it('should return 404 if category not found', async () => { + const nonExistentCategoryId = 999; + + const productData = { + name: 'Test Product', + image: 'test.jpg', + gallery: [], + shortDesc: 'A test product', + longDesc: 'Description of a test product', + categoryId: nonExistentCategoryId, + quantity: 10, + regularPrice: 20, + salesPrice: 18, + tags: ['tag1', 'tag2'], + type: 'Simple', + isAvailable: true, + }; + + const response = await request(app) + .post('/api/v1/product') + .set('Authorization', `Bearer ${token}`) + .send(productData); + + expect(response.statusCode).toBe(404); + expect(response.body.message).toBe('Category not found'); + }); + + it('should return validation errors for invalid product data', async () => { + const invalidProductData = { + name: '', + image: '', + }; + const response = await request(app) + .post('/api/v1/product') + .set('Authorization', `Bearer ${token}`) + .send(invalidProductData); + expect(response.statusCode).toEqual(400); + expect(response.body.errors).toBeDefined(); + }); + + it('should retrieve all products', async () => { + const response = await request(app).get('/api/v1/product'); + + expect(response.statusCode).toEqual(200); + expect(response.body.message).toEqual('Data retrieved successfully'); + expect(Array.isArray(response.body.data)).toBeTruthy(); + }); + + it('should retrieve a single product by ID', async () => { + const response = await request(app) + .get(`/api/v1/product/${productId}`) + .set('Authorization', `Bearer ${token}`); + + expect(response.statusCode).toEqual(200); + expect(response.body.message).toEqual('Data retrieved successfully'); + expect(response.body.data).toBeDefined(); + }); + + it('should update a product by ID', async () => { + const updatedProductData = { + name: 'Updated Product Name', + image: 'Updated.jpg', + gallery: [], + shortDesc: 'This is a updated', + longDesc: 'Detailed description of the Updated product', + categoryId: categoryId, + quantity: 3, + regularPrice: 10, + salesPrice: 7, + tags: ['tag1', 'tag2'], + type: 'Variable', + isAvailable: true, + }; + + const response = await request(app) + .put(`/api/v1/product/${productId}`) + .set('Authorization', `Bearer ${token}`) + .send(updatedProductData); + + expect(response.statusCode).toEqual(200); + expect(response.body.message).toEqual('Product successfully updated'); + expect(response.body.data).toBeDefined(); + }); + + it('should return a 404 for a non-existent product while updating', async () => { + const updatedProductData = { + name: 'Updated Product Name', + image: 'Updated.jpg', + gallery: [], + shortDesc: 'This is a updated', + longDesc: 'Detailed description of the Updated product', + categoryId: categoryId, + quantity: 3, + regularPrice: 10, + salesPrice: 7, + tags: ['tag1', 'tag2'], + type: 'Variable', + isAvailable: true, + }; + const nonExistentProductId = -999; + const response = await request(app) + .put(`/api/v1/product/${nonExistentProductId}`) + .set('Authorization', `Bearer ${token}`) + .send(updatedProductData); + + expect(response.statusCode).toEqual(404); + expect(response.body.message).toEqual('Product not found'); + }); + + it('should return 404 if category not found while updating', async () => { + const nonExistentCategoryId = 999; + + const productData = { + name: 'Test Product', + image: 'test.jpg', + gallery: [], + shortDesc: 'A test product', + longDesc: 'Description of a test product', + categoryId: nonExistentCategoryId, + quantity: 10, + regularPrice: 20, + salesPrice: 18, + tags: ['tag1', 'tag2'], + type: 'Simple', + isAvailable: true, + }; + + const response = await request(app) + .put(`/api/v1/product/${productId}`) + .set('Authorization', `Bearer ${token}`) + .send(productData); + + expect(response.statusCode).toBe(404); + expect(response.body.message).toBe('Category not found'); + }); + it('should return a 404 for a non-existent product', async () => { + const nonExistentProductId = -999; + const response = await request(app) + .get(`/api/v1/product/${nonExistentProductId}`) + .set('Authorization', `Bearer ${token}`); + expect(response.statusCode).toEqual(404); + expect(response.body.message).toEqual('Product not found'); + }); + + it('should return validation errors for invalid update data', async () => { + const invalidUpdateData = { + name: '', + }; + const response = await request(app) + .put(`/api/v1/product/${productId}`) + .set('Authorization', `Bearer ${token}`) + .send(invalidUpdateData); + expect(response.statusCode).toEqual(400); + expect(response.body.errors).toBeDefined(); + }); + + it('should delete a product by ID', async () => { + const response = await request(app) + .delete(`/api/v1/product/${productId}`) + .set('Authorization', `Bearer ${token}`); + + expect(response.statusCode).toEqual(200); + expect(response.body.message).toEqual('Product deleted successfully'); + }); + + it('should return a 404 for a non-existent product', async () => { + const nonExistentProductId = -999; + const response = await request(app) + .delete(`/api/v1/product/${nonExistentProductId}`) + .set('Authorization', `Bearer ${token}`); + expect(response.statusCode).toEqual(404); + expect(response.body.message).toEqual('Product Not Found'); + }); + + it('should delete all products', async () => { + const response = await request(app) + .delete('/api/v1/product') + .set('Authorization', `Bearer ${token}`); + + expect(response.statusCode).toEqual(200); + expect(response.body.message).toEqual('All product deleted successfully'); + }); +}); diff --git a/src/__test__/testSetup.ts b/src/__test__/testSetup.ts index e4a5759b..969af324 100644 --- a/src/__test__/testSetup.ts +++ b/src/__test__/testSetup.ts @@ -1,6 +1,10 @@ import { DbConnection } from '../database/index'; import UserModel from '../database/models/userModel'; import { Role } from '../database/models'; +import Category from '../database/models/categoryEntity'; +import Product from '../database/models/productEntity'; +import request from 'supertest'; +import app from '../app'; export async function beforeAllHook() { await DbConnection.instance.initializeDb(); @@ -8,17 +12,80 @@ export async function beforeAllHook() { // Get repositories const userRepository = await DbConnection.connection.getRepository(UserModel); const roleRepository = await DbConnection.connection.getRepository(Role); + const categoryRepository = + await DbConnection.connection.getRepository(Category); + const productRepository = + await DbConnection.connection.getRepository(Product); - // Delete all users and roles + // Delete all users,roles and categories await userRepository.createQueryBuilder().delete().execute(); await roleRepository.createQueryBuilder().delete().execute(); + await categoryRepository.createQueryBuilder().delete().execute(); + await productRepository.createQueryBuilder().delete().execute(); +} + +// Get Vendor Token function +export async function getVendorToken() { + const userRepository = await DbConnection.connection.getRepository(UserModel); + + const formData = { + name: 'Vendor', + permissions: ['test-permission1', 'test-permission2'], + }; + await request(app).post('/api/v1/roles/create_role').send(formData); + + const userData = { + firstName: 'Test', + lastName: 'User', + email: 'test1@gmail.com', + password: 'TestPassword123', + userType: 'vendor', + }; + await request(app).post('/api/v1/register').send(userData); + + const updatedUser = await userRepository.findOne({ + where: { email: userData.email }, + }); + if (updatedUser) { + updatedUser.isVerified = true; + await userRepository.save(updatedUser); + } + + const loginResponse = await request(app).post('/api/v1/login').send({ + email: userData.email, + password: userData.password, + }); + + expect(loginResponse.status).toBe(200); + expect(loginResponse.body.message).toBe( + 'Please provide the 2FA code sent to your email.' + ); + + const user = await userRepository.findOne({ + where: { email: userData.email }, + }); + if (!user) throw new Error('User not found'); + + const verifyResponse = await request(app) + .post(`/api/v1/verify2FA/${user.id}`) + .send({ + code: user.twoFactorCode, + }); + expect(verifyResponse.status).toBe(200); + expect(verifyResponse.body).toHaveProperty('token'); + expect(verifyResponse.body.token).toBeDefined(); + return verifyResponse.body.token; } export async function afterAllHook() { - const userRepository = DbConnection.connection.getRepository(UserModel); - const repository = await userRepository.clear(); - // eslint-disable-next-line no-console - console.log(repository); + await DbConnection.connection.transaction(async (transactionManager) => { + const userRepository = transactionManager.getRepository(UserModel); + const categoryRepository = transactionManager.getRepository(Category); + const productRepository = transactionManager.getRepository(Product); + await userRepository.createQueryBuilder().delete().execute(); + await categoryRepository.createQueryBuilder().delete().execute(); + await productRepository.createQueryBuilder().delete().execute(); + }); await DbConnection.instance.disconnectDb(); -} \ No newline at end of file +} diff --git a/src/__test__/userController.test.ts b/src/__test__/userController.test.ts index 69d33326..1a48549d 100644 --- a/src/__test__/userController.test.ts +++ b/src/__test__/userController.test.ts @@ -3,7 +3,7 @@ import app from '../app'; import { afterAllHook, beforeAllHook } from './testSetup'; import jwt from 'jsonwebtoken'; import dbConnection from '../database'; -import UserModel from '../database/models/userModel'; +import UserModel from '../database/models/userModel'; const userRepository = dbConnection.getRepository(UserModel); beforeAll(beforeAllHook); @@ -25,7 +25,10 @@ describe('User Registration Tests', () => { expect(response.body.user).toHaveProperty('firstName', userData.firstName); expect(response.body.user).toHaveProperty('lastName', userData.lastName); expect(response.body.user).toHaveProperty('email', userData.email); - expect(response.body.user).toHaveProperty('userType', response.body.user.userType); + expect(response.body.user).toHaveProperty( + 'userType', + response.body.user.userType + ); }); it('should return a 400 status code if validation fails', async () => { @@ -146,38 +149,41 @@ describe('User Registration Tests', () => { }); }); - describe('User Login Tests', () => { it('should log in a vendor with valid credentials', async () => { - const formData = { - name: 'Vendor', - permissions: ['test-permission1', 'test-permission2'], - }; - - await request(app).post('/api/v1/roles/create_role').send(formData); - - const userData = { - firstName: 'Test', - lastName: 'User', - email: 'test1@gmail.com', - password: 'TestPassword123', - userType: 'vendor' - }; + const formData = { + name: 'Vendor', + permissions: ['test-permission1', 'test-permission2'], + }; + + await request(app).post('/api/v1/roles/create_role').send(formData); + + const userData = { + firstName: 'Test', + lastName: 'User', + email: 'test1@gmail.com', + password: 'TestPassword123', + userType: 'vendor', + }; await request(app).post('/api/v1/register').send(userData); - const updatedUser = await userRepository.findOne({ where: { email: userData.email } }); - if (updatedUser) { - updatedUser.isVerified = true; - await userRepository.save(updatedUser); - - const loginResponse = await request(app).post('/api/v1/login').send({ - email: userData.email, - password: userData.password, - }); - - expect(loginResponse.status).toBe(200); - expect(loginResponse.body.message).toBe('Please provide the 2FA code sent to your email.'); - } + const updatedUser = await userRepository.findOne({ + where: { email: userData.email }, + }); + if (updatedUser) { + updatedUser.isVerified = true; + await userRepository.save(updatedUser); + + const loginResponse = await request(app).post('/api/v1/login').send({ + email: userData.email, + password: userData.password, + }); + + expect(loginResponse.status).toBe(200); + expect(loginResponse.body.message).toBe( + 'Please provide the 2FA code sent to your email.' + ); + } }); it('should verify the 2FA code for a vendor user', async () => { @@ -188,12 +194,14 @@ describe('User Login Tests', () => { password: 'TestPassword123', userType: 'vendor', }; - + // Register the user await request(app).post('/api/v1/register').send(userData); - + // Verify the user - let user = await userRepository.findOne({ where: { email: userData.email } }); + let user = await userRepository.findOne({ + where: { email: userData.email }, + }); if (user) { user.isVerified = true; await userRepository.save(user); @@ -202,119 +210,124 @@ describe('User Login Tests', () => { email: userData.email, password: userData.password, }); - expect(loginResponse.status).toBe(200); - expect(loginResponse.body.message).toBe('Please provide the 2FA code sent to your email.'); + expect(loginResponse.body.message).toBe( + 'Please provide the 2FA code sent to your email.' + ); user = await userRepository.findOne({ where: { email: userData.email } }); - + if (user) { - - const verifyResponse = await request(app).post(`/api/v1/verify2FA/${user.id}`).send({ - code: user.twoFactorCode, - }); - + const verifyResponse = await request(app) + .post(`/api/v1/verify2FA/${user.id}`) + .send({ + code: user.twoFactorCode, + }); expect(verifyResponse.status).toBe(200); expect(verifyResponse.body).toHaveProperty('token'); } }); - it('should log in a buyer with valid credentials', async () => { const formData = { name: 'Buyer', permissions: ['test-permission1', 'test-permission2'], }; - + // Create the role first - const roleResponse = await request(app).post('/api/v1/roles/create_role').send(formData); - + const roleResponse = await request(app) + .post('/api/v1/roles/create_role') + .send(formData); + const userData = { firstName: 'Test', lastName: 'User', email: 'test2@gmail.com', password: 'TestPassword123', - userType: roleResponse.body.id + userType: roleResponse.body.id, }; await request(app).post('/api/v1/register').send(userData); - - const updatedUser = await userRepository.findOne({ where: { email: userData.email } }); + + const updatedUser = await userRepository.findOne({ + where: { email: userData.email }, + }); if (updatedUser) { updatedUser.isVerified = true; await userRepository.save(updatedUser); - + const loginResponse = await request(app).post('/api/v1/login').send({ email: userData.email, password: userData.password, }); - + expect(loginResponse.status).toBe(200); expect(loginResponse.body.token).toBeDefined(); expect(loginResponse.body.message).toBe('Buyer Logged in successfully'); - + // Decode the token and check its properties const decodedToken = jwt.decode(loginResponse.body.token); - expect(decodedToken).toHaveProperty('userId'); + expect(decodedToken).toHaveProperty('user'); expect(decodedToken).toHaveProperty('iat'); expect(decodedToken).toHaveProperty('exp'); } }); it('should return a 401 status code if the email is not verified', async () => { - const userData = { - firstName: 'Test', - lastName: 'User', - email: 'test@gmail.com', - password: 'TestPassword123', - userType: 'buyer', - }; - await request(app).post('/api/v1/register').send(userData); - const updatedUser = await userRepository.findOne({ - where: { email: userData.email }, - }); - - if (updatedUser) { - updatedUser.isVerified = false; - await userRepository.save(updatedUser); - const loginResponse = await request(app).post('/api/v1/login').send({ - email: userData.email, - password: userData.password, - }); - - expect(loginResponse.status).toBe(401); - expect(loginResponse.body.message).toBe('Please verify your email. Confirmation link has been sent.'); // Corrected message - } + const userData = { + firstName: 'Test', + lastName: 'User', + email: 'test@gmail.com', + password: 'TestPassword123', + userType: 'buyer', + }; + await request(app).post('/api/v1/register').send(userData); + const updatedUser = await userRepository.findOne({ + where: { email: userData.email }, + }); + + if (updatedUser) { + updatedUser.isVerified = false; + await userRepository.save(updatedUser); + const loginResponse = await request(app).post('/api/v1/login').send({ + email: userData.email, + password: userData.password, + }); + + expect(loginResponse.status).toBe(401); + expect(loginResponse.body.message).toBe( + 'Please verify your email. Confirmation link has been sent.' + ); // Corrected message + } }); - + it('should return a 401 status code if the password does not match', async () => { - const userData = { - firstName: 'Test', - lastName: 'User', - email: 'test@gmail.com', - password: 'TestPassword123', - userType: 'buyer', - }; - await request(app).post('/api/v1/register').send(userData); - - const loginResponse = await request(app).post('/api/v1/login').send({ - email: userData.email, - password: 'IncorrectPassword', - }); - expect(loginResponse.status).toBe(401); - expect(loginResponse.body.message).toBe('Password does not match'); + const userData = { + firstName: 'Test', + lastName: 'User', + email: 'test@gmail.com', + password: 'TestPassword123', + userType: 'buyer', + }; + await request(app).post('/api/v1/register').send(userData); + + const loginResponse = await request(app).post('/api/v1/login').send({ + email: userData.email, + password: 'IncorrectPassword', + }); + expect(loginResponse.status).toBe(401); + expect(loginResponse.body.message).toBe('Password does not match'); }); it('should return a 404 status code if the user is not found', async () => { - const nonExistentEmail = 'nonexistent@example.com'; const loginResponse = await request(app).post('/api/v1/login').send({ - email: nonExistentEmail, - password: 'TestPassword123', + email: nonExistentEmail, + password: 'TestPassword123', }); - + expect(loginResponse.status).toBe(404); expect(loginResponse.body.message).toBe('User Not Found'); - }); - }); \ No newline at end of file + }); +}); diff --git a/src/app.ts b/src/app.ts index 7c3b5d2f..821bbf85 100644 --- a/src/app.ts +++ b/src/app.ts @@ -4,14 +4,14 @@ import morgan from 'morgan'; import swaggerUi from 'swagger-ui-express'; import swaggerSpec from './docs/swaggerconfig'; import 'reflect-metadata'; -import userRoute from './routes/userRoutes'; -import roleRoutes from './routes/roleRoutes'; +import router from './routes/index'; import fs from 'fs'; import path from 'path'; import authRoutes from './routes/auth-routes'; import cookieSession from 'cookie-session'; import passport from 'passport'; +import userRouter from './routes/userRoutes'; // Require Passport midleware require('./middlewares/passport-setup'); @@ -25,9 +25,7 @@ const logStream = fs.createWriteStream(path.join(__dirname, 'output.log'), { flags: 'a', }); -//Data Sanitation Against SQL injection - -//Data Sanitation Against SiteScripts +//Data Sanitation Against SQL injection morgan.token('type', function (req: Request) { return req.headers['content-type']; @@ -80,8 +78,8 @@ app.get('/', (req: Request, res: Response) => { }); // Middleware to handle all endpoint routes -app.use('/api/v1', userRoute); -app.use('/api/v1/roles', roleRoutes); +app.use('/api/v1', router); +app.use('/api/v1', userRouter); // Endpoints for serving social login app.use('/auth', authRoutes); diff --git a/src/controller/categoryController.ts b/src/controller/categoryController.ts new file mode 100644 index 00000000..92bddbff --- /dev/null +++ b/src/controller/categoryController.ts @@ -0,0 +1,128 @@ +import { Request, Response } from 'express'; +import dbConnection from '../database'; +import Category from '../database/models/categoryEntity'; +import { check, validationResult } from 'express-validator'; +import errorHandler from '../middlewares/errorHandler'; + +const categoryRepository = dbConnection.getRepository(Category); + +interface categoryRequestBody { + name: string; + description: string; +} + +const createCategoryRules = [ + check('name').isLength({ min: 1 }).withMessage('Category name is required'), + check('description') + .isLength({ min: 1 }) + .withMessage('Category description is required'), +]; + +export const createCategory = [ + ...createCategoryRules, + errorHandler(async (req: Request, res: Response) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + const { name, description } = req.body as categoryRequestBody; + + const existingCategory = await categoryRepository.findOne({ + where: { name }, + }); + if (existingCategory) { + return res.status(409).json({ message: 'Category name already exists' }); + } + const newCategory = new Category({ + name: name, + description: description, + }); + const updatedCategory = await categoryRepository.save(newCategory); + return res.status(201).json({ + message: 'Category successfully created', + data: updatedCategory, + }); + }), +]; + +export const getAllCategories = errorHandler( + async (req: Request, res: Response) => { + const categories = await categoryRepository.find(); + return res + .status(200) + .json({ message: 'Data retrieved successfully', data: categories }); + } +); + +export const getCategory = errorHandler(async (req: Request, res: Response) => { + const categoryId: number = parseInt(req.params.categoryId); + + const category = await categoryRepository.findOne({ + where: { id: categoryId }, + }); + + if (!category) { + return res.status(404).json({ message: 'Category Not Found' }); + } + + res + .status(200) + .json({ message: 'Data retrieved successfully', data: category }); +}); + +export const updateCategory = [ + ...createCategoryRules, + errorHandler(async (req: Request, res: Response) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + + const categoryId: number = parseInt(req.params.categoryId); + const { name, description } = req.body as categoryRequestBody; + + const category = await categoryRepository.findOne({ + where: { id: categoryId }, + }); + + if (!category) { + return res.status(404).json({ message: 'Category Not Found' }); + } + + const existingCategory = await categoryRepository.findOne({ + where: { name }, + }); + + if (existingCategory && existingCategory.id !== categoryId) { + return res.status(409).json({ message: 'Category name already exists' }); + } + + category.name = name; + category.description = description; + + const updatedCategory = await categoryRepository.save(category); + + return res.status(200).json({ + message: 'Category successfully updated', + data: updatedCategory, + }); + }), +]; + +export const deleteCategory = errorHandler( + async (req: Request, res: Response) => { + const categoryId: number = parseInt(req.params.categoryId); + + const category = await categoryRepository.findOne({ + where: { id: categoryId }, + }); + + if (!category) { + return res.status(404).json({ message: 'Category Not Found' }); + } + + await categoryRepository.delete(categoryId); + + res.status(200).json({ message: 'Category deleted successfully' }); + } +); diff --git a/src/controller/productController.ts b/src/controller/productController.ts new file mode 100644 index 00000000..3fe2f009 --- /dev/null +++ b/src/controller/productController.ts @@ -0,0 +1,290 @@ +import { Request, Response } from 'express'; +import Product from '../database/models/productEntity'; +import Category from '../database/models/categoryEntity'; +import UserModel from '../database/models/userModel'; +import dbConnection from '../database'; +import { check, validationResult } from 'express-validator'; +import errorHandler from '../middlewares/errorHandler'; + +const productRepository = dbConnection.getRepository(Product); +const categoryRepository = dbConnection.getRepository(Category); +const userRepository = dbConnection.getRepository(UserModel); + +interface ProductRequestBody { + name: string; + image: string; + gallery: string[]; + shortDesc: string; + longDesc: string; + categoryId: number; + quantity: number; + regularPrice: number; + salesPrice: number; + tags: string[]; + type: 'Simple' | 'Grouped' | 'Variable'; + isAvailable: boolean; +} + +const createProductRules = [ + check('name').isLength({ min: 1 }).withMessage('Product name is required'), + check('image').isLength({ min: 1 }).withMessage('Product image is required'), + check('gallery').isArray().withMessage('Gallery must be an array'), + check('shortDesc') + .isLength({ min: 1 }) + .withMessage('Short description is required'), + check('longDesc') + .isLength({ min: 1 }) + .withMessage('Long description is required'), + check('categoryId') + .isInt({ min: 1 }) + .withMessage('Valid category ID is required'), + check('quantity') + .isInt({ min: 0 }) + .withMessage('Quantity must be a non-negative integer'), + check('regularPrice') + .isFloat({ min: 0 }) + .withMessage('Regular price must be a non-negative number'), + check('salesPrice') + .isFloat({ min: 0 }) + .withMessage('Sales price must be a non-negative number'), + check('tags').isArray().withMessage('Tags must be an array'), + check('type') + .isIn(['Simple', 'Grouped', 'Variable']) + .withMessage('Invalid product type'), +]; + +export const createProduct = [ + ...createProductRules, + errorHandler(async (req: Request, res: Response) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + const vendorId = req.user!.id; + + const { + name, + image, + gallery, + shortDesc, + longDesc, + categoryId, + quantity, + regularPrice, + salesPrice, + tags, + type, + } = req.body as ProductRequestBody; + + const category = await categoryRepository.findOne({ + where: { id: categoryId }, + }); + if (!category) { + return res.status(404).json({ message: 'Category not found' }); + } + + const vendor = await userRepository.findOne({ + where: { id: vendorId }, + select: { + id: true, + firstName: true, + }, + }); + + if (!vendor) { + return res.status(404).json({ message: 'Vendor not found' }); + } + + const existingProduct = await productRepository.findOne({ + where: { name }, + }); + + if (existingProduct) { + return res.status(409).json({ message: 'Product name already exists' }); + } + + const newProduct = new Product({ + name, + image, + gallery, + shortDesc, + longDesc, + category, + vendor, + quantity, + regularPrice, + salesPrice, + tags, + type, + }); + const updatedProduct = await productRepository.save(newProduct); + return res.status(201).json({ + message: 'Product successfully created', + data: updatedProduct, + }); + }), +]; + +const updateProductRules = [ + check('name').isLength({ min: 1 }).withMessage('Product name is required'), + check('image').isLength({ min: 1 }).withMessage('Product image is required'), + check('gallery').isArray().withMessage('Gallery must be an array'), + check('shortDesc') + .isLength({ min: 1 }) + .withMessage('Short description is required'), + check('longDesc') + .isLength({ min: 1 }) + .withMessage('Long description is required'), + check('categoryId') + .isInt({ min: 1 }) + .withMessage('Valid category ID is required'), + check('quantity') + .isInt({ min: 0 }) + .withMessage('Quantity must be a non-negative integer'), + check('regularPrice') + .isFloat({ min: 0 }) + .withMessage('Regular price must be a non-negative number'), + check('salesPrice') + .isFloat({ min: 0 }) + .withMessage('Sales price must be a non-negative number'), + check('tags').isArray().withMessage('Tags must be an array'), + check('type') + .isIn(['Simple', 'Grouped', 'Variable']) + .withMessage('Invalid product type'), + check('isAvailable') + .isBoolean() + .withMessage('isAvailable must be a boolean value'), +]; +export const updateProduct = [ + ...updateProductRules, + errorHandler(async (req: Request, res: Response) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + + const productId: number = parseInt(req.params.productId); + const { + name, + image, + gallery, + shortDesc, + longDesc, + categoryId, + quantity, + regularPrice, + salesPrice, + tags, + type, + isAvailable, + } = req.body as ProductRequestBody; + + const product = await productRepository.findOne({ + where: { id: productId }, + }); + + if (!product) { + return res.status(404).json({ message: 'Product not found' }); + } + + const category = await categoryRepository.findOne({ + where: { id: categoryId }, + }); + + if (!category) { + return res.status(404).json({ message: 'Category not found' }); + } + + product.name = name; + product.image = image; + product.gallery = gallery; + product.shortDesc = shortDesc; + product.longDesc = longDesc; + product.category = category; + product.quantity = quantity; + product.regularPrice = regularPrice; + product.salesPrice = salesPrice; + product.tags = tags; + product.type = type; + product.isAvailable = isAvailable; + + const updatedProduct = await productRepository.save(product); + + return res.status(200).json({ + message: 'Product successfully updated', + data: updatedProduct, + }); + }), +]; + +export const getAllProducts = errorHandler( + async (req: Request, res: Response) => { + const products = await productRepository.find({ + select: { + category: { + name: true, + }, + vendor: { + firstName: true, + }, + }, + relations: ['category', 'vendor'], + }); + return res + .status(200) + .json({ message: 'Data retrieved successfully', data: products }); + } +); + +export const getProduct = errorHandler(async (req: Request, res: Response) => { + const productId: number = parseInt(req.params.productId); + + const product = await productRepository.findOne({ + where: { id: productId }, + select: { + category: { + name: true, + }, + vendor: { + firstName: true, + }, + }, + relations: ['category', 'vendor'], + }); + + if (!product) { + return res.status(404).json({ message: 'Product not found' }); + } + + return res + .status(200) + .json({ message: 'Data retrieved successfully', data: product }); +}); + +export const deleteProduct = errorHandler( + async (req: Request, res: Response) => { + const productId: number = parseInt(req.params.productId); + + const product = await productRepository.findOne({ + where: { id: productId }, + }); + + if (!product) { + return res.status(404).json({ message: 'Product Not Found' }); + } + + await productRepository.delete(productId); + + return res.status(200).json({ message: 'Product deleted successfully' }); + } +); + +export const deleteAllProduct = errorHandler( + async (req: Request, res: Response) => { + const deletedProducts = await productRepository.delete({}); + return res.status(200).json({ + message: 'All product deleted successfully', + count: deletedProducts.affected, + }); + } +); diff --git a/src/controller/userController.ts b/src/controller/userController.ts index 83e782c1..62597b3c 100644 --- a/src/controller/userController.ts +++ b/src/controller/userController.ts @@ -7,7 +7,7 @@ import UserModel from '../database/models/userModel'; import sendEmail from '../emails/index'; import { sendCode } from '../emails/mailer'; import jwt from 'jsonwebtoken'; -import errorHandler from '../middlewares/errorHandler' +import errorHandler from '../middlewares/errorHandler'; // Assuming dbConnection.getRepository(UserModel) returns a repository instance const userRepository = dbConnection.getRepository(UserModel); @@ -164,14 +164,13 @@ export const deleteUser = async (req: Request, res: Response) => { return res .status(500) .json({ error: 'An error occurred while deleting the record.' }); + } +}; - }} - - - export const Login = errorHandler(async (req: Request, res: Response) => { - const user = await userRepository.findOne({ - where: { email: req.body['email'] }, - relations: ['userType'] +export const Login = errorHandler(async (req: Request, res: Response) => { + const user = await userRepository.findOne({ + where: { email: req.body['email'] }, + relations: ['userType'], }); if (!user) { return res.status(404).send({ message: 'User Not Found' }); @@ -187,8 +186,13 @@ export const deleteUser = async (req: Request, res: Response) => { { expiresIn: '1d' } ); const confirmLink = `${process.env.APP_URL}/api/v1/confirm?token=${token}`; - await sendEmail('confirm', user.email, { name: user.firstName, link: confirmLink }); - return res.status(401).send({ message: 'Please verify your email. Confirmation link has been sent.' }); + await sendEmail('confirm', user.email, { + name: user.firstName, + link: confirmLink, + }); + return res.status(401).send({ + message: 'Please verify your email. Confirmation link has been sent.', + }); } if (user.userType.name === 'Vendor') { @@ -196,17 +200,19 @@ export const deleteUser = async (req: Request, res: Response) => { await userRepository.update(user.id, { twoFactorCode }); - await sendCode( - user.email, - 'Your 2FA Code', - './templates/2fa.html', - { name: user.firstName, twoFactorCode: twoFactorCode.toString() } - ); + await sendCode(user.email, 'Your 2FA Code', './templates/2fa.html', { + name: user.firstName, + twoFactorCode: twoFactorCode.toString(), + }); - res.status(200).json({ message: 'Please provide the 2FA code sent to your email.' }); + res + .status(200) + .json({ message: 'Please provide the 2FA code sent to your email.' }); } else { - const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET as jwt.Secret, { expiresIn: '1h' }); - res.status(200).json({ token, message: 'Buyer Logged in successfully'}); + const token = jwt.sign({ user }, process.env.JWT_SECRET as jwt.Secret, { + expiresIn: '1h', + }); + res.status(200).json({ token, message: 'Buyer Logged in successfully' }); } }); @@ -214,7 +220,10 @@ export const verify2FA = errorHandler(async (req: Request, res: Response) => { const { code } = req.body; const { userId } = req.params; - const user = await userRepository.findOne({ where: { id: Number(userId) } }); + const user = await userRepository.findOne({ + where: { id: Number(userId) }, + relations: ['userType'], + }); if (!user) { return res.status(401).json({ error: 'Invalid credentials' }); @@ -223,6 +232,8 @@ export const verify2FA = errorHandler(async (req: Request, res: Response) => { return res.status(401).json({ error: 'Invalid code' }); } - const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET as jwt.Secret, { expiresIn: '1h' }); + const token = jwt.sign({ user }, process.env.JWT_SECRET as jwt.Secret, { + expiresIn: '1h', + }); return res.status(200).json({ token }); }); diff --git a/src/database/models/categoryEntity.ts b/src/database/models/categoryEntity.ts new file mode 100644 index 00000000..31a5dbf8 --- /dev/null +++ b/src/database/models/categoryEntity.ts @@ -0,0 +1,36 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + OneToMany, +} from 'typeorm'; +import Product from './productEntity'; + +@Entity() +export default class Category { + @PrimaryGeneratedColumn() + id: number; + + @Column({ unique: true }) + name: string; + + @Column({ length: 250 }) + description: string; + + @OneToMany(() => Product, (product) => product.category, { + cascade: ['update'], + }) + products: Product[]; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; + + constructor(category: Partial) { + Object.assign(this, category); + } +} diff --git a/src/database/models/index.ts b/src/database/models/index.ts index 42df6b1e..000308b3 100644 --- a/src/database/models/index.ts +++ b/src/database/models/index.ts @@ -1,2 +1,3 @@ export * from './userModel'; export * from './roleEntity'; +export * from './productEntity'; diff --git a/src/database/models/productEntity.ts b/src/database/models/productEntity.ts new file mode 100644 index 00000000..4c2fa97c --- /dev/null +++ b/src/database/models/productEntity.ts @@ -0,0 +1,65 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, +} from 'typeorm'; +import Category from './categoryEntity'; +import UserModel from './userModel'; + +@Entity() +export default class Product { + @PrimaryGeneratedColumn() + id: number; + + @Column({ length: 200 }) + name: string; + + @Column() + image: string; + + @Column('simple-array') + gallery: string[]; + + @Column({ length: 250 }) + shortDesc: string; + + @Column() + longDesc: string; + + @ManyToOne(() => Category) + category: Category; + + @Column() + quantity: number; + + @Column() + regularPrice: number; + + @Column() + salesPrice: number; + + @Column('simple-array') + tags: string[]; + + @Column({ default: 'Simple' }) + type: 'Simple' | 'Grouped' | 'Variable'; + + @Column({ default: true }) + isAvailable: boolean; + + @ManyToOne(() => UserModel) + vendor: UserModel; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; + + constructor(product: Partial) { + Object.assign(this, product); + } +} diff --git a/src/database/models/roleEntity.ts b/src/database/models/roleEntity.ts index 8fcf0706..d7c76ad6 100644 --- a/src/database/models/roleEntity.ts +++ b/src/database/models/roleEntity.ts @@ -1,4 +1,5 @@ -import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; +import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'; +import UserModel from './userModel'; @Entity() export class Role { @@ -8,6 +9,9 @@ export class Role { @Column({ unique: true }) name: string; + @OneToMany(() => UserModel, (user) => user.userType, { cascade: ['update'] }) + users: UserModel[]; + @Column('simple-array') permissions: string[]; } diff --git a/src/database/models/userModel.ts b/src/database/models/userModel.ts index 38e64b63..f71170e2 100644 --- a/src/database/models/userModel.ts +++ b/src/database/models/userModel.ts @@ -1,12 +1,6 @@ -import { - Entity, - PrimaryGeneratedColumn, - Column, - ManyToOne, -} from 'typeorm'; +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'; import { Role } from './roleEntity'; - @Entity() export default class UserModel { @PrimaryGeneratedColumn() @@ -18,7 +12,7 @@ export default class UserModel { @Column() lastName: string; - @Column() + @Column({ unique: true }) email: string; @Column({ nullable: true }) @@ -42,8 +36,10 @@ export default class UserModel { @Column({ default: false }) isVerified: boolean; + @Column({ default: 'active' }) + status: 'active' | 'inactive'; - @Column({ nullable: true }) + @Column({ nullable: true }) twoFactorCode: number; constructor(user: Partial) { diff --git a/src/docs/2Fadocs.ts b/src/docs/2Fadocs.ts index 510122d5..e5ac8bff 100644 --- a/src/docs/2Fadocs.ts +++ b/src/docs/2Fadocs.ts @@ -1,6 +1,6 @@ /** * @swagger - * /api/v1/verify2FA/{userId}: + * /api/v1/user/verify2FA/{userId}: * post: * summary: Verify 2FA code * tags: [Login] @@ -52,4 +52,4 @@ * error: * type: string * description: An error message indicating internal server error - */ \ No newline at end of file + */ diff --git a/src/docs/categoryDocs.ts b/src/docs/categoryDocs.ts new file mode 100644 index 00000000..5891db19 --- /dev/null +++ b/src/docs/categoryDocs.ts @@ -0,0 +1,207 @@ +/** + * @swagger + * tags: + * name: Category + * description: Category management + */ +/** + * @swagger + * /api/v1/category/: + * post: + * summary: create a new category + * tags: [Category] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * description: + * type: string + * responses: + * '201': + * description: Category successfully created + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * description: A message indicating successful registration + * data: + * type: object + * properties: + * id: + * type: integer + * description: The unique identifier of the category + * name: + * type: string + * description: The name of the category + * description: + * type: string + * description: description of the category + * '400': + * description: Bad request + * content: + * application/json: + * schema: + * type: object + * properties: + * errors: + * type: array + * items: + * type: object + * properties: + * msg: + * type: string + * description: The error message + * '409': + * description: Conflict - Category name already exists + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * description: A message indicating the name of category already exists + */ +/** +/** + * @swagger + * /api/v1/category/{categoryId}: + * put: + * summary: update an existing category + * tags: [Category] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: categoryId + * type: string + * required: true + * description: ID of the category + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * description: + * type: string + * responses: + * '200': + * description: Category successfully updated + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * description: A message indicating successful updation + * data: + * type: object + * properties: + * id: + * type: integer + * description: The unique identifier of the category + * name: + * type: string + * description: The name of the category + * description: + * type: string + * description: description of the category + * '400': + * description: Bad request + * content: + * application/json: + * schema: + * type: object + * properties: + * errors: + * type: array + * items: + * type: object + * properties: + * msg: + * type: string + * description: The error message + * '409': + * description: Conflict - Category name already exists + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * description: A message indicating the name of category already exists + */ +/** +/** + * @swagger + * /api/v1/category/: + * get: + * summary: Get all categories + * tags: [Category] + * responses: + * '200': + * description: Successful + * '404': + * description: Categories not found + * '500': + * description: Internal Server Error + */ +/** + * @swagger + * /api/v1/category/{categoryId}: + * get: + * summary: Get an existing category + * tags: [Category] + * parameters: + * - in: path + * name: categoryId + * type: string + * required: true + * description: ID of the category + * responses: + * '200': + * description: Successful + * '404': + * description: category not found + * '500': + * description: Internal Server Error + */ + +/** + * @swagger + * /api/v1/category/{categoryId}: + * delete: + * summary: Deletes an existing category + * tags: [Category] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: categoryId + * type: string + * required: true + * description: ID of the category to delete + * responses: + * '200': + * description: Delete Successful + * '404': + * description: category not found + * '500': + * description: Internal Server Error + */ diff --git a/src/docs/productDoc.ts b/src/docs/productDoc.ts new file mode 100644 index 00000000..f2c0bd27 --- /dev/null +++ b/src/docs/productDoc.ts @@ -0,0 +1,186 @@ +/** + * @swagger + * tags: + * name: Product + * description: Operations related to products + */ + +/** + * @swagger + * /api/v1/product: + * get: + * summary: Get all products + * tags: [Product] + * responses: + * '200': + * description: Successful operation + * '500': + * description: Internal server error + * + * delete: + * summary: Delete all products + * tags: [Product] + * security: + * - bearerAuth: [] + * responses: + * '200': + * description: Products deleted successfully + * '500': + * description: Failed to delete products + */ + +/** + * @swagger + * /api/v1/product/{productId}: + * get: + * summary: Get a product by ID + * tags: [Product] + * parameters: + * - in: path + * name: productId + * type: integer + * required: true + * description: ID of the product to retrieve + * responses: + * '200': + * description: Successful operation + * '404': + * description: Product not found + * '500': + * description: Internal server error + * + * delete: + * summary: Delete a product by ID + * tags: [Product] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: productId + * type: integer + * required: true + * description: ID of the product to delete + * responses: + * '200': + * description: Product deleted successfully + * '404': + * description: Product not found + * '500': + * description: Internal server error + */ + +/** + * @swagger + * /api/v1/product: + * post: + * summary: Create a new product + * tags: [Product] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * image: + * type: string + * gallery: + * type: array + * items: + * type: string + * shortDesc: + * type: string + * longDesc: + * type: string + * categoryId: + * type: integer + * quantity: + * type: integer + * regularPrice: + * type: number + * salesPrice: + * type: number + * tags: + * type: array + * items: + * type: string + * type: + * type: string + * enum: ['Simple', 'Grouped', 'Variable'] + * isAvailable: + * type: boolean + * responses: + * '201': + * description: Product successfully created + * '400': + * description: Bad request + * '404': + * description: Category not found + * '409': + * description: Product name already exists + */ + +/** + * @swagger + * /api/v1/product/{productId}: + * put: + * summary: Update an existing product + * tags: [Product] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: productId + * type: integer + * required: true + * description: ID of the product to update + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * image: + * type: string + * gallery: + * type: array + * items: + * type: string + * shortDesc: + * type: string + * longDesc: + * type: string + * categoryId: + * type: integer + * quantity: + * type: integer + * regularPrice: + * type: number + * salesPrice: + * type: number + * tags: + * type: array + * items: + * type: string + * type: + * type: string + * enum: ['Simple', 'Grouped', 'Variable'] + * isAvailable: + * type: boolean + * responses: + * '200': + * description: Product successfully updated + * '400': + * description: Bad request + * '404': + * description: Product not found + * '500': + * description: Internal server error + */ diff --git a/src/docs/userAuth.ts b/src/docs/userAuth.ts index c9a3d2eb..4787521f 100644 --- a/src/docs/userAuth.ts +++ b/src/docs/userAuth.ts @@ -1,6 +1,6 @@ /** * @swagger - * /api/v1/login: + * /api/v1/user/login: * post: * summary: Login user * tags: [Login] @@ -90,4 +90,4 @@ * message: * type: string * description: An error message indicating internal server error - */ \ No newline at end of file + */ diff --git a/src/docs/userRegisterDocs.ts b/src/docs/userRegisterDocs.ts index de7bda60..893944d4 100644 --- a/src/docs/userRegisterDocs.ts +++ b/src/docs/userRegisterDocs.ts @@ -6,7 +6,7 @@ */ /** * @swagger - * /api/v1/register: + * /api/v1/user/register: * post: * summary: Register a new user * tags: [User] @@ -90,7 +90,7 @@ /** * @swagger - * /api/v1/confirm: + * /api/v1/user/confirm: * get: * summary: Confirm user email * tags: [User] @@ -133,3 +133,40 @@ * type: string * description: An error message indicating user not found */ + +/** + * @swagger + * /api/v1/user/getAllUsers: + * get: + * summary: Get all Users + * tags: [User] + * responses: + * '200': + * description: Successful + * '500': + * description: Internal Server Error + */ + +/** + * @swagger + * /api/v1/user/delete/{id}: + * delete: + * summary: Deletes an existing User + * tags: [User] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * type: string + * required: true + * description: ID of the user to delete + * responses: + * '200': + * description: Record deleted successfully. + * '404': + * description: Record not found. + * '500': + * description: An error occurred while deleting the record. + */ + diff --git a/src/middlewares/authorize.ts b/src/middlewares/authorize.ts index 4f368c77..c5c9984c 100644 --- a/src/middlewares/authorize.ts +++ b/src/middlewares/authorize.ts @@ -6,24 +6,37 @@ const roleRepository = dbConnection.getRepository(Role); export const checkRole = (roles: string[]) => { return async (req: Request, res: Response, next: NextFunction) => { - if (req.user && roles.includes(req.user.userType.name)) { - next(); - } else { - res.status(403).json({msg:'Forbidden'}); + try { + // Assuming req.user contains the user information after authentication + if ( + req.user && + req.user.userType && + roles.includes(req.user.userType.name) + ) { + return next(); + } else { + return res.status(403).json({ msg: 'Forbidden' }); + } + } catch (error) { + return res.status(500).json({ message: 'Internal server error' }); } }; }; export const checkPermissions = async (permission: string) => { return async (req: Request, res: Response, next: NextFunction) => { - const userRole = await roleRepository.findOneBy({ - name: req.user!.userType.name, - }); + try { + const userRole = await roleRepository.findOne({ + where: { name: req.user!.userType.name }, + }); - if (userRole && userRole.permissions.includes(permission)) { - next(); - } else { - res.status(403).json({msg:'Forbidden'}); + if (userRole && userRole.permissions.includes(permission)) { + return next(); + } else { + return res.status(403).json({ msg: 'Forbidden' }); + } + } catch (error) { + return res.status(500).json({ message: 'Internal server error' }); } }; }; diff --git a/src/middlewares/errorHandler.ts b/src/middlewares/errorHandler.ts index bed0a0a7..86b8a0c6 100644 --- a/src/middlewares/errorHandler.ts +++ b/src/middlewares/errorHandler.ts @@ -1,16 +1,20 @@ import { Request, Response } from 'express'; -type MiddlewareFunction = (req: Request, res: Response) => Promise> | undefined>; +type MiddlewareFunction = ( + req: Request, + res: Response +) => Promise> | undefined>; function errorHandler(func: MiddlewareFunction): MiddlewareFunction { - return async (req: Request, res: Response) => { - try { - return await func(req, res); - } catch (error) { - const message = (error as { detail?: string }).detail || 'Internal Server Error'; - return res.status(500).send(message); - } - }; - } + return async (req: Request, res: Response) => { + try { + return await func(req, res); + } catch (error) { + const message = + (error as { detail?: string }).detail || 'Internal Server Error'; + return res.status(500).send(message); + } + }; +} export default errorHandler; \ No newline at end of file diff --git a/src/middlewares/isLoggedIn.ts b/src/middlewares/isLoggedIn.ts new file mode 100644 index 00000000..17fea984 --- /dev/null +++ b/src/middlewares/isLoggedIn.ts @@ -0,0 +1,33 @@ +import { Request, Response, NextFunction } from 'express'; +import jwt from 'jsonwebtoken'; + +declare module 'express-serve-static-core' { + interface Request { + userId?: number; + userEmail?: string; + userType: { + id: number; + name: string; + permissions: []; + }; + } +} + +export const IsLoggedIn = (req: Request, res: Response, next: NextFunction) => { + const token = req.headers.authorization?.split(' ')[1]; + + if (!token) { + return res.status(401).json({ message: 'Unauthorized: No token provided' }); + } + + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET as jwt.Secret); + + // @ts-expect-error this is because ts + req.user = decoded.user; + + next(); + } catch (error) { + return res.status(401).json({ message: 'Unauthorized: Invalid token' }); + } +}; diff --git a/src/middlewares/passport-setup.ts b/src/middlewares/passport-setup.ts index 5e95b267..23e86496 100644 --- a/src/middlewares/passport-setup.ts +++ b/src/middlewares/passport-setup.ts @@ -2,7 +2,7 @@ import passport from 'passport'; import GooglePassport from 'passport-google-oauth20'; import FacebookPassport from 'passport-facebook'; import dbConnection from '../database'; -import UserModel from '../database/models/userModel'; +import UserModel from '../database/models/userModel'; import dotenv from 'dotenv'; dotenv.config(); diff --git a/src/routes/categoryRoutes.ts b/src/routes/categoryRoutes.ts new file mode 100644 index 00000000..f28acf61 --- /dev/null +++ b/src/routes/categoryRoutes.ts @@ -0,0 +1,23 @@ +import { Router } from 'express'; +import { + createCategory, + deleteCategory, + getAllCategories, + getCategory, + updateCategory, +} from '../controller/categoryController'; +import { IsLoggedIn } from '../middlewares/isLoggedIn'; + +const categoryRouter = Router(); + +categoryRouter + .route('/') + .post(IsLoggedIn, createCategory) + .get(getAllCategories); +categoryRouter + .route('/:categoryId') + .get(getCategory) + .put(IsLoggedIn, updateCategory) + .delete(IsLoggedIn, deleteCategory); + +export default categoryRouter; diff --git a/src/routes/index.ts b/src/routes/index.ts index f6117b86..da75aa8f 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,8 +1,14 @@ -// import roleRoutes from './roleRoutes' -// import { Router } from 'express' +import { Router } from 'express'; +import userRouter from './userRoutes'; +import roleRoutes from './roleRoutes'; +import productRoutes from './productRoutes'; +import categoryRoutes from './categoryRoutes'; -// const router = Router() +const router = Router(); -// router.use('/roles', roleRoutes) +router.use('/user', userRouter); +router.use('/roles', roleRoutes); +router.use('/product', productRoutes); +router.use('/category', categoryRoutes); -// export default router +export default router; diff --git a/src/routes/productRoutes.ts b/src/routes/productRoutes.ts new file mode 100644 index 00000000..d1f51f25 --- /dev/null +++ b/src/routes/productRoutes.ts @@ -0,0 +1,26 @@ +import { Router } from 'express'; +import { + createProduct, + deleteAllProduct, + deleteProduct, + getAllProducts, + getProduct, + updateProduct, +} from '../controller/productController'; +import { IsLoggedIn } from '../middlewares/isLoggedIn'; +import { checkRole } from '../middlewares/authorize'; + +const productRouter = Router(); + +productRouter + .route('/') + .post(IsLoggedIn, checkRole(['Vendor']), createProduct) + .get(getAllProducts) + .delete(IsLoggedIn, deleteAllProduct); +productRouter + .route('/:productId') + .get(getProduct) + .put(IsLoggedIn, checkRole(['Vendor']), updateProduct) + .delete(IsLoggedIn, deleteProduct); + +export default productRouter; diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts index f1b1ac6a..5e89371a 100644 --- a/src/routes/userRoutes.ts +++ b/src/routes/userRoutes.ts @@ -6,18 +6,16 @@ import { getAllUsers, deleteUser, Login, - verify2FA + verify2FA, } from '../controller/userController'; -const route = Router(); -route.post('/register', registerUser); -route.get('/getAllUsers', getAllUsers); -route.get('/confirm', confirmEmail); -route.delete('/delete/:id', deleteUser); -route.delete('/deleteAllUsers', deleteAllUsers); -route.post('/login',Login) -route.get('/all-users', getAllUsers); -route.post('/verify2FA/:userId', verify2FA); - -export default route; +const userRouter = Router(); +userRouter.post('/register', registerUser); +userRouter.get('/getAllUsers', getAllUsers); +userRouter.get('/confirm', confirmEmail); +userRouter.delete('/delete/:id', deleteUser); +userRouter.delete('/deleteAllUsers', deleteAllUsers); +userRouter.post('/login', Login); +userRouter.post('/verify2FA/:userId', verify2FA); +export default userRouter;