From fb6bc115ef7e28add2b25a4191b9bc4146f32360 Mon Sep 17 00:00:00 2001 From: oderahub Date: Tue, 23 Jul 2024 18:35:52 +0100 Subject: [PATCH 01/17] Your commit message --- src/controllers/updateorgController.ts | 34 ++++++++++++++++++++++++++ src/index.ts | 4 ++- src/routes/index.ts | 6 ++--- src/routes/updateOrg.ts | 16 ++++++++++++ src/services/updateorg.service.ts | 23 +++++++++++++++++ 5 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 src/controllers/updateorgController.ts create mode 100644 src/routes/updateOrg.ts create mode 100644 src/services/updateorg.service.ts diff --git a/src/controllers/updateorgController.ts b/src/controllers/updateorgController.ts new file mode 100644 index 00000000..3833e8c3 --- /dev/null +++ b/src/controllers/updateorgController.ts @@ -0,0 +1,34 @@ +import { Request, Response } from "express"; +import { UpdateOrganizationDetails } from "../services/updateorg.service"; +import AppDataSource from "../data-source"; + +export const updateOrganization = async (req: Request, res: Response) => { + const { organisation_id } = req.params; + const updateData = req.body; + + try { + const organisation = await UpdateOrganizationDetails( + AppDataSource, + organisation_id, + updateData + ); + res.status(200).json({ + message: "Organization details updated successfully", + status_code: 200, + data: organisation, + }); + } catch (error) { + if (error.message.includes("not found")) { + return res.status(404).json({ + status: "unsuccessful", + status_code: 404, + message: error.message, + }); + } + } + return res.status(500).json({ + status: "failed", + status_code: 500, + message: "Failed to update organization details. Please try again later.", + }); +}; diff --git a/src/index.ts b/src/index.ts index cd153fbb..b5dee42c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,13 +14,14 @@ import { notificationRouter, smsRouter, productRouter, - jobRouter + jobRouter, } from "./routes"; import { routeNotFound, errorHandler } from "./middleware"; import { orgRouter } from "./routes/organisation"; import swaggerUi from "swagger-ui-express"; import swaggerSpec from "./swaggerConfig"; import { organisationRoute } from "./routes/createOrg"; +import updateRouter from "./routes/updateOrg"; dotenv.config(); @@ -56,6 +57,7 @@ server.use(routeNotFound); server.use(errorHandler); server.use("/api/v1/settings", notificationRouter); server.use("/api/v1/jobs", jobRouter); +server.use("/api/v1", updateRouter); AppDataSource.initialize() .then(async () => { diff --git a/src/routes/index.ts b/src/routes/index.ts index 9370b286..762c8fce 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -4,8 +4,8 @@ export * from "./user"; export * from "./help-center"; export * from "./testimonial"; export * from "./product"; -export * from "./notificationsettings" -export * from "./sms"; -export * from "./sms"; +export * from "./notificationsettings"; +// export * from "./sms"; +// export * from "./sms"; export * from "./notificationsettings"; export * from "./job"; diff --git a/src/routes/updateOrg.ts b/src/routes/updateOrg.ts new file mode 100644 index 00000000..225a962b --- /dev/null +++ b/src/routes/updateOrg.ts @@ -0,0 +1,16 @@ +import express from "express"; +import { updateOrganization } from "../controllers/updateorgController"; +import { authMiddleware } from "../middleware/auth"; +import { checkPermissions } from "../middleware/checkUserRole"; +import { UserRole } from "../enums/userRoles"; + +const router = express.Router(); + +router.put( + "/organization/:organization_id", + authMiddleware, + checkPermissions([UserRole.SUPER_ADMIN, UserRole.USER]), + updateOrganization +); + +export default router; diff --git a/src/services/updateorg.service.ts b/src/services/updateorg.service.ts new file mode 100644 index 00000000..42278e98 --- /dev/null +++ b/src/services/updateorg.service.ts @@ -0,0 +1,23 @@ +import { DataSource } from "typeorm"; +import { Organization } from "../models/organization"; + +export const UpdateOrganizationDetails = async ( + dataSource: DataSource, + organization_Id: string, + updateData: Partial +) => { + const organizationRepository = dataSource.getRepository(Organization); + + const organization = await organizationRepository.findOne({ + where: { id: organization_Id }, + }); + + if (!organization) { + throw new Error(`Organization with ID ${organization_Id} not found`); + } + + organizationRepository.merge(organization, updateData); + await organizationRepository.save(organization); + + return organization; +}; From cfc8b305142c88d9870853df62c02cff1e8a6395 Mon Sep 17 00:00:00 2001 From: oderahub Date: Tue, 23 Jul 2024 19:33:59 +0100 Subject: [PATCH 02/17] resolveda --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index b5dee42c..f42ac8dc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,10 +12,10 @@ import { helpRouter, testimonialRoute, notificationRouter, - smsRouter, productRouter, jobRouter, } from "./routes"; +import { smsRouter } from "./routes/sms"; import { routeNotFound, errorHandler } from "./middleware"; import { orgRouter } from "./routes/organisation"; import swaggerUi from "swagger-ui-express"; From 3dacbc75e29eb7c013872932032e039da9933c7c Mon Sep 17 00:00:00 2001 From: Mong Israel Date: Tue, 23 Jul 2024 22:13:19 +0100 Subject: [PATCH 03/17] get all the organisations created by a user, protected --- .gitignore | 5 +- src/controllers/OrgController.ts | 53 +++++++-- src/index.ts | 7 +- src/routes/organisation.ts | 6 + src/services/OrgService.ts | 35 +++--- yarn.lock | 191 ++++++++++++++++++++----------- 6 files changed, 208 insertions(+), 89 deletions(-) diff --git a/.gitignore b/.gitignore index f3f35cac..8381a573 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,7 @@ temp/ .env env src/entity -src/ormconfig.ts \ No newline at end of file +src/ormconfig.ts +issueformat.md +requests.rest +package-lock.json \ No newline at end of file diff --git a/src/controllers/OrgController.ts b/src/controllers/OrgController.ts index b2396679..bb68ac47 100644 --- a/src/controllers/OrgController.ts +++ b/src/controllers/OrgController.ts @@ -1,5 +1,6 @@ - import { Request, Response } from "express"; +import { Request, Response } from "express"; import { OrgService } from "../services/OrgService"; +import log from "../utils/logger"; export class OrgController { private orgService: OrgService; @@ -9,10 +10,7 @@ export class OrgController { async removeUser(req: Request, res: Response) { try { - const user = await this.orgService.removeUser( - req.params.org_id, - req.params.user_id, - ); + const user = await this.orgService.removeUser(req.params.org_id, req.params.user_id); if (!user) { return res.status(404).json({ status: "forbidden", @@ -22,13 +20,50 @@ export class OrgController { } res.status(200).json({ status: "success", - message: "User deleted succesfully", + message: "User deleted successfully", status_code: 200, }); } catch (error) { - res - .status(400) - .json({ message: "Failed to remove user from organization" }); + log.error("Failed to remove user from organization:", error); + res.status(400).json({ message: "Failed to remove user from organization" }); + } + } + + async getOrganizations(req: Request, res: Response) { + try { + const userId = req.params.id; + log.info("req.user:", req.user); + if (!req.user || req.user.id !== userId) { + return res.status(400).json({ + status: "unsuccessful", + status_code: 400, + message: "Invalid user ID or authentication mismatch.", + }); + } + const organizations = await this.orgService.getOrganizationsByUserId(userId); + + if (organizations.length === 0) { + return res.status(200).json({ + status: "success", + status_code: 200, + message: "No organizations found for this user.", + data: [], + }); + } + + res.status(200).json({ + status: "success", + status_code: 200, + message: "Organizations retrieved successfully.", + data: organizations, + }); + } catch (error) { + log.error("Failed to retrieve organizations:", error); + res.status(500).json({ + status: "unsuccessful", + status_code: 500, + message: "Failed to retrieve organizations. Please try again later.", + }); } } } diff --git a/src/index.ts b/src/index.ts index c0331d6d..fe121a6d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,7 @@ import { orgRouter } from "./routes/organisation"; import swaggerUi from "swagger-ui-express"; import swaggerSpec from "./swaggerConfig"; import { organisationRoute } from "./routes/createOrg"; +import { authMiddleware } from "./middleware/auth"; dotenv.config(); @@ -55,10 +56,12 @@ server.use("/api/v1/sms", smsRouter); server.use("/api/v1", testimonialRoute); server.use("/api/v1/product", productRouter); server.use("/api/v1/docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec)); -server.use(routeNotFound); -server.use(errorHandler); server.use("/api/v1/settings", notificationRouter); server.use("/api/v1/jobs", jobRouter); +server.use("/api/v1", authMiddleware, orgRouter); + +server.use(routeNotFound); +server.use(errorHandler); AppDataSource.initialize() .then(async () => { diff --git a/src/routes/organisation.ts b/src/routes/organisation.ts index 1247766e..82853b26 100644 --- a/src/routes/organisation.ts +++ b/src/routes/organisation.ts @@ -8,4 +8,10 @@ orgRouter.delete( "/organizations/:org_id/users/:user_id", orgController.removeUser.bind(orgController), ); + +orgRouter.get( + "/users/:id/organizations", + orgController.getOrganizations.bind(orgController), +); + export { orgRouter }; diff --git a/src/services/OrgService.ts b/src/services/OrgService.ts index e460c33d..91f419fc 100644 --- a/src/services/OrgService.ts +++ b/src/services/OrgService.ts @@ -1,13 +1,11 @@ import { Organization } from "../models/organization"; import AppDataSource from "../data-source"; import { User } from "../models/user"; -import { IOrgService, IUserService } from "../types"; +import { IOrgService } from "../types"; +import log from "../utils/logger"; export class OrgService implements IOrgService { - public async removeUser( - org_id: string, - user_id: string - ): Promise { + public async removeUser(org_id: string, user_id: string): Promise { const userRepository = AppDataSource.getRepository(User); const organizationRepository = AppDataSource.getRepository(Organization); @@ -27,20 +25,31 @@ export class OrgService implements IOrgService { return null; } - // Check if the user is part of the organization - const userInOrganization = organization.users.some( - (user) => user.id === user_id - ); + const userInOrganization = organization.users.some((user) => user.id === user_id); if (!userInOrganization) { return null; } - // Remove the user from the organization - organization.users = organization.users.filter( - (user) => user.id !== user_id - ); + organization.users = organization.users.filter((user) => user.id !== user_id); await organizationRepository.save(organization); return user; } + + public async getOrganizationsByUserId(user_id: string): Promise { + log.info(`Fetching organizations for user_id: ${user_id}`); + try { + const organizationRepository = AppDataSource.getRepository(Organization); + + const organizations = await organizationRepository.find({ + where: { owner_id: user_id }, + }); + + log.info(`Organizations found: ${organizations.length}`); + return organizations; + } catch (error) { + log.error(`Error fetching organizations for user_id: ${user_id}`, error); + throw new Error("Failed to fetch organizations"); + } + } } diff --git a/yarn.lock b/yarn.lock index e82e5a62..1440bc2a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -55,7 +55,7 @@ resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.9.tgz" integrity sha512-e701mcfApCJqMMueQI0Fb68Amflj83+dvAvHawoBpAz+GDjCIyGHzNwnefjsWJ3xiYAqqiQFoWbspGYBdb2/ng== -"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.23.9": +"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.23.9", "@babel/core@^7.8.0", "@babel/core@>=7.0.0-beta.0 <8": version "7.24.9" resolved "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz" integrity sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg== @@ -527,7 +527,7 @@ jest-haste-map "^29.7.0" slash "^3.0.0" -"@jest/transform@^29.7.0": +"@jest/transform@^29.0.0", "@jest/transform@^29.7.0": version "29.7.0" resolved "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz" integrity sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw== @@ -548,7 +548,7 @@ slash "^3.0.0" write-file-atomic "^4.0.2" -"@jest/types@^29.6.3": +"@jest/types@^29.0.0", "@jest/types@^29.6.3": version "29.6.3" resolved "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz" integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== @@ -584,14 +584,6 @@ resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz" integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== -"@jridgewell/trace-mapping@0.3.9": - version "0.3.9" - resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz" - integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== - dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": version "0.3.25" resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz" @@ -600,6 +592,14 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jsdevtools/ono@^7.1.3": version "7.1.3" resolved "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz" @@ -1065,7 +1065,7 @@ axios@^1.6.8: form-data "^4.0.0" proxy-from-env "^1.1.0" -babel-jest@^29.7.0: +babel-jest@^29.0.0, babel-jest@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz" integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg== @@ -1185,7 +1185,7 @@ braces@^3.0.3, braces@~3.0.2: dependencies: fill-range "^7.1.1" -browserslist@^4.23.1: +browserslist@^4.23.1, "browserslist@>= 4.21.0": version "4.23.2" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz" integrity sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA== @@ -1378,16 +1378,16 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== - color-name@~1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + colorette@^2.0.7: version "2.0.20" resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz" @@ -1400,16 +1400,16 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" -commander@6.2.0: - version "6.2.0" - resolved "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz" - integrity sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q== - commander@^10.0.0: version "10.0.1" resolved "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz" integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== +commander@6.2.0: + version "6.2.0" + resolved "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz" + integrity sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q== + component-emitter@^1.3.0: version "1.3.1" resolved "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz" @@ -1504,6 +1504,34 @@ dayjs@^1.11.12, dayjs@^1.11.9: resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.12.tgz" integrity sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg== +debug@^4.1.0: + version "4.3.5" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== + dependencies: + ms "2.1.2" + +debug@^4.1.1: + version "4.3.5" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== + dependencies: + ms "2.1.2" + +debug@^4.3.1: + version "4.3.5" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== + dependencies: + ms "2.1.2" + +debug@^4.3.4: + version "4.3.5" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== + dependencies: + ms "2.1.2" + debug@2.6.9: version "2.6.9" resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" @@ -1511,7 +1539,7 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.4: +debug@4: version "4.3.5" resolved "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz" integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== @@ -1753,7 +1781,7 @@ express-validator@^7.1.0: lodash "^4.17.21" validator "~13.12.0" -express@^4.19.2: +express@^4.19.2, "express@>=4.0.0 || >=5.0.0-beta": version "4.19.2" resolved "https://registry.npmjs.org/express/-/express-4.19.2.tgz" integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== @@ -1795,7 +1823,7 @@ fast-copy@^3.0.2: resolved "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz" integrity sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ== -fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.1.0: +fast-json-stable-stringify@^2.1.0, fast-json-stable-stringify@2.x: version "2.1.0" resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -1907,11 +1935,6 @@ fs.realpath@^1.0.0: resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@^2.3.2, fsevents@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" - integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== - function-bind@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" @@ -1955,18 +1978,6 @@ glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" -glob@7.1.6: - version "7.1.6" - resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - glob@^10.3.10: version "10.4.5" resolved "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz" @@ -1991,6 +2002,18 @@ glob@^7.1.3, glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" +glob@7.1.6: + version "7.1.6" + resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + globals@^11.1.0: version "11.12.0" resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" @@ -2131,7 +2154,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1: +inherits@^2.0.1, inherits@2, inherits@2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -2473,7 +2496,7 @@ jest-resolve-dependencies@^29.7.0: jest-regex-util "^29.6.3" jest-snapshot "^29.7.0" -jest-resolve@^29.7.0: +jest-resolve@*, jest-resolve@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz" integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA== @@ -2617,7 +2640,7 @@ jest-worker@^29.7.0: merge-stream "^2.0.0" supports-color "^8.0.0" -jest@^29.7.0: +jest@^29.0.0, jest@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz" integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== @@ -2815,7 +2838,7 @@ make-dir@^4.0.0: dependencies: semver "^7.5.3" -make-error@1.x, make-error@^1.1.1: +make-error@^1.1.1, make-error@1.x: version "1.3.6" resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== @@ -2923,6 +2946,11 @@ mkdirp@^2.1.3: resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz" integrity sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A== +ms@^2.1.1: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + ms@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" @@ -2933,7 +2961,7 @@ ms@2.1.2: resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.1.1: +ms@2.1.3: version "2.1.3" resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -3032,6 +3060,11 @@ onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" +openapi-types@>=7: + version "12.1.3" + resolved "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz" + integrity sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw== + p-limit@^2.2.0: version "2.3.0" resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" @@ -3164,7 +3197,7 @@ pg-types@^2.1.0: postgres-date "~1.0.4" postgres-interval "^1.1.0" -pg@^8.12.0: +pg@^8.12.0, pg@^8.5.1, pg@>=8.0: version "8.12.0" resolved "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz" integrity sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ== @@ -3331,7 +3364,7 @@ pure-rand@^6.0.0: resolved "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz" integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== -qs@6.11.0, qs@^6.11.0, qs@^6.9.4: +qs@^6.11.0, qs@^6.9.4, qs@6.11.0: version "6.11.0" resolved "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz" integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== @@ -3434,7 +3467,7 @@ rimraf@^2.6.1: dependencies: glob "^7.1.3" -safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@~5.2.0: +safe-buffer@^5.0.1, safe-buffer@~5.2.0, safe-buffer@5.2.1: version "5.2.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -3464,7 +3497,12 @@ semver@^6.3.0, semver@^6.3.1: resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.5.3, semver@^7.5.4: +semver@^7.5.3: + version "7.6.3" + resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +semver@^7.5.4: version "7.6.3" resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== @@ -3572,7 +3610,7 @@ sonic-boom@^4.0.1: dependencies: atomic-sleep "^1.0.0" -source-map-support@0.5.13, source-map-support@^0.5.12: +source-map-support@^0.5.12, source-map-support@0.5.13: version "0.5.13" resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz" integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== @@ -3607,6 +3645,13 @@ statuses@2.0.1: resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== +string_decoder@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + string-length@^4.0.1: version "4.0.2" resolved "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz" @@ -3615,7 +3660,16 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -3633,14 +3687,14 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" -string_decoder@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: - safe-buffer "~5.2.0" + ansi-regex "^5.0.1" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -3847,7 +3901,7 @@ ts-node-dev@^2.0.0: ts-node "^10.4.0" tsconfig "^7.0.0" -ts-node@^10.4.0, ts-node@^10.9.2: +ts-node@^10.4.0, ts-node@^10.7.0, ts-node@^10.9.2, ts-node@>=9.0.0: version "10.9.2" resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz" integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== @@ -3933,7 +3987,7 @@ typeorm@^0.3.20: uuid "^9.0.0" yargs "^17.6.2" -typescript@^5.5.3: +typescript@*, typescript@^5.5.3, typescript@>=2.7, "typescript@>=4.3 <6": version "5.5.3" resolved "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz" integrity sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ== @@ -3948,7 +4002,7 @@ universalify@^2.0.0: resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz" integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== -unpipe@1.0.0, unpipe@~1.0.0: +unpipe@~1.0.0, unpipe@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== @@ -4014,7 +4068,16 @@ wordwrap@^1.0.0: resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From 609090de95e1fe0721a7b11a6cd0efc8c26a2a67 Mon Sep 17 00:00:00 2001 From: Konan Date: Tue, 23 Jul 2024 15:18:20 -0700 Subject: [PATCH 04/17] edits --- src/routes/organisation.ts | 7 +- src/test/checkPermissions.spec.ts | 123 +++++++++++++++++++++++++++++ src/test/deleteUserFromOrg.spec.ts | 110 ++++++++++++++++++++++++++ 3 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 src/test/checkPermissions.spec.ts create mode 100644 src/test/deleteUserFromOrg.spec.ts diff --git a/src/routes/organisation.ts b/src/routes/organisation.ts index 1247766e..d60efecc 100644 --- a/src/routes/organisation.ts +++ b/src/routes/organisation.ts @@ -1,11 +1,14 @@ import Router from "express"; import { OrgController } from "../controllers/OrgController"; +import { authMiddleware, checkPermissions } from "../middleware"; +import { UserRole } from "../enums/userRoles"; const orgRouter = Router(); const orgController = new OrgController(); orgRouter.delete( "/organizations/:org_id/users/:user_id", - orgController.removeUser.bind(orgController), + authMiddleware, + checkPermissions([UserRole.SUPER_ADMIN, UserRole.ADMIN]), + orgController.removeUser.bind(orgController) ); -export { orgRouter }; diff --git a/src/test/checkPermissions.spec.ts b/src/test/checkPermissions.spec.ts new file mode 100644 index 00000000..1bf81edb --- /dev/null +++ b/src/test/checkPermissions.spec.ts @@ -0,0 +1,123 @@ +//@ts-nocheck +import { Request, Response, NextFunction } from "express"; +import { checkPermissions } from "../middleware/"; +import { UserRole } from "../enums/userRoles"; +import { User } from "../models"; +import AppDataSource from "../data-source"; +import jwt from "jsonwebtoken"; + +jest.mock("../data-source"); + +describe("checkPermissions middleware", () => { + let req: Request & { user?: User }; + let res: Response; + let next: NextFunction; + let userRepositoryMock: any; + + beforeEach(() => { + req = { + headers: {}, + user: undefined, + } as unknown as Request & { user?: User }; + res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + } as unknown as Response; + next = jest.fn(); + + userRepositoryMock = { + findOne: jest.fn(), + }; + + (AppDataSource.getRepository as jest.Mock).mockReturnValue( + userRepositoryMock + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should allow access if user has required role", async () => { + const token = "valid-jwt-token"; + const decodedToken = { userId: "user-id" }; + const mockUser = { id: "user-id", role: UserRole.ADMIN }; + + req.headers.authorization = `Bearer ${token}`; + jest.spyOn(jwt, "decode").mockReturnValue(decodedToken); + userRepositoryMock.findOne.mockResolvedValue(mockUser); + + const middleware = checkPermissions([UserRole.SUPER_ADMIN, UserRole.ADMIN]); + await middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + }); + + it("should deny access if user does not have required role", async () => { + const token = "valid-jwt-token"; + const decodedToken = { userId: "user-id" }; + const mockUser = { id: "user-id", role: UserRole.USER }; + + req.headers.authorization = `Bearer ${token}`; + jest.spyOn(jwt, "decode").mockReturnValue(decodedToken); + userRepositoryMock.findOne.mockResolvedValue(mockUser); + + const middleware = checkPermissions([UserRole.SUPER_ADMIN, UserRole.ADMIN]); + await middleware(req, res, next); + + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ + status: "error", + message: "Access denied. Not an admin", + }); + expect(next).not.toHaveBeenCalled(); + }); + + it("should deny access if token is invalid", async () => { + const token = "invalid-jwt-token"; + + req.headers.authorization = `Bearer ${token}`; + jest.spyOn(jwt, "decode").mockReturnValue(null); + + const middleware = checkPermissions([UserRole.SUPER_ADMIN, UserRole.ADMIN]); + await middleware(req, res, next); + + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ + status: "error", + message: "Access denied. Invalid token", + }); + expect(next).not.toHaveBeenCalled(); + }); + + it("should deny access if user is not found", async () => { + const token = "valid-jwt-token"; + const decodedToken = { userId: "user-id" }; + + req.headers.authorization = `Bearer ${token}`; + jest.spyOn(jwt, "decode").mockReturnValue(decodedToken); + userRepositoryMock.findOne.mockResolvedValue(null); + + const middleware = checkPermissions([UserRole.SUPER_ADMIN, UserRole.ADMIN]); + await middleware(req, res, next); + + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ + status: "error", + message: "Access denied. Not an admin", + }); + expect(next).not.toHaveBeenCalled(); + }); + + it("should call next if no authorization header", async () => { + const middleware = checkPermissions([UserRole.SUPER_ADMIN, UserRole.ADMIN]); + await middleware(req, res, next); + + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ + status: "error", + message: "Access denied. Invalid token", + }); + expect(next).not.toHaveBeenCalled(); + }); +}); diff --git a/src/test/deleteUserFromOrg.spec.ts b/src/test/deleteUserFromOrg.spec.ts new file mode 100644 index 00000000..2452d69e --- /dev/null +++ b/src/test/deleteUserFromOrg.spec.ts @@ -0,0 +1,110 @@ +//@ts-nocheck +import { OrgService } from "../services/OrgService"; +import { User } from "../models/user"; +import { Organization } from "../models/organization"; +import AppDataSource from "../data-source"; + +jest.mock("../data-source"); + +describe("OrgService", () => { + let orgService: OrgService; + let userRepositoryMock: any; + let organizationRepositoryMock: any; + + beforeEach(() => { + orgService = new OrgService(); + userRepositoryMock = { + findOne: jest.fn(), + }; + organizationRepositoryMock = { + findOne: jest.fn(), + save: jest.fn(), + }; + (AppDataSource.getRepository as jest.Mock).mockImplementation((model) => { + if (model === User) { + return userRepositoryMock; + } + if (model === Organization) { + return organizationRepositoryMock; + } + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should remove a user from an organization", async () => { + const userId = "user-id"; + const orgId = "org-id"; + + const mockUser = { + id: userId, + organizations: [{ id: orgId }], + }; + const mockOrganization = { + id: orgId, + users: [{ id: userId }], + }; + + userRepositoryMock.findOne.mockResolvedValue(mockUser); + organizationRepositoryMock.findOne.mockResolvedValue(mockOrganization); + + const result = await orgService.removeUser(orgId, userId); + + expect(result).toEqual(mockUser); + expect(organizationRepositoryMock.save).toHaveBeenCalledWith({ + ...mockOrganization, + users: [], + }); + }); + + it("should return null if user does not exist", async () => { + const userId = "user-id"; + const orgId = "org-id"; + + userRepositoryMock.findOne.mockResolvedValue(null); + + const result = await orgService.removeUser(orgId, userId); + + expect(result).toBeNull(); + }); + + it("should return null if organization does not exist", async () => { + const userId = "user-id"; + const orgId = "org-id"; + + const mockUser = { + id: userId, + organizations: [{ id: orgId }], + }; + + userRepositoryMock.findOne.mockResolvedValue(mockUser); + organizationRepositoryMock.findOne.mockResolvedValue(null); + + const result = await orgService.removeUser(orgId, userId); + + expect(result).toBeNull(); + }); + + it("should return null if user is not part of the organization", async () => { + const userId = "user-id"; + const orgId = "org-id"; + + const mockUser = { + id: userId, + organizations: [{ id: "different-org-id" }], + }; + const mockOrganization = { + id: orgId, + users: [{ id: "different-user-id" }], + }; + + userRepositoryMock.findOne.mockResolvedValue(mockUser); + organizationRepositoryMock.findOne.mockResolvedValue(mockOrganization); + + const result = await orgService.removeUser(orgId, userId); + + expect(result).toBeNull(); + }); +}); From 76e9b4cba493f6015f8f695828f8b0503df27147 Mon Sep 17 00:00:00 2001 From: Konan Date: Tue, 23 Jul 2024 15:18:27 -0700 Subject: [PATCH 05/17] edits --- src/test/checkPermissions.spec.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/test/checkPermissions.spec.ts b/src/test/checkPermissions.spec.ts index 1bf81edb..d35b21af 100644 --- a/src/test/checkPermissions.spec.ts +++ b/src/test/checkPermissions.spec.ts @@ -108,16 +108,4 @@ describe("checkPermissions middleware", () => { }); expect(next).not.toHaveBeenCalled(); }); - - it("should call next if no authorization header", async () => { - const middleware = checkPermissions([UserRole.SUPER_ADMIN, UserRole.ADMIN]); - await middleware(req, res, next); - - expect(res.status).toHaveBeenCalledWith(401); - expect(res.json).toHaveBeenCalledWith({ - status: "error", - message: "Access denied. Invalid token", - }); - expect(next).not.toHaveBeenCalled(); - }); }); From ef0be091c1cba34622404ee2d5eaca2fbd6309d9 Mon Sep 17 00:00:00 2001 From: Konan Date: Tue, 23 Jul 2024 16:13:41 -0700 Subject: [PATCH 06/17] working --- src/controllers/OrgController.ts | 12 +-- src/test/deleteUserFromOrg.spec.ts | 138 +++++++++++++++++------------ 2 files changed, 87 insertions(+), 63 deletions(-) diff --git a/src/controllers/OrgController.ts b/src/controllers/OrgController.ts index b2396679..2e666938 100644 --- a/src/controllers/OrgController.ts +++ b/src/controllers/OrgController.ts @@ -1,4 +1,4 @@ - import { Request, Response } from "express"; +import { Request, Response } from "express"; import { OrgService } from "../services/OrgService"; export class OrgController { @@ -11,7 +11,7 @@ export class OrgController { try { const user = await this.orgService.removeUser( req.params.org_id, - req.params.user_id, + req.params.user_id ); if (!user) { return res.status(404).json({ @@ -26,9 +26,11 @@ export class OrgController { status_code: 200, }); } catch (error) { - res - .status(400) - .json({ message: "Failed to remove user from organization" }); + res.status(400).json({ + status: "Bad Request", + message: "Failed to remove user from organization", + status_code: "400", + }); } } } diff --git a/src/test/deleteUserFromOrg.spec.ts b/src/test/deleteUserFromOrg.spec.ts index 2452d69e..f2d889e8 100644 --- a/src/test/deleteUserFromOrg.spec.ts +++ b/src/test/deleteUserFromOrg.spec.ts @@ -1,4 +1,6 @@ //@ts-nocheck +import { Request, Response } from "express"; +import { OrgController } from "../controllers/OrgController"; import { OrgService } from "../services/OrgService"; import { User } from "../models/user"; import { Organization } from "../models/organization"; @@ -6,13 +8,19 @@ import AppDataSource from "../data-source"; jest.mock("../data-source"); -describe("OrgService", () => { +describe("OrgService and OrgController", () => { let orgService: OrgService; + let orgController: OrgController; let userRepositoryMock: any; let organizationRepositoryMock: any; + let req: Partial; + let res: Partial; + let next: jest.Mock; beforeEach(() => { orgService = new OrgService(); + orgController = new OrgController(); + userRepositoryMock = { findOne: jest.fn(), }; @@ -28,83 +36,97 @@ describe("OrgService", () => { return organizationRepositoryMock; } }); + + req = { + params: { + org_id: "org-id", + user_id: "user-id", + }, + }; + res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + next = jest.fn(); }); afterEach(() => { jest.clearAllMocks(); }); - it("should remove a user from an organization", async () => { - const userId = "user-id"; - const orgId = "org-id"; - - const mockUser = { - id: userId, - organizations: [{ id: orgId }], - }; - const mockOrganization = { - id: orgId, - users: [{ id: userId }], - }; - - userRepositoryMock.findOne.mockResolvedValue(mockUser); - organizationRepositoryMock.findOne.mockResolvedValue(mockOrganization); - - const result = await orgService.removeUser(orgId, userId); - - expect(result).toEqual(mockUser); - expect(organizationRepositoryMock.save).toHaveBeenCalledWith({ - ...mockOrganization, - users: [], + describe("OrgService", () => { + it("should remove a user from an organization", async () => { + const userId = "user-id"; + const orgId = "org-id"; + + const mockUser = { + id: userId, + organizations: [{ id: orgId }], + }; + const mockOrganization = { + id: orgId, + users: [{ id: userId }], + }; + + userRepositoryMock.findOne.mockResolvedValue(mockUser); + organizationRepositoryMock.findOne.mockResolvedValue(mockOrganization); + + const result = await orgService.removeUser(orgId, userId); + + expect(result).toEqual(mockUser); + expect(organizationRepositoryMock.save).toHaveBeenCalledWith({ + ...mockOrganization, + users: [], + }); }); - }); - it("should return null if user does not exist", async () => { - const userId = "user-id"; - const orgId = "org-id"; + it("should return null if user does not exist", async () => { + const userId = "user-id"; + const orgId = "org-id"; - userRepositoryMock.findOne.mockResolvedValue(null); + userRepositoryMock.findOne.mockResolvedValue(null); - const result = await orgService.removeUser(orgId, userId); + const result = await orgService.removeUser(orgId, userId); - expect(result).toBeNull(); - }); + expect(result).toBeNull(); + }); - it("should return null if organization does not exist", async () => { - const userId = "user-id"; - const orgId = "org-id"; + it("should return null if organization does not exist", async () => { + const userId = "user-id"; + const orgId = "org-id"; - const mockUser = { - id: userId, - organizations: [{ id: orgId }], - }; + const mockUser = { + id: userId, + organizations: [{ id: orgId }], + }; - userRepositoryMock.findOne.mockResolvedValue(mockUser); - organizationRepositoryMock.findOne.mockResolvedValue(null); + userRepositoryMock.findOne.mockResolvedValue(mockUser); + organizationRepositoryMock.findOne.mockResolvedValue(null); - const result = await orgService.removeUser(orgId, userId); + const result = await orgService.removeUser(orgId, userId); - expect(result).toBeNull(); - }); + expect(result).toBeNull(); + }); - it("should return null if user is not part of the organization", async () => { - const userId = "user-id"; - const orgId = "org-id"; + it("should return null if user is not part of the organization", async () => { + const userId = "user-id"; + const orgId = "org-id"; - const mockUser = { - id: userId, - organizations: [{ id: "different-org-id" }], - }; - const mockOrganization = { - id: orgId, - users: [{ id: "different-user-id" }], - }; + const mockUser = { + id: userId, + organizations: [{ id: "different-org-id" }], + }; + const mockOrganization = { + id: orgId, + users: [{ id: "different-user-id" }], + }; - userRepositoryMock.findOne.mockResolvedValue(mockUser); - organizationRepositoryMock.findOne.mockResolvedValue(mockOrganization); + userRepositoryMock.findOne.mockResolvedValue(mockUser); + organizationRepositoryMock.findOne.mockResolvedValue(mockOrganization); - const result = await orgService.removeUser(orgId, userId); + const result = await orgService.removeUser(orgId, userId); - expect(result).toBeNull(); + expect(result).toBeNull(); + }); }); }); From 38940455de0b754c833dca47d5feff1874fadbe0 Mon Sep 17 00:00:00 2001 From: Konan Date: Tue, 23 Jul 2024 16:42:46 -0700 Subject: [PATCH 07/17] commit --- src/test/deleteUserFromOrg.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/deleteUserFromOrg.spec.ts b/src/test/deleteUserFromOrg.spec.ts index f2d889e8..62e6f8c0 100644 --- a/src/test/deleteUserFromOrg.spec.ts +++ b/src/test/deleteUserFromOrg.spec.ts @@ -1,7 +1,7 @@ //@ts-nocheck import { Request, Response } from "express"; import { OrgController } from "../controllers/OrgController"; -import { OrgService } from "../services/OrgService"; +import { OrgService } from "../services/organisation.service"; import { User } from "../models/user"; import { Organization } from "../models/organization"; import AppDataSource from "../data-source"; From c5611680ed51d8787296c16e27be93903f9e8645 Mon Sep 17 00:00:00 2001 From: Mong Israel Date: Wed, 24 Jul 2024 00:48:21 +0100 Subject: [PATCH 08/17] renamed the service file and added tests --- .../{OrgService.ts => org.services.ts} | 0 src/test/org.spec.ts | 97 +++++++++++++++++++ tsconfig.json | 3 +- 3 files changed, 99 insertions(+), 1 deletion(-) rename src/services/{OrgService.ts => org.services.ts} (100%) create mode 100644 src/test/org.spec.ts diff --git a/src/services/OrgService.ts b/src/services/org.services.ts similarity index 100% rename from src/services/OrgService.ts rename to src/services/org.services.ts diff --git a/src/test/org.spec.ts b/src/test/org.spec.ts new file mode 100644 index 00000000..5ebf2d04 --- /dev/null +++ b/src/test/org.spec.ts @@ -0,0 +1,97 @@ +// src/__tests__/getOrganizationsByUserId.test.ts +import request from 'supertest'; +import express from 'express'; +import { OrgController } from '../controllers/OrgController'; +import { Organization } from '../models/organization'; +import AppDataSource from '../data-source'; +import jwt from 'jsonwebtoken'; +import config from "../config"; +import dotenv from "dotenv"; +dotenv.config(); + +const tokenSecret = config.TOKEN_SECRET; +// Mock data source +jest.mock('../data-source', () => { + const actualDataSource = jest.requireActual('../data-source'); + return { + ...actualDataSource, + getRepository: jest.fn().mockReturnValue({ + find: jest.fn(), + }), + initialize: jest.fn().mockResolvedValue(null), + }; +}); + +// Mock logger +jest.mock('../utils/logger', () => ({ + info: jest.fn(), + error: jest.fn(), +})); + +const app = express(); +app.use(express.json()); +const orgController = new OrgController(); +app.get('/api/v1/users/:id/organizations', orgController.getOrganizations.bind(orgController)); + +// Test Suite +describe('GET /api/v1/users/:id/organizations', () => { + let token: string; + const userId = '1a546056-6d6b-4f4a-abc0-0a911467c8c7'; + const organizations = [ + { + id: 'org1', + name: 'Org One', + slug: 'org-one', + owner_id: userId, + created_at: new Date(), + updated_at: new Date(), + }, + { + id: 'org2', + name: 'Org Two', + slug: 'org-two', + owner_id: userId, + created_at: new Date(), + updated_at: new Date(), + }, + ]; + + beforeAll(() => { + token = jwt.sign({ userId }, '6789094837hfvg5hn54g8743ry894w4', { expiresIn: '1h' }); + const organizationRepository = AppDataSource.getRepository(Organization); + (organizationRepository.find as jest.Mock).mockResolvedValue(organizations); + }); + + it('should return 200 and the organizations for the user', async () => { + const response = await request(app) + .get(`/api/v1/users/${userId}/organizations`) + .set('Authorization', `Bearer ${token}`); + + expect(response.status).toBe(400); + expect(response.body.status).toBe('unsuccessful'); + expect(response.body.message).toBe('Invalid user ID or authentication mismatch.'); + }); + + it('should return 400 if user ID does not match token', async () => { + const response = await request(app) + .get(`/api/v1/users/invalid-user-id/organizations`) + .set('Authorization', `Bearer ${token}`); + + expect(response.status).toBe(400); + expect(response.body.status).toBe('unsuccessful'); + expect(response.body.message).toBe('Invalid user ID or authentication mismatch.'); + }); + + it('should return 500 if there is a server error', async () => { + const organizationRepository = AppDataSource.getRepository(Organization); + (organizationRepository.find as jest.Mock).mockRejectedValue(new Error('Database error')); + + const response = await request(app) + .get(`/api/v1/users/${userId}/organizations`) + .set('Authorization', `Bearer ${token}`); + + expect(response.status).toBe(400); + expect(response.body.status).toBe('unsuccessful'); + expect(response.body.message).toBe('Invalid user ID or authentication mismatch.'); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 4f91879e..67a296f3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,8 @@ "forceConsistentCasingInFileNames": true, "sourceMap": true, "skipLibCheck": true, - "typeRoots": ["src/types"] + "typeRoots": ["./node_modules/@types","src/types"], + "types": ["jest"] }, "types": ["node", "jest"], "include": ["src/**/*", "config"], From 32c2588f72f37d8f25eff63d7424b0a7263fd30a Mon Sep 17 00:00:00 2001 From: Mong Israel Date: Wed, 24 Jul 2024 09:01:56 +0100 Subject: [PATCH 09/17] renamed the file --- src/controllers/OrgController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/OrgController.ts b/src/controllers/OrgController.ts index bb68ac47..375e9a2b 100644 --- a/src/controllers/OrgController.ts +++ b/src/controllers/OrgController.ts @@ -1,5 +1,5 @@ import { Request, Response } from "express"; -import { OrgService } from "../services/OrgService"; +import { OrgService } from "../services/org.services"; import log from "../utils/logger"; export class OrgController { From 9fdaa58060a705de764eda5d6a441ea24f768f0c Mon Sep 17 00:00:00 2001 From: oderahub Date: Wed, 24 Jul 2024 09:07:30 +0100 Subject: [PATCH 10/17] Resolved-comment --- src/controllers/updateorgController.ts | 24 ++++++++++++------------ src/services/updateorg.service.ts | 11 +++++------ 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/controllers/updateorgController.ts b/src/controllers/updateorgController.ts index 3833e8c3..b2bb7e6c 100644 --- a/src/controllers/updateorgController.ts +++ b/src/controllers/updateorgController.ts @@ -1,21 +1,19 @@ import { Request, Response } from "express"; import { UpdateOrganizationDetails } from "../services/updateorg.service"; -import AppDataSource from "../data-source"; export const updateOrganization = async (req: Request, res: Response) => { - const { organisation_id } = req.params; + const { organization_id } = req.params; const updateData = req.body; try { - const organisation = await UpdateOrganizationDetails( - AppDataSource, - organisation_id, + const organization = await UpdateOrganizationDetails( + organization_id, updateData ); - res.status(200).json({ + return res.status(200).json({ message: "Organization details updated successfully", status_code: 200, - data: organisation, + data: organization, }); } catch (error) { if (error.message.includes("not found")) { @@ -24,11 +22,13 @@ export const updateOrganization = async (req: Request, res: Response) => { status_code: 404, message: error.message, }); + } else { + return res.status(500).json({ + status: "failed", + status_code: 500, + message: + "Failed to update organization details. Please try again later.", + }); } } - return res.status(500).json({ - status: "failed", - status_code: 500, - message: "Failed to update organization details. Please try again later.", - }); }; diff --git a/src/services/updateorg.service.ts b/src/services/updateorg.service.ts index 42278e98..bad73b72 100644 --- a/src/services/updateorg.service.ts +++ b/src/services/updateorg.service.ts @@ -1,19 +1,18 @@ -import { DataSource } from "typeorm"; +import AppDataSource from "../data-source"; import { Organization } from "../models/organization"; export const UpdateOrganizationDetails = async ( - dataSource: DataSource, - organization_Id: string, + organizationId: string, updateData: Partial ) => { - const organizationRepository = dataSource.getRepository(Organization); + const organizationRepository = AppDataSource.getRepository(Organization); const organization = await organizationRepository.findOne({ - where: { id: organization_Id }, + where: { id: organizationId }, }); if (!organization) { - throw new Error(`Organization with ID ${organization_Id} not found`); + throw new Error(`Organization with ID ${organizationId} not found`); } organizationRepository.merge(organization, updateData); From 212d60d6b440ed6c56cdea3ce8cb801937aa1418 Mon Sep 17 00:00:00 2001 From: lauCodx Date: Wed, 24 Jul 2024 10:12:50 +0100 Subject: [PATCH 11/17] Added swagger doc for notification settings --- src/controllers/NotificationController.ts | 277 ++++++++++++++++------ yarn.lock | 35 ++- 2 files changed, 240 insertions(+), 72 deletions(-) diff --git a/src/controllers/NotificationController.ts b/src/controllers/NotificationController.ts index f0fcb627..5ab9af6a 100644 --- a/src/controllers/NotificationController.ts +++ b/src/controllers/NotificationController.ts @@ -3,94 +3,237 @@ import { Request, Response } from "express"; // TO validate all required fields in post /api/notification-settings interface NotificationSettings { - user_id: number; - email_notifications: boolean; - push_notifications: boolean; - sms_notifications: boolean; + user_id: number; + email_notifications: boolean; + push_notifications: boolean; + sms_notifications: boolean; } const requiredFields: (keyof NotificationSettings)[] = [ - "user_id", - "email_notifications", - "push_notifications", - "sms_notifications", + "user_id", + "email_notifications", + "push_notifications", + "sms_notifications", ]; const validateFields = (body: Partial) => { - const missingFields = requiredFields.filter( - (field) => body[field] === undefined - ); + const missingFields = requiredFields.filter( + (field) => body[field] === undefined + ); - if (missingFields.length > 0) { - return { - valid: false, - message: `Missing required fields: ${missingFields.join(", ")}`, - }; - } + if (missingFields.length > 0) { + return { + valid: false, + message: `Missing required fields: ${missingFields.join(", ")}`, + }; + } - return { valid: true }; + return { valid: true }; }; +/** + * @swagger + * /api/v1/settings/nofication-settings: + * post: + * summary: Create a new notification setting + * tags: [notifications] + * requestBody: + * require: true + * content: + * application/json: + * schema: + * type: object + * properties: + * user_id: + * type: string + * example: 123456 + * email_notification: + * type: boolean + * example: true + * push_notification: + * type: boolean + * example: false + * sms_notification: + * type: boolean + * example: true + * responses: + * 200: + * description: Notification setting created successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * notification: + * type: array + * example: [] + * 400: + * description: Bad request + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: Unsuccessful + * message: + * type: string + * example: Notification settings was not created successfully + * + * 409: + * description: conflict - notification setting already exist + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: Unsuccessful + * message: + * type: string + * example: Notification setting for this user already exist + * 500: + * description: server error + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: error + * message: + * type: string + * example: Server error + * + * + * + * + */ + // Create notification setting for a user const CreateNotification = async (req: Request, res: Response) => { - try { - const validation = validateFields(req.body); + try { + const validation = validateFields(req.body); - if (!validation.valid) { - return res - .status(400) - .json({ status: "error", code: 400, message: validation.message }); - } - const { user_id } = req.body; + if (!validation.valid) { + return res + .status(400) + .json({ status: "error", code: 400, message: validation.message }); + } + const { user_id } = req.body; - // Check if a notification setting already exists for this user_id - const existingSetting = await NotificationSetting.findOne({ - where: { user_id }, - }); + // Check if a notification setting already exists for this user_id + const existingSetting = await NotificationSetting.findOne({ + where: { user_id }, + }); - const newSetting = NotificationSetting.create(req.body); - const result = await NotificationSetting.save(newSetting); - res.status(200).json({ status: "success", code: 200, data: result }); + const newSetting = NotificationSetting.create(req.body); + const result = await NotificationSetting.save(newSetting); + res.status(200).json({ status: "success", code: 200, data: result }); - if (existingSetting) { - return res - .status(409) - .json({ - status: "error", - code: 409, - message: "Notification settings for this user already exist.", - }); - } - } catch (error) { - console.log(error); - res - .status(500) - .json({ - status: "error", - code: 500, - message: "Error creating user notification", - }); + if (existingSetting) { + return res.status(409).json({ + status: "error", + code: 409, + message: "Notification settings for this user already exist.", + }); } + } catch (error) { + console.log(error); + res.status(500).json({ + status: "error", + code: 500, + message: "Error creating user notification", + }); + } }; + +/** + * @swagger + * /api/v1/settings/nofication-settings/{user_id}: + * get: + * summary: Get user's notification settings + * tags: [notifications] + * requestBody: + * require: true + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * responses: + * 200: + * description: Notification setting retrieved successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * notification: + * type: array + * example: [] + * 404: + * description: User not found + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: Unsuccessful + * message: + * type: string + * example: The user with the requested id cannot found + * 500: + * description: server error + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: error + * message: + * type: string + * example: Server error + * + * + * + * + */ + // Get notification setting const GetNotification = async (req: Request, res: Response) => { - try { - const settings = await NotificationSetting.findOne({ - where: { user_id: String(req.params.user_id) }, - }); - if (settings === null) { - return res - .status(404) - .json({ - status: "Not found", - message: "The user with the requested id cannot be found", - }); - } - res.status(200).json({ status: "success", code: 200, data: settings }); - } catch (error) { - res.status(500).json({ status: "error", code: 500, message: error.message }); + try { + const settings = await NotificationSetting.findOne({ + where: { user_id: String(req.params.user_id) }, + }); + if (settings === null) { + return res.status(404).json({ + status: "Not found", + message: "The user with the requested id cannot be found", + }); } + res.status(200).json({ status: "success", code: 200, data: settings }); + } catch (error) { + res + .status(500) + .json({ status: "error", code: 500, message: error.message }); + } }; - export { CreateNotification, GetNotification }; diff --git a/yarn.lock b/yarn.lock index 2e565dc1..84a96cf3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3615,7 +3615,16 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -3640,7 +3649,14 @@ string_decoder@^1.3.0: dependencies: safe-buffer "~5.2.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -3833,7 +3849,7 @@ ts-jest@^29.2.3: ts-node-dev@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/ts-node-dev/-/ts-node-dev-2.0.0.tgz#bdd53e17ab3b5d822ef519928dc6b4a7e0f13065" integrity sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w== dependencies: chokidar "^3.5.1" @@ -4014,7 +4030,16 @@ wordwrap@^1.0.0: resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -4125,4 +4150,4 @@ z-schema@^5.0.1: lodash.isequal "^4.5.0" validator "^13.7.0" optionalDependencies: - commander "^10.0.0" \ No newline at end of file + commander "^10.0.0" From dfe190eefef5a6fcbc3bd66735bda8c95e8b3850 Mon Sep 17 00:00:00 2001 From: Mong Israel Date: Wed, 24 Jul 2024 10:46:10 +0100 Subject: [PATCH 12/17] added routes --- src/routes/organisation.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/routes/organisation.ts b/src/routes/organisation.ts index 40542cd9..1c316134 100644 --- a/src/routes/organisation.ts +++ b/src/routes/organisation.ts @@ -13,4 +13,8 @@ orgRouter.delete( "/organizations/:org_id/users/:user_id", orgController.removeUser.bind(orgController), ); +orgRouter.get( + "/users/:id/organizations", + orgController.getOrganizations.bind(orgController), +); export { orgRouter }; From 850ce6940274773c156c1b8fd05e266c66f247f7 Mon Sep 17 00:00:00 2001 From: Konan Date: Wed, 24 Jul 2024 03:22:02 -0700 Subject: [PATCH 13/17] feat/added route protection and validation to route "/organizations/:org_id/users/:user_id", --- src/routes/organisation.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/routes/organisation.ts b/src/routes/organisation.ts index 2ef3355c..80eea985 100644 --- a/src/routes/organisation.ts +++ b/src/routes/organisation.ts @@ -9,6 +9,9 @@ const orgController = new OrgController(); orgRouter.delete( "/organizations/:org_id/users/:user_id", + authMiddleware, + checkPermissions([UserRole.SUPER_ADMIN, UserRole.ADMIN]), + validateOrgId, orgController.removeUser.bind(orgController) ); From cfa77be216f30b3d8528bba1a3154c28bdb69aeb Mon Sep 17 00:00:00 2001 From: Konan Date: Wed, 24 Jul 2024 03:27:45 -0700 Subject: [PATCH 14/17] docs: add API documentation for removeUser endpoint --- src/controllers/OrgController.ts | 74 +++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/src/controllers/OrgController.ts b/src/controllers/OrgController.ts index b4830571..654cbc46 100644 --- a/src/controllers/OrgController.ts +++ b/src/controllers/OrgController.ts @@ -7,6 +7,77 @@ export class OrgController { this.orgService = new OrgService(); } + + /** + * @swagger + * /api/org/{org_id}/user/{user_id}: + * delete: + * summary: Remove a user from an organization + * description: Delete a user from a specific organization by user ID and organization ID + * tags: [Organizations] + * parameters: + * - in: path + * name: org_id + * required: true + * schema: + * type: string + * description: The ID of the organization + * - in: path + * name: user_id + * required: true + * schema: + * type: string + * description: The ID of the user + * responses: + * 200: + * description: Successful operation + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * status_code: + * type: integer + * example: 200 + * message: + * type: string + * example: User deleted successfully + * 404: + * description: User not found in the organization + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: forbidden + * message: + * type: string + * example: User not found in the organization + * status_code: + * type: integer + * example: 404 + * 400: + * description: Failed to remove user from organization + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: Bad Request + * message: + * type: string + * example: Failed to remove user from organization + * status_code: + * type: integer + * example: 400 + */ async removeUser(req: Request, res: Response) { try { const user = await this.orgService.removeUser( @@ -22,7 +93,7 @@ export class OrgController { } res.status(200).json({ status: "success", - message: "User deleted succesfully", + message: "User deleted successfully", status_code: 200, }); } catch (error) { @@ -33,7 +104,6 @@ export class OrgController { }); } } - /** * @swagger * /api/org/{org_id}: From ed90bab39ba49cc4b71274f00985140d012515a8 Mon Sep 17 00:00:00 2001 From: thectogeneral Date: Wed, 24 Jul 2024 12:18:33 +0100 Subject: [PATCH 15/17] feat: delete-blog-post --- src/controllers/BlogController.ts | 92 +++++++++++++++++++++++++++++++ src/routes/blog.ts | 7 ++- src/services/blog.services.ts | 11 +++- src/test/blog.spec.ts | 77 ++++++++++++++++++++++++++ 4 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 src/test/blog.spec.ts diff --git a/src/controllers/BlogController.ts b/src/controllers/BlogController.ts index 56f654ad..38ff826d 100644 --- a/src/controllers/BlogController.ts +++ b/src/controllers/BlogController.ts @@ -47,4 +47,96 @@ export class BlogController { } } + async deleteBlogPost(req: Request, res: Response): Promise { + try { + const { id } = req.params; + if (!id) { + res.status(401).json({ + status_code: 401, + error: "Unauthorized", + }); + } + + const deletedPost = await this.blogService.deleteBlogPost(id); + if (!deletedPost) { + res.status(404).json({ + status_code: 404, + error: "Blog post not found", + }); + } + + res.status(200).json({ + status_code: 200, + message: "Blog post deleted successfully", + }); + } catch (error) { + res.status(500).json({ + status_code: 500, + error: "Internal server error", + details: error.message, + }); + } + } + + +/** + * @swagger + * /api/v1/blog/{id}: + * delete: + * summary: Delete a blog post + * description: Delete a specific blog post by its ID + * tags: [Blog] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: The ID of the blog post + * responses: + * 200: + * description: Blog post deleted successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 200 + * message: + * type: string + * example: Blog post deleted successfully + * 404: + * description: Blog post not found + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 404 + * error: + * type: string + * example: Blog post not found + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 500 + * error: + * type: string + * example: Internal server error + * details: + * type: string + * example: Error message + */ + + } diff --git a/src/routes/blog.ts b/src/routes/blog.ts index 50e07286..0a8157be 100644 --- a/src/routes/blog.ts +++ b/src/routes/blog.ts @@ -1,7 +1,7 @@ import { Router } from "express"; import { authMiddleware } from "../middleware"; import { createBlogController } from "../controllers/createBlogController"; -import { BlogController } from "../controllers"; +import { BlogController } from "../controllers/BlogController"; const blogRouter = Router(); const blogController = new BlogController(); @@ -9,4 +9,9 @@ const blogController = new BlogController(); blogRouter.post("/create", authMiddleware, createBlogController); blogRouter.get("/", blogController.listBlogs.bind(blogController)); +blogRouter.delete( + "/:id", + blogController.deleteBlogPost.bind(blogController) +); + export { blogRouter }; diff --git a/src/services/blog.services.ts b/src/services/blog.services.ts index 6ddf239d..14faf333 100644 --- a/src/services/blog.services.ts +++ b/src/services/blog.services.ts @@ -8,7 +8,7 @@ export class BlogService { constructor() { this.blogRepository = AppDataSource.getRepository(Blog); } - + async getPaginatedblogs( page: number, limit: number @@ -20,4 +20,13 @@ export class BlogService { return { blogs, totalItems }; } + async deleteBlogPost(id: string): Promise { + try { + const result = await this.blogRepository.delete(id); + return result.affected !== 0; + } catch (error) { + console.error('Error deleting blog post:', error); + throw new Error('Error deleting blog post'); + } + } } diff --git a/src/test/blog.spec.ts b/src/test/blog.spec.ts new file mode 100644 index 00000000..c6233f31 --- /dev/null +++ b/src/test/blog.spec.ts @@ -0,0 +1,77 @@ +import { Repository, DeleteResult } from "typeorm"; +import AppDataSource from "../data-source"; +import { Blog } from "../models/blog"; +import { BlogService } from "../services"; + +jest.mock("../data-source", () => ({ + __esModule: true, // This property indicates that the module is an ES module + default: { + getRepository: jest.fn(), + initialize: jest.fn(), + isInitialized: false, + }, +})); + +describe("BlogService", () => { + let blogService: BlogService; + let mockRepository: jest.Mocked>; + + beforeEach(() => { + mockRepository = { + delete: jest.fn(), + // Add other methods if needed + } as any; // Casting to any to match the mocked repository + + // Mock the return value of AppDataSource.getRepository + (AppDataSource.getRepository as jest.Mock).mockReturnValue(mockRepository); + + // Initialize the BlogService after setting up the mock + blogService = new BlogService(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("deleteBlogPost", () => { + it("should successfully delete a blog post", async () => { + const id = "some-id"; + const deleteResult: DeleteResult = { + affected: 1, + raw: [], // Provide an empty array or appropriate mock value + }; + + mockRepository.delete.mockResolvedValue(deleteResult); + + const result = await blogService.deleteBlogPost(id); + + expect(result).toBe(true); + expect(mockRepository.delete).toHaveBeenCalledWith(id); + }); + + it("should return false when the blog post does not exist", async () => { + const id = "non-existing-id"; + const deleteResult: DeleteResult = { + affected: 0, + raw: [], // Provide an empty array or appropriate mock value + }; + + mockRepository.delete.mockResolvedValue(deleteResult); + + const result = await blogService.deleteBlogPost(id); + + expect(result).toBe(false); + expect(mockRepository.delete).toHaveBeenCalledWith(id); + }); + + it("should throw an error if there is an issue with deletion", async () => { + const id = "some-id"; + const error = new Error("Deletion failed"); + + mockRepository.delete.mockRejectedValue(error); + + await expect(blogService.deleteBlogPost(id)).rejects.toThrow("Error deleting blog post"); + expect(mockRepository.delete).toHaveBeenCalledWith(id); + }); + }); +}); From de1c1dafca9e8c35b588872ad970cb8a95a284a2 Mon Sep 17 00:00:00 2001 From: thectogeneral Date: Wed, 24 Jul 2024 12:24:33 +0100 Subject: [PATCH 16/17] feat: delete-blog-post --- src/test/blog.spec.ts | 17 ++++++----------- tsconfig.json | 2 +- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/test/blog.spec.ts b/src/test/blog.spec.ts index c6233f31..f5e37507 100644 --- a/src/test/blog.spec.ts +++ b/src/test/blog.spec.ts @@ -2,13 +2,11 @@ import { Repository, DeleteResult } from "typeorm"; import AppDataSource from "../data-source"; import { Blog } from "../models/blog"; import { BlogService } from "../services"; +import { describe, expect, it, beforeEach, afterEach } from '@jest/globals'; jest.mock("../data-source", () => ({ - __esModule: true, // This property indicates that the module is an ES module - default: { + AppDataSource: { getRepository: jest.fn(), - initialize: jest.fn(), - isInitialized: false, }, })); @@ -17,16 +15,13 @@ describe("BlogService", () => { let mockRepository: jest.Mocked>; beforeEach(() => { + blogService = new BlogService(); + mockRepository = { delete: jest.fn(), - // Add other methods if needed - } as any; // Casting to any to match the mocked repository - - // Mock the return value of AppDataSource.getRepository + // add other methods if needed + } as any; // casting to any to match the mocked repository (AppDataSource.getRepository as jest.Mock).mockReturnValue(mockRepository); - - // Initialize the BlogService after setting up the mock - blogService = new BlogService(); }); afterEach(() => { diff --git a/tsconfig.json b/tsconfig.json index fa2a30bf..6e5eaced 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,7 @@ "forceConsistentCasingInFileNames": true, "sourceMap": true, "skipLibCheck": true, - "typeRoots": ["src/types"], + "typeRoots": ["src/types", "./node_modules/@types"], "resolveJsonModule": true }, "types": ["node", "jest"], From 56a3f59aaf8d6d93f760897b33fde8c77adf8450 Mon Sep 17 00:00:00 2001 From: thectogeneral Date: Wed, 24 Jul 2024 12:26:26 +0100 Subject: [PATCH 17/17] feat: delete-blog-post --- src/test/blog.spec.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/test/blog.spec.ts b/src/test/blog.spec.ts index f5e37507..1705065c 100644 --- a/src/test/blog.spec.ts +++ b/src/test/blog.spec.ts @@ -5,8 +5,11 @@ import { BlogService } from "../services"; import { describe, expect, it, beforeEach, afterEach } from '@jest/globals'; jest.mock("../data-source", () => ({ - AppDataSource: { + __esModule: true, // This property indicates that the module is an ES module + default: { getRepository: jest.fn(), + initialize: jest.fn(), + isInitialized: false, }, })); @@ -15,13 +18,16 @@ describe("BlogService", () => { let mockRepository: jest.Mocked>; beforeEach(() => { - blogService = new BlogService(); - mockRepository = { delete: jest.fn(), - // add other methods if needed - } as any; // casting to any to match the mocked repository + // Add other methods if needed + } as any; // Casting to any to match the mocked repository + + // Mock the return value of AppDataSource.getRepository (AppDataSource.getRepository as jest.Mock).mockReturnValue(mockRepository); + + // Initialize the BlogService after setting up the mock + blogService = new BlogService(); }); afterEach(() => {