From c39404b2512259931166e24ba0d91d976da90263 Mon Sep 17 00:00:00 2001 From: Joslyn Manzi Karenzi Date: Fri, 17 May 2024 14:47:51 +0200 Subject: [PATCH] feat(view-product): retrieve a specific product -buyer should be able to retrieve a single product by id [Delivers #54] --- .env.example | 15 -- .github/workflows/workflow_for_ecomm.yml | 2 + package-lock.json | 170 +++++++++++---------- package.json | 1 + src/__test__/buyerController.test.ts | 75 +++++++++ src/__test__/testSetup.ts | 37 +++++ src/__test__/userController.test.ts | 98 ++++++++++++ src/controller/buyerController.ts | 25 +++ src/controller/roleController.ts | 2 +- src/controller/userController.ts | 46 +++++- src/database/models/productEntity.ts | 2 +- src/database/models/roleEntity.ts | 4 +- src/database/models/userModel.ts | 2 +- src/docs/buyerDocs.ts | 22 +++ src/docs/userRegisterDocs.ts | 84 ++++++++++- src/emails/index.ts | 48 +++--- src/emails/mailer.ts | 25 ++- src/emails/templates/2fa.html | 184 +++++++++++------------ src/emails/templates/confirm.html | 172 +++++++++++---------- src/emails/templates/reset.html | 172 +++++++++++---------- src/middlewares/errorHandler.ts | 2 +- src/routes/buyerRoutes.ts | 15 ++ src/routes/index.ts | 2 + src/routes/roleRoutes.ts | 2 +- src/routes/userRoutes.ts | 22 +-- 25 files changed, 819 insertions(+), 410 deletions(-) delete mode 100644 .env.example create mode 100644 src/__test__/buyerController.test.ts create mode 100644 src/controller/buyerController.ts create mode 100644 src/docs/buyerDocs.ts create mode 100644 src/routes/buyerRoutes.ts diff --git a/.env.example b/.env.example deleted file mode 100644 index f8194ea1..00000000 --- a/.env.example +++ /dev/null @@ -1,15 +0,0 @@ -DB_PORT_DEV=*** -DB_USER_DEV = *** -DB_PASSWORD_DEV = *** -DB_NAME_DEV = *** -DB_HOST_DEV = *** -APP_URL=*** -ALL=/api/v1 -DOCS=/api-docs -APP_PORT=3000 -# Testing DB Keys -DB_PORT_TEST= *** -DB_USER_TEST = *** -DB_PASSWORD_TEST = *** -DB_NAME_TEST = *** -DB_HOST_TEST= *** \ No newline at end of file diff --git a/.github/workflows/workflow_for_ecomm.yml b/.github/workflows/workflow_for_ecomm.yml index 298cd09e..6ed0974f 100644 --- a/.github/workflows/workflow_for_ecomm.yml +++ b/.github/workflows/workflow_for_ecomm.yml @@ -55,3 +55,5 @@ jobs: FACEBOOK_APP_SECRET: ${{ secrets.FACEBOOK_APP_SECRET }} FACEBOOK_CALLBACK_URL: ${{ secrets.FACEBOOK_CALLBACK_URL }} COOKIES_KEY: ${{ secrets.COOKIES_KEY }} + EMAIL_USER: ${{ secrets.EMAIL_USER }} + EMAIL_PASS: ${{ secrets.EMAIL_PASS }} diff --git a/package-lock.json b/package-lock.json index ff66d0d4..b5ed54c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@types/express-validator": "^3.0.0", "@types/passport-google-oauth20": "^2.0.14", "axios": "^1.6.8", "bcrypt": "^5.1.1", @@ -1416,6 +1417,42 @@ "node-pre-gyp": "bin/node-pre-gyp" } }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1738,6 +1775,15 @@ "@types/send": "*" } }, + "node_modules/@types/express-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/express-validator/-/express-validator-3.0.0.tgz", + "integrity": "sha512-LusnB0YhTXpBT25PXyGPQlK7leE1e41Vezq1hHEUwjfkopM1Pkv2X2Ppxqh9c+w/HZ6Udzki8AJotKNjDTGdkQ==", + "deprecated": "This is a stub types definition for express-validator (https://github.com/ctavan/express-validator). express-validator provides its own type definitions, so you don't need @types/express-validator installed!", + "dependencies": { + "express-validator": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -1870,9 +1916,9 @@ } }, "node_modules/@types/node": { - "version": "20.12.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.8.tgz", - "integrity": "sha512-NU0rJLJnshZWdE/097cdCBbyW1h4hEg0xpovcoAQYHl8dnEyp/NAOiE45pvc+Bd1Dt+2r94v2eGFpQJ4R7g+2w==", + "version": "20.12.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", + "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", "dependencies": { "undici-types": "~5.26.4" } @@ -1995,9 +2041,9 @@ "dev": true }, "node_modules/@types/superagent": { - "version": "8.1.7", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.7.tgz", - "integrity": "sha512-NmIsd0Yj4DDhftfWvvAku482PZum4DBW7U51OvS8gvOkDDY0WT1jsVyDV3hK+vplrsYw8oDwi9QxOM7U68iwww==", + "version": "8.1.6", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.6.tgz", + "integrity": "sha512-yzBOv+6meEHSzV2NThYYOA6RtqvPr3Hbob9ZLp3i07SH27CrYVfm8CrF7ydTmidtelsFiKx2I4gZAiAOamGgvQ==", "dev": true, "dependencies": { "@types/cookiejar": "^2.1.5", @@ -3011,9 +3057,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001616", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001616.tgz", - "integrity": "sha512-RHVYKov7IcdNjVHJFNY/78RdG4oGVjbayxv8u5IO74Wv7Hlq4PnJE6mo/OjFijjVFNy5ijnCt6H3IIo4t+wfEw==", + "version": "1.0.30001614", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001614.tgz", + "integrity": "sha512-jmZQ1VpmlRwHgdP1/uiKzgiAuGOfLEJsYFP4+GBou/QQ4U6IOJCB4NP1c+1p9RGLpwObcT94jA5/uO+F1vBbog==", "dev": true, "funding": [ { @@ -3777,9 +3823,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.756", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.756.tgz", - "integrity": "sha512-RJKZ9+vEBMeiPAvKNWyZjuYyUqMndcP1f335oHqn3BEQbs2NFtVrnK5+6Xg5wSM9TknNNpWghGDUCKGYF+xWXw==", + "version": "1.4.752", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.752.tgz", + "integrity": "sha512-P3QJreYI/AUTcfBVrC4zy9KvnZWekViThgQMX/VpJ+IsOBbcX5JFpORM4qWapwWQ+agb2nYAOyn/4PMXOk0m2Q==", "dev": true }, "node_modules/emittery": { @@ -5932,21 +5978,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-report/node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/istanbul-lib-source-maps": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", @@ -6750,9 +6781,9 @@ } }, "node_modules/libphonenumber-js": { - "version": "1.10.62", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.62.tgz", - "integrity": "sha512-zbLf2yhgrs+TN4rHT7ral38WQEXjS4TWKp8QD3P5fJmHh3lCtTiPyr8XDPGaA7T41HDz2qxR7x3uwr+aNbShJQ==" + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.1.tgz", + "integrity": "sha512-Wze1LPwcnzvcKGcRHFGFECTaLzxOtujwpf924difr5zniyYv1C2PiW0419qDR7m8lKDxsImu5mwxFuXhXpjmvw==" }, "node_modules/lines-and-columns": { "version": "1.2.4", @@ -6908,27 +6939,20 @@ } }, "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, "dependencies": { - "semver": "^6.0.0" + "semver": "^7.5.3" }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -7063,11 +7087,11 @@ } }, "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, "node_modules/minizlib": { @@ -7301,9 +7325,9 @@ } }, "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", "dependencies": { "abbrev": "1" }, @@ -7311,7 +7335,7 @@ "nopt": "bin/nopt.js" }, "engines": { - "node": ">=6" + "node": "*" } }, "node_modules/normalize-path": { @@ -9204,9 +9228,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.17.3", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.3.tgz", - "integrity": "sha512-+Y5WS+VzjxcjtdtzyrwnwBmx0eLqHgJt+9i7odilXoEpYymKQM2OI1teORwUtqqRNBdH+KRusBr47wBdx3RoRA==" + "version": "5.17.2", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.2.tgz", + "integrity": "sha512-V/NqUw6QoTrjSpctp2oLQvxrl3vW29UsUtZyq7B1CF0v870KOFbYGDQw8rpKaKm0JxTwHpWnW1SN9YuKZdiCyw==" }, "node_modules/swagger-ui-express": { "version": "5.0.0", @@ -9254,6 +9278,14 @@ "node": ">=10" } }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/tar/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -9378,20 +9410,6 @@ "nodetouch": "bin/nodetouch.js" } }, - "node_modules/touch/node_modules/nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "*" - } - }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -9878,14 +9896,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/typeorm/node_modules/minipass": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.0.tgz", - "integrity": "sha512-oGZRv2OT1lO2UF1zUcwdTb3wqUwI0kBGTgt/T7OdSj6M6N5m3o5uPf0AIW6lVxGGoiWUR7e2AwTE+xiwK8WQig==", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/typeorm/node_modules/mkdirp": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", @@ -9964,9 +9974,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.15.tgz", - "integrity": "sha512-K9HWH62x3/EalU1U6sjSZiylm9C8tgq2mSvshZpqc7QE69RaA2qjhkW2HlNA0tFpEbtyFz7HTqbSdN4MSwUodA==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", "dev": true, "funding": [ { @@ -9983,7 +9993,7 @@ } ], "dependencies": { - "escalade": "^3.1.2", + "escalade": "^3.1.1", "picocolors": "^1.0.0" }, "bin": { diff --git a/package.json b/package.json index c3b9ae67..f4af3e26 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "homepage": "https://github.com/atlp-rwanda/dynamites-ecomm-be#readme", "dependencies": { "@types/passport-google-oauth20": "^2.0.14", + "@types/express-validator": "^3.0.0", "axios": "^1.6.8", "bcrypt": "^5.1.1", "bcryptjs": "^2.4.3", diff --git a/src/__test__/buyerController.test.ts b/src/__test__/buyerController.test.ts new file mode 100644 index 00000000..4c3932fc --- /dev/null +++ b/src/__test__/buyerController.test.ts @@ -0,0 +1,75 @@ +import request from 'supertest'; +import app from '../app'; +import { + afterAllHook, + beforeAllHook, + getBuyerToken, + getVendorToken, +} from './testSetup'; +beforeAll(beforeAllHook); +afterAll(afterAllHook); + +describe('Buyer Controller test', () => { + let buyerToken: string; + let vendorToken: string; + let productId: number; + let categoryId: number; + + beforeAll(async () => { + buyerToken = await getBuyerToken(); + vendorToken = await getVendorToken(); + }); + + it('should get a product by id', async () => { + // create a category + const categoryData = { + name: 'Category4', + description: 'category description', + }; + + const categoryResponse = await request(app) + .post('/api/v1/category') + .set('Authorization', `Bearer ${vendorToken}`) + .send(categoryData); + + categoryId = categoryResponse.body.data.id; + + const productData = { + name: 'New Product Two', + 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 ${vendorToken}`) + .send(productData); + + productId = response.body.data.id; + + const getResponse = await request(app) + .get(`/api/v1/buyer/get_product/${productId}`) + .set('Authorization', `Bearer ${buyerToken}`); + + + expect(getResponse.statusCode).toEqual(200); + expect(getResponse.body.msg).toEqual('Product retrieved successfully'); + }); + + it('should return a 404 if product is not found', async () => { + const response = await request(app) + .get('/api/v1/buyer/get_product/5') + .set('Authorization', `Bearer ${buyerToken}`); + expect(response.status).toBe(404); + expect(response.body.msg).toBe('Product not found'); + }); +}); diff --git a/src/__test__/testSetup.ts b/src/__test__/testSetup.ts index 969af324..b6737ac0 100644 --- a/src/__test__/testSetup.ts +++ b/src/__test__/testSetup.ts @@ -77,15 +77,52 @@ export async function getVendorToken() { return verifyResponse.body.token; } +export const getBuyerToken = async () => { + const userRepository = await DbConnection.connection.getRepository(UserModel); + const formData = { + name: 'Buyer', + permissions: ['test-permission1', 'test-permission2'], + }; + const roleResponse = await request(app) + .post('/api/v1/roles/create_role') + .send(formData); + + const userData = { + firstName: 'Tester', + lastName: 'Test', + email: 'test4@gmail.com', + password: 'TestPassword123', + userType: 'buyer', + }; + const res = 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, + }); + + return loginResponse.body.token; +}; + export async function afterAllHook() { await DbConnection.connection.transaction(async (transactionManager) => { const userRepository = transactionManager.getRepository(UserModel); const categoryRepository = transactionManager.getRepository(Category); const productRepository = transactionManager.getRepository(Product); + const roleRepository = transactionManager.getRepository(Role); await userRepository.createQueryBuilder().delete().execute(); await categoryRepository.createQueryBuilder().delete().execute(); await productRepository.createQueryBuilder().delete().execute(); + await roleRepository.createQueryBuilder().delete().execute(); }); await DbConnection.instance.disconnectDb(); } diff --git a/src/__test__/userController.test.ts b/src/__test__/userController.test.ts index 1a48549d..332a5ab0 100644 --- a/src/__test__/userController.test.ts +++ b/src/__test__/userController.test.ts @@ -1,5 +1,6 @@ import request from 'supertest'; import app from '../app'; +// import { Role } from '../database/models'; import { afterAllHook, beforeAllHook } from './testSetup'; import jwt from 'jsonwebtoken'; import dbConnection from '../database'; @@ -9,7 +10,9 @@ const userRepository = dbConnection.getRepository(UserModel); beforeAll(beforeAllHook); afterAll(afterAllHook); + describe('User Registration Tests', () => { + it('should register a new user with valid data', async () => { const userData = { firstName: 'Test', @@ -331,3 +334,98 @@ describe('User Login Tests', () => { expect(loginResponse.body.message).toBe('User Not Found'); }); }); + + describe('update user Profile', () => { + interface IUser { + id: number; + firstName: string; + lastName: string; + email: string; + password?: string; + userType?: Role; + googleId?: string; + facebookId?: string; + picture?: string; + provider?: string; + isVerified: boolean; + twoFactorCode?: number; + } + + interface Role { + id: number; + name: string; + permissions: string[]; + } + + + let user: IUser | undefined | null; + const userData = { + firstName: 'jan', + lastName: 'bosco', + email: 'bosco@gmail.com', + password: 'boscoPassword123', + }; + + beforeEach(async () => { + + await request(app).post('/api/v1/register').send(userData); + user = await userRepository.findOne({ where: { email: userData.email } }); + }); + + it('should update the user profile successfully', async () => { + if (user) { + const newUserData = { + firstName: 'NewFirstName', + lastName: 'NewLastName', + email: 'newemail@example.com', + password: 'bosco@gmail.com', + }; + + const response = await request(app) + .put(`/api/v1/updateProfile/${user?.id}`) + .send(newUserData); + expect(response.statusCode).toBe(201); + expect(response.body.message).toBe('User updated successfully'); + } + }); + + it('should return 404 when user not found', async () => { + const Id = 999; + const response = await request(app) + .put(`/api/v1/updateProfile/${Id}`) + .send(userData); + expect(response.statusCode).toBe(404); + expect(response.body.error).toBe('User not found'); + }); + + it('should return 400 when email already exists', async () => { + if (user) { + const newUserData = { + firstName: 'NewFirstName', + lastName: 'NewLastName', + email: 'newemail@example.com', + password: 'bosco@gmail.com', + }; + + const response = await request(app) + .put(`/api/v1/updateProfile/${user.id}`) + .send(newUserData); + expect(response.statusCode).toBe(400); + expect(response.body.error).toBe('Email is already taken'); + } + }); + + it('should return 400 when validation fails for user data', async () => { + if (user) { + const emptyData = {}; + + const response = await request(app) + .put(`/api/v1/updateProfile/${user.id}`) + .send(emptyData); + + expect(response.statusCode).toBe(400); + expect(response.body).toBeDefined(); + } + }); +}); + diff --git a/src/controller/buyerController.ts b/src/controller/buyerController.ts new file mode 100644 index 00000000..839970a5 --- /dev/null +++ b/src/controller/buyerController.ts @@ -0,0 +1,25 @@ +import { Request, Response } from 'express'; +import dbConnection from '../database'; +import Product from '../database/models/productEntity'; +import errorHandler from '../middlewares/errorHandler'; + +const productRepository = dbConnection.getRepository(Product); + +export const getOneProduct = errorHandler( + async (req: Request, res: Response) => { + const productId = parseInt(req.params.id); + + const product = await productRepository.findOne({ + where: { id: productId }, + relations: ['category'], + }); + + if (!product) { + return res.status(404).json({ msg: 'Product not found' }); + } + + return res + .status(200) + .json({ msg: 'Product retrieved successfully', product }); + } +); diff --git a/src/controller/roleController.ts b/src/controller/roleController.ts index 39a2e834..9648af64 100644 --- a/src/controller/roleController.ts +++ b/src/controller/roleController.ts @@ -6,7 +6,7 @@ import { updateRoleSchema, changeRoleSchema, } from '../middlewares/roleSchema'; -import UserModel from '../database/models/userModel'; +import UserModel from '../database/models/userModel'; const roleRepository = dbConnection.getRepository(Role); const userRepository = dbConnection.getRepository(UserModel); diff --git a/src/controller/userController.ts b/src/controller/userController.ts index 62597b3c..47fa99f6 100644 --- a/src/controller/userController.ts +++ b/src/controller/userController.ts @@ -7,7 +7,9 @@ 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' +import { validate } from 'class-validator'; // For input validation + // Assuming dbConnection.getRepository(UserModel) returns a repository instance const userRepository = dbConnection.getRepository(UserModel); @@ -20,6 +22,13 @@ interface CreateUserRequestBody { password: string; userType: 'vendor' | 'buyer'; } +interface UpdateRrofileRequestBody { + firstName: string; + lastName: string; + email: string; + password:string; +} + // Define validation and sanitization rules const registerUserRules = [ @@ -237,3 +246,38 @@ export const verify2FA = errorHandler(async (req: Request, res: Response) => { }); return res.status(200).json({ token }); }); + +export const updateProfile = errorHandler(async (req: Request, res: Response) => { + const userId: number = parseInt(req.params.id); + const { firstName, lastName, email } = req.body as UpdateRrofileRequestBody; + + const user = await userRepository.findOne({ where: { id: userId } }); + + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + + user.firstName = firstName || user.firstName; + user.lastName = lastName || user.lastName; + + + const emailExists = await userRepository.findOne({ where: { email } }); + + if (emailExists) { + return res.status(400).json({ error: 'Email is already taken' }); + } + + user.email = email; + + + const errors = await validate(user); + + if (errors.length > 0) { + return res.status(400).json({ errors }); + } + + await userRepository.save(user); + + return res.status(201).json({ message: 'User updated successfully' }); +}); + diff --git a/src/database/models/productEntity.ts b/src/database/models/productEntity.ts index 4c2fa97c..e036eea8 100644 --- a/src/database/models/productEntity.ts +++ b/src/database/models/productEntity.ts @@ -50,7 +50,7 @@ export default class Product { @Column({ default: true }) isAvailable: boolean; - @ManyToOne(() => UserModel) + @ManyToOne(() => UserModel, { onDelete: 'CASCADE' }) vendor: UserModel; @CreateDateColumn() diff --git a/src/database/models/roleEntity.ts b/src/database/models/roleEntity.ts index d7c76ad6..41fdf575 100644 --- a/src/database/models/roleEntity.ts +++ b/src/database/models/roleEntity.ts @@ -13,5 +13,5 @@ export class Role { users: UserModel[]; @Column('simple-array') - permissions: string[]; -} + permissions: string[]; +} \ No newline at end of file diff --git a/src/database/models/userModel.ts b/src/database/models/userModel.ts index f71170e2..158bb4b8 100644 --- a/src/database/models/userModel.ts +++ b/src/database/models/userModel.ts @@ -45,4 +45,4 @@ export default class UserModel { constructor(user: Partial) { Object.assign(this, user); } -} +} \ No newline at end of file diff --git a/src/docs/buyerDocs.ts b/src/docs/buyerDocs.ts new file mode 100644 index 00000000..649d4f75 --- /dev/null +++ b/src/docs/buyerDocs.ts @@ -0,0 +1,22 @@ +/** + * @swagger + * /api/v1/buyer/get_product/{id}: + * get: + * summary: Get a specific product + * tags: [Buyer] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * type: string + * required: true + * description: ID of the product to get + * responses: + * '200': + * description: Successful + * '404': + * description: Product not found + * '500': + * description: Internal Server Error + */ diff --git a/src/docs/userRegisterDocs.ts b/src/docs/userRegisterDocs.ts index 893944d4..d2b961c6 100644 --- a/src/docs/userRegisterDocs.ts +++ b/src/docs/userRegisterDocs.ts @@ -4,6 +4,7 @@ * name: User * description: User management */ + /** * @swagger * /api/v1/user/register: @@ -88,6 +89,7 @@ * description: A message indicating the email already exists */ + /** * @swagger * /api/v1/user/confirm: @@ -169,4 +171,84 @@ * '500': * description: An error occurred while deleting the record. */ - +/** + * @swagger + * /api/v1/updateProfile/{id}: + * put: + * summary: Update user profile + * tags: + * - User + * parameters: + * - in: path + * name: id + * required: true + * description: ID of the user to update + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * firstName: + * type: string + * description: The updated first name of the user. + * lastName: + * type: string + * description: The updated last name of the user. + * email: + * type: string + * format: email + * description: The updated email address of the user. + * oldPassword: + * type: string + * format: password + * description: The old password of the user for verification. + * newPassword: + * type: string + * format: password + * description: The new password of the user (optional). + * responses: + * '200': + * description: User profile updated successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * description: A message indicating successful profile update. + * '400': + * description: Bad request or validation errors. + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * description: Error message indicating the reason for the bad request. + * '404': + * description: Not Found - User not found. + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * description: A message indicating the user was not found. + * '500': + * description: Internal server error. + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * description: A message indicating an internal server error occurred. + */ diff --git a/src/emails/index.ts b/src/emails/index.ts index acd03cd7..8f88ef6f 100644 --- a/src/emails/index.ts +++ b/src/emails/index.ts @@ -29,28 +29,32 @@ async function sendEmail(emailType: EmailType, recipient: string, data: Data) { // Send the Email - const domain = process.env.MAILGUN_DOMAIN - const key = process.env.MAILGUN_TOKEN as string - const body = { - from: `Dynamites Account Team `, - to: [recipient], - subject: 'Verification Email', - html: html - } - const mailgunResponse = await axios.post(`https://api.mailgun.net/v3/${domain}/messages`, body, { - auth: { - username: 'api', - password: key - }, - headers: { - 'Content-Type': 'multipart/form-data', - } - }) + const domain = process.env.MAILGUN_DOMAIN; + const key = process.env.MAILGUN_TOKEN as string; + const body = { + from: `Dynamites Account Team `, + to: [recipient], + subject: 'Verification Email', + html: html, + }; + const mailgunResponse = await axios.post( + `https://api.mailgun.net/v3/${domain}/messages`, + body, + { + auth: { + username: 'api', + password: key, + }, + headers: { + 'Content-Type': 'multipart/form-data', + }, + } + ); - return mailgunResponse - } catch (error) { - throw new Error(`Error sending email: ${error}`); - } + return mailgunResponse; + } catch (error) { + throw new Error(`Error sending email: ${error}`); + } } -export default sendEmail; \ No newline at end of file +export default sendEmail; diff --git a/src/emails/mailer.ts b/src/emails/mailer.ts index f6a1b712..88be4209 100644 --- a/src/emails/mailer.ts +++ b/src/emails/mailer.ts @@ -2,24 +2,35 @@ import mailgun from 'mailgun-js'; import fs from 'fs'; import path from 'path'; import dotenv from 'dotenv'; -dotenv.config() +dotenv.config(); const mg = mailgun({ - apiKey: process.env.MAILGUN_TOKEN || 'default_api_key', - domain: process.env.MAILGUN_DOMAIN || 'default_domain' + apiKey: process.env.MAILGUN_TOKEN || 'default_api_key', + domain: process.env.MAILGUN_DOMAIN || 'default_domain', }); -export async function sendCode(to: string, subject: string, htmlTemplatePath: string, replacements: Record) { - const template = await fs.promises.readFile(path.resolve(__dirname, htmlTemplatePath), 'utf8'); +export async function sendCode( + to: string, + subject: string, + htmlTemplatePath: string, + replacements: Record +) { + const template = await fs.promises.readFile( + path.resolve(__dirname, htmlTemplatePath), + 'utf8' + ); let html = template; for (const placeholder in replacements) { - html = html.replace(new RegExp(`{{${placeholder}}}`, 'g'), replacements[placeholder]); + html = html.replace( + new RegExp(`{{${placeholder}}}`, 'g'), + replacements[placeholder] + ); } const mailOptions = { from: 'dynamitesecommerce@gmail.com', to, subject, - html + html, }; // Send the email diff --git a/src/emails/templates/2fa.html b/src/emails/templates/2fa.html index 2c9718f3..1faa428e 100644 --- a/src/emails/templates/2fa.html +++ b/src/emails/templates/2fa.html @@ -1,109 +1,105 @@ - + - - - - + + + + - - - -
-
-
- Welcome to Dynamites Ecommerce,{{name}}! -
-

- We've generated a 2FA code for you. -

-
- {{twoFactorCode}} -
+ + +
+
+
Welcome to Dynamites Ecommerce,{{name}}!
+

We've generated a 2FA code for you.

+
{{twoFactorCode}}

- Please enter this code to complete your authentication process. Let's keep your account safe and secure! + Please enter this code to complete your authentication process. Let's + keep your account safe and secure!

+
-
- - - \ No newline at end of file + + diff --git a/src/emails/templates/confirm.html b/src/emails/templates/confirm.html index 5b7f9853..3affc2b3 100644 --- a/src/emails/templates/confirm.html +++ b/src/emails/templates/confirm.html @@ -1,105 +1,103 @@ - + - - - - + + + + - - - -
-
-
- Welcome to Dynamites Ecommerce -
+ + +
+
+
Welcome to Dynamites Ecommerce
+

Hello there {{name}},

- Hello there {{name}}, -

-

- Thank you for signing up. Please confirm your email address to activate your account. + Thank you for signing up. Please confirm your email address to + activate your account.

-

- If you are having trouble clicking the "Confirm Email Address" button, copy and paste the URL below into your web browser: {{link}} -

+

+ If you are having trouble clicking the "Confirm Email Address" + button, copy and paste the URL below into your web browser: + {{link}} +

+
-
- - - \ No newline at end of file + + diff --git a/src/emails/templates/reset.html b/src/emails/templates/reset.html index ed3548a5..20ca4778 100644 --- a/src/emails/templates/reset.html +++ b/src/emails/templates/reset.html @@ -1,105 +1,103 @@ - + - - - - + + + + - - - -
-
-
- Welcome back to Dynamites Ecommerce -
+ + +
+
+
Welcome back to Dynamites Ecommerce
+

Dear {{name}},

- Dear {{name}}, -

-

- We received your request to reset password. If this was you, click on the following link to reset your password. + We received your request to reset password. If this was you, click on + the following link to reset your password.

-

- If you are having trouble clicking the "Reset Password" button above, copy and paste the URL below into your web browser: {{link}} -

+

+ If you are having trouble clicking the "Reset Password" button + above, copy and paste the URL below into your web browser: + {{link}} +

+
-
- - - \ No newline at end of file + + diff --git a/src/middlewares/errorHandler.ts b/src/middlewares/errorHandler.ts index 86b8a0c6..3d741642 100644 --- a/src/middlewares/errorHandler.ts +++ b/src/middlewares/errorHandler.ts @@ -17,4 +17,4 @@ function errorHandler(func: MiddlewareFunction): MiddlewareFunction { }; } -export default errorHandler; \ No newline at end of file +export default errorHandler; diff --git a/src/routes/buyerRoutes.ts b/src/routes/buyerRoutes.ts new file mode 100644 index 00000000..a07bc67d --- /dev/null +++ b/src/routes/buyerRoutes.ts @@ -0,0 +1,15 @@ +import { Router } from 'express'; +import { checkRole } from '../middlewares/authorize'; +import { getOneProduct } from '../controller/buyerController'; +import { IsLoggedIn } from '../middlewares/isLoggedIn'; + +const buyerRouter = Router(); + +buyerRouter.get( + '/get_product/:id', + IsLoggedIn, + checkRole(['Buyer']), + getOneProduct +); + +export default buyerRouter; diff --git a/src/routes/index.ts b/src/routes/index.ts index da75aa8f..6887a8c7 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -3,6 +3,7 @@ import userRouter from './userRoutes'; import roleRoutes from './roleRoutes'; import productRoutes from './productRoutes'; import categoryRoutes from './categoryRoutes'; +import buyerRoutes from './buyerRoutes'; const router = Router(); @@ -10,5 +11,6 @@ router.use('/user', userRouter); router.use('/roles', roleRoutes); router.use('/product', productRoutes); router.use('/category', categoryRoutes); +router.use('/buyer', buyerRoutes); export default router; diff --git a/src/routes/roleRoutes.ts b/src/routes/roleRoutes.ts index 2c0242d9..5c1a4724 100644 --- a/src/routes/roleRoutes.ts +++ b/src/routes/roleRoutes.ts @@ -9,6 +9,6 @@ roleRouter.post('/create_role', roleController.createRole); roleRouter.put('/update_role', roleController.updateRole); roleRouter.delete('/delete_role/:id', roleController.deleteRole); roleRouter.patch('/change_user_role', roleController.changeRole); -roleRouter.get('/test', checkRole(['Admin']), roleController.getRoles) +roleRouter.get('/test', checkRole(['Admin']), roleController.getRoles); export default roleRouter; diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts index 5e89371a..a718f882 100644 --- a/src/routes/userRoutes.ts +++ b/src/routes/userRoutes.ts @@ -7,15 +7,19 @@ import { deleteUser, Login, verify2FA, + updateProfile } from '../controller/userController'; -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); +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); + +route.put('/updateProfile/:id',updateProfile); +export default route; -export default userRouter;