diff --git a/README.md b/README.md index 3b3a2d4d..7f93df74 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,8 @@ This is the backend for E-Commerce-Ninjas, written in Node.js with TypeScript. - Verification Email Endpoint - Resend verification Endpoint - Login Endpoint -- Admin change status Endpoint +- Admin Update Status Endpoint +- Admin Update Role Endpoint ## TABLE OF API ENDPOINTS SPECIFICATION AND DESCRIPTION @@ -42,8 +43,9 @@ This is the backend for E-Commerce-Ninjas, written in Node.js with TypeScript. | 3 | GET | /api/auth/verify-email/:token | 200 OK | public | Verifying email | | 4 | POST | /api/auth/send-verify-email | 200 OK | public | Resend verification email | | 5 | POST | /api/auth/login | 200 OK | public | Login with Email and Password | -| 6 | PUT | /api/users/admin-update-user-status/:id | 200 OK | private | Admin change status | - +| 5 | PUT | /api/users/admin-update-role/:id | 200 OK | private | Update the user role by admin| +| 6 | PUT | /api/users/admin-update-user-status/:id | 200 OK | private | Admin Update Status Endpoint | +| 7 | PUT | /api/users/admin-update-role/:id | 200 OK | private | Admin Update Role Endpoint | ## INSTALLATION diff --git a/package.json b/package.json index f7e34f47..52f57a2a 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "start": "ts-node-dev src/index.ts", "dev": "ts-node-dev src/index.ts", "test": "cross-env NODE_ENV=test npm run deleteAllTables && cross-env NODE_ENV=test npm run createAllTables && cross-env NODE_ENV=test npm run createAllSeeders && nyc cross-env NODE_ENV=test mocha --require ts-node/register 'src/**/*.spec.ts' --timeout 600000 --exit", - "test-dev": "cross-env NODE_ENV=development npm run deleteAllTables && cross-env NODE_ENV=development npm run createAllTables && cross-env NODE_ENV=development npm run createAllSeeders && nyc cross-env NODE_ENV=development mocha --require ts-node/register 'src/**/*.spec.ts' --timeout 600000 --exit", "coveralls": "nyc --reporter=lcov --reporter=text-lcov npm test | coveralls", "coverage": "cross-env NODE_ENV=test nyc mocha --require ts-node/register 'src/**/*.spec.ts' --timeout 600000 --exit", "lint": "eslint . --ext .ts", diff --git a/public/BUILD.txt b/public/BUILD.txt deleted file mode 100644 index 0099cd3b..00000000 --- a/public/BUILD.txt +++ /dev/null @@ -1,11 +0,0 @@ -Believe: Know that the problem you have identified has a solution, and trust that you and your team can build something to address it. - -Understand: Take the time to learn about the experience of the people affected by this problem / who will be using your solution – what does their journey look like? What are their pain points? What factors are affecting how they operate in / engage with the world? - -Invent: Create a Minimum Viable Product or prototype to test out an idea that you have! Be sure to keep in mind how your “invention” will meet your users’ needs. - -Listen: Get feedback from your users on your MVP / prototype and modify it as needed. - -Deliver: Continue to deliver refined versions of your MVP / prototype and improve it over time! - -Password@123 \ No newline at end of file diff --git a/public/ProjectManagement.jpg b/public/ProjectManagement.jpg deleted file mode 100644 index 1f26cd38..00000000 Binary files a/public/ProjectManagement.jpg and /dev/null differ diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/middlewares/validation.ts b/src/middlewares/validation.ts index dfc0115c..09e222f4 100644 --- a/src/middlewares/validation.ts +++ b/src/middlewares/validation.ts @@ -99,4 +99,7 @@ const verifyUserCredentials = async (req: Request, res: Response, next: NextFunc -export { validation, isUserExist, isAccountVerified, verifyUserCredentials }; \ No newline at end of file + + + +export { validation, isUserExist, isAccountVerified,verifyUserCredentials }; \ No newline at end of file diff --git a/src/modules/auth/controller/authControllers.ts b/src/modules/auth/controller/authControllers.ts index 4b6dde4e..772ca29f 100644 --- a/src/modules/auth/controller/authControllers.ts +++ b/src/modules/auth/controller/authControllers.ts @@ -35,7 +35,7 @@ const sendVerifyEmail = async (req: any, res: Response) => { const verifyEmail = async (req: any, res: Response) => { try { await authRepositories.destroySession(req.user.id, req.session.token) - await authRepositories.UpdateUserByAttributes("isVerified", true, "id", req.user.id); + await authRepositories.updateUserByAttributes("isVerified", true, "id", req.user.id); res.status(httpStatus.OK).json({ status: httpStatus.OK, message: "Account verified successfully, now login." }); } catch (error) { return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ status: httpStatus.INTERNAL_SERVER_ERROR, message: error.message }); diff --git a/src/modules/auth/repository/authRepositories.ts b/src/modules/auth/repository/authRepositories.ts index afed5226..4eb75488 100644 --- a/src/modules/auth/repository/authRepositories.ts +++ b/src/modules/auth/repository/authRepositories.ts @@ -11,7 +11,7 @@ const findUserByAttributes = async (key:string, value:any) =>{ return await Users.findOne({ where: { [key]: value} }) } -const UpdateUserByAttributes = async (updatedKey:string, updatedValue:any, whereKey:string, whereValue:any) =>{ +const updateUserByAttributes = async (updatedKey:string, updatedValue:any, whereKey:string, whereValue:any) =>{ await Users.update({ [updatedKey]: updatedValue }, { where: { [whereKey]: whereValue} }); return await findUserByAttributes(whereKey, whereValue) } @@ -28,4 +28,4 @@ const destroySession = async (userId: number, token:string) =>{ return await Session.destroy({ where: {userId, token } }); } -export default { createUser, createSession, findUserByAttributes, destroySession, UpdateUserByAttributes, findSessionByUserId } \ No newline at end of file +export default { createUser, createSession, findUserByAttributes, destroySession, updateUserByAttributes, findSessionByUserId } \ No newline at end of file diff --git a/src/modules/user/controller/userControllers.ts b/src/modules/user/controller/userControllers.ts index 9e994339..40f1e77c 100644 --- a/src/modules/user/controller/userControllers.ts +++ b/src/modules/user/controller/userControllers.ts @@ -1,14 +1,35 @@ +// user Controllers import { Request, Response } from "express"; -import httpStatus from "http-status"; + import authRepositories from "../../auth/repository/authRepositories"; +import httpStatus from "http-status"; + +const updateUserRole = async (req: Request, res: Response) => { + try { + const data = await authRepositories.updateUserByAttributes("role", req.body.role, "id", req.params.id) + return res.status(httpStatus.OK).json({ + message: "User role updated successfully", + data + }); + } catch (error) { + return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ + status: httpStatus.INTERNAL_SERVER_ERROR, + message: error.message + }); + } +}; + const updateUserStatus = async (req: Request, res: Response): Promise => { try { const userId: number = Number(req.params.id); - const data = await authRepositories.UpdateUserByAttributes("status", req.body.status, "id", userId); + const data = await authRepositories.updateUserByAttributes("status", req.body.status, "id", userId); res.status(httpStatus.OK).json({ message: "Status updated successfully.", data }); } catch (error) { res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ status: httpStatus.INTERNAL_SERVER_ERROR, message: error.message }); } }; -export default { updateUserStatus }; + + + +export default { updateUserStatus,updateUserRole }; \ No newline at end of file diff --git a/src/modules/user/test/user.spec.ts b/src/modules/user/test/user.spec.ts index 26014c60..34823c9a 100644 --- a/src/modules/user/test/user.spec.ts +++ b/src/modules/user/test/user.spec.ts @@ -17,15 +17,15 @@ describe("Update User Status test case ", () => { let getUserStub: sinon.SinonStub; let updateUserStub: sinon.SinonStub; const testUserId = 1; - let userId: number= null; - const unknownId = 100; + let userId: number = null; + const unknownId = 100; it("should register a new user", (done) => { router() .post("/api/auth/register") .send({ - email: "niyofo8179@acuxi.com", + email: "nda1234@gmail.com", password: "userPassword@123" }) .end((error, response) => { @@ -37,97 +37,184 @@ describe("Update User Status test case ", () => { done(error); }); }); - it("should update the user status successfully", (done) => { - router() - .put(`/api/users/admin-update-user-status/${userId}`) - .send({ status: "disabled" }) - .end((err, res) => { - expect(res).to.have.status(200); - expect(res.body).to.be.an("object"); - expect(res.body).to.have.property("message", "Status updated successfully."); - done(err); - }); + it("should update the user status successfully", (done) => { + router() + .put(`/api/users/admin-update-user-status/${userId}`) + .send({ status: "disabled" }) + .end((err, res) => { + expect(res).to.have.status(200); + expect(res.body).to.be.an("object"); + expect(res.body).to.have.property("message", "Status updated successfully."); + done(err); }); + }); - it("should handle invalid user status", (done) => { - router() - .put(`/api/users/admin-update-user-status/${testUserId}`) - .send({ status: "disableddd" }) - .end((err, res) => { - expect(res).to.have.status(400); - expect(res.body).to.be.an("object"); - expect(res.body).to.have.property("message", "Status must be either 'enabled' or 'disabled'"); - done(err); - }); + it("should handle invalid user status", (done) => { + router() + .put(`/api/users/admin-update-user-status/${testUserId}`) + .send({ status: "disableddd" }) + .end((err, res) => { + expect(res).to.have.status(400); + expect(res.body).to.be.an("object"); + expect(res.body).to.have.property("message", "Status must be either 'enabled' or 'disabled'"); + done(err); }); + }); - it("should return 404 if user doesn't exist", (done) => { - router() - .put(`/api/users/admin-update-user-status/${unknownId}`) - .send({ status: "disabled" }) - .end((err, res) => { - expect(res).to.have.status(httpStatus.NOT_FOUND); - expect(res.body).to.be.an("object"); - expect(res.body).to.have.property("status",httpStatus.NOT_FOUND); - expect(res.body).to.have.property("message", "User not found"); - done(err); - }); + it("should return 404 if user doesn't exist", (done) => { + router() + .put(`/api/users/admin-update-user-status/${unknownId}`) + .send({ status: "disabled" }) + .end((err, res) => { + expect(res).to.have.status(httpStatus.NOT_FOUND); + expect(res.body).to.be.an("object"); + expect(res.body).to.have.property("status", httpStatus.NOT_FOUND); + expect(res.body).to.have.property("message", "User not found"); + done(err); }); + }); }); describe("User Repository Functions", () => { - let findOneStub: sinon.SinonStub; - let updateStub: sinon.SinonStub; - - beforeEach(() => { - findOneStub = sinon.stub(Users, "findOne"); - updateStub = sinon.stub(Users, "update"); - }); - - afterEach(async () => { - sinon.restore(); - await Users.destroy({ where: {} }); + let findOneStub: sinon.SinonStub; + let updateStub: sinon.SinonStub; + + beforeEach(() => { + findOneStub = sinon.stub(Users, "findOne"); + updateStub = sinon.stub(Users, "update"); + }); + + afterEach(async () => { + sinon.restore(); + }); + + describe("getSingleUserById", () => { + it("should return a user if found", async () => { + const user = { id: 1, status: true }; + findOneStub.resolves(user); + const result = await authRepositories.findUserByAttributes("id", 1); + expect(findOneStub.calledOnce).to.be.true; + expect(findOneStub.calledWith({ where: { id: 1 } })).to.be.true; + expect(result).to.equal(user); }); - - describe("getSingleUserById", () => { - it("should return a user if found", async () => { - const user = { id: 1, status: true }; - findOneStub.resolves(user); - const result = await authRepositories.findUserByAttributes("id",1); + + it("should throw an error if there is a database error", async () => { + findOneStub.rejects(new Error("Database error")); + try { + await authRepositories.findUserByAttributes("id", 1); + } catch (error) { expect(findOneStub.calledOnce).to.be.true; - expect(findOneStub.calledWith({ where: { id: 1 } })).to.be.true; - expect(result).to.equal(user); - }); - - it("should throw an error if there is a database error", async () => { - findOneStub.rejects(new Error("Database error")); - try { - await authRepositories.findUserByAttributes("id",1); - } catch (error) { - expect(findOneStub.calledOnce).to.be.true; - expect(error.message).to.equal("Database error"); - } - }); + expect(error.message).to.equal("Database error"); + } }); - - describe("updateUserStatus", () => { - it("should update the user status successfully", async () => { - updateStub.resolves([1]); - const user = { id: 1, status: true }; - const result = await authRepositories.UpdateUserByAttributes("status", "enabled", "id", 1); + }); + + describe("updateUserStatus", () => { + it("should update the user status successfully", async () => { + updateStub.resolves([1]); + const user = { id: 1, status: true }; + const result = await authRepositories.updateUserByAttributes("status", "enabled", "id", 1); + expect(updateStub.calledOnce).to.be.true; + expect(updateStub.calledWith({ status: true }, { where: { id: 1 } })).to.be.false; + }); + + it("should throw an error if there is a database error", async () => { + updateStub.rejects(new Error("Database error")); + try { + await authRepositories.updateUserByAttributes("status", "enabled", "id", 1); + } catch (error) { expect(updateStub.calledOnce).to.be.true; - expect(updateStub.calledWith({ status: true }, { where: { id: 1 } })).to.be.false; + expect(error.message).to.equal("Database error"); + } + }); + }); +}); + + + +describe("Admin update User roles", () => { + + before(async () => { + await Users.destroy({ + where: {} + }) + }) + after(async () => { + await Users.destroy({ + where: {} + }) + }) + let userIdd: number = null; + + + it("should register a new user", (done) => { + router() + .post("/api/auth/register") + .send({ + email: "nda1234@gmail.com", + password: "userPassword@123" + }) + .end((error, response) => { + expect(response.status).to.equal(httpStatus.CREATED); + expect(response.body).to.be.an("object"); + expect(response.body).to.have.property("data"); + userIdd = response.body.data.user.id; + expect(response.body).to.have.property("message", "Account created successfully. Please check email to verify account."); + done(error); }); - - it("should throw an error if there is a database error", async () => { - updateStub.rejects(new Error("Database error")); - try { - await authRepositories.UpdateUserByAttributes("status", "enabled", "id", 1); - } catch (error) { - expect(updateStub.calledOnce).to.be.true; - expect(error.message).to.equal("Database error"); - } + }); + + it("Should notify if no role is specified", async () => { + + const response = await router() + .put(`/api/users/admin-update-role/${userIdd}`); + + expect(response.status).to.equal(httpStatus.BAD_REQUEST); + expect(response.body).to.have.property("message"); + }); + + it("Should notify if the role is other than ['Admin', 'Buyer', 'Seller']", async () => { + + const response = await router() + .put(`/api/users/admin-update-role/${userIdd}`) + .send({ role: "Hello" }); + + expect(response.status).to.equal(httpStatus.BAD_REQUEST); + expect(response.body).to.have.property("message", "Only Admin, Buyer and Seller are allowed."); + }); + + it("Should return error when invalid Id is passed", async () => { + const response = await router() + .put("/api/users/admin-update-role/invalid-id") + .send({ role: "Admin" }); + + expect(response.status).to.equal(httpStatus.INTERNAL_SERVER_ERROR); + expect(response).to.have.property("status", httpStatus.INTERNAL_SERVER_ERROR); + }); + + + it("Should update User and return updated user", (done) => { + router() + .put(`/api/users/admin-update-role/${userIdd}`) + .send({ role: "Admin" }) + .end((err, res) => { + expect(res).to.have.status(httpStatus.OK); + expect(res.body).to.be.an("object"); + expect(res.body).to.have.property("message", "User role updated successfully"); + done(err); }); - }); }); + + + it("Should return 404 if user is not found", (done) => { + router().put("/api/users/admin-update-role/10001").send({ role: "Admin" }).end((err, res) => { + expect(res).to.have.status(httpStatus.NOT_FOUND); + expect(res.body).to.be.an("object"); + expect(res.body).to.have.property("message", "User not found") + done(err) + }) + }) + + +}); \ No newline at end of file diff --git a/src/modules/user/validation/userValidations.ts b/src/modules/user/validation/userValidations.ts index 4b775e53..106ca645 100644 --- a/src/modules/user/validation/userValidations.ts +++ b/src/modules/user/validation/userValidations.ts @@ -7,3 +7,12 @@ export const statusSchema = Joi.object({ "any.required": "Status is required" }) }); + + +export const roleSchema = Joi.object({ + role: Joi.string().valid("Admin", "Buyer", "Seller").required().messages({ + "any.required": "The 'role' parameter is required.", + "string.base": "The 'role' parameter must be a string.", + "any.only": "Only Admin, Buyer and Seller are allowed." + }) +}); diff --git a/src/routes/index.ts b/src/routes/index.ts index 2a71fef1..197898e7 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,11 +1,12 @@ import { Router } from "express" import authRouter from "./authRouter" -import userRouter from "./userRouter" +import userRouter from "./userRouter"; + const router: Router = Router() router.use("/auth", authRouter); - router.use("/users", userRouter); + export default router; diff --git a/src/routes/userRouter.ts b/src/routes/userRouter.ts index 6bfe550f..ec66fdda 100644 --- a/src/routes/userRouter.ts +++ b/src/routes/userRouter.ts @@ -1,11 +1,12 @@ import { Router } from "express"; import userControllers from "../modules/user/controller/userControllers"; import {isUserExist, validation} from "../middlewares/validation"; -import { statusSchema } from "../modules/user/validation/userValidations"; +import { statusSchema,roleSchema } from "../modules/user/validation/userValidations"; const router: Router = Router() router.put("/admin-update-user-status/:id", validation(statusSchema), isUserExist, userControllers.updateUserStatus); +router.put("/admin-update-role/:id",validation(roleSchema),isUserExist, userControllers.updateUserRole); -export default router; +export default router; \ No newline at end of file diff --git a/swagger.json b/swagger.json index 816b55ab..bf6be133 100644 --- a/swagger.json +++ b/swagger.json @@ -3,7 +3,7 @@ "info": { "version": "1.0.0", "title": "E-commerce-ninjas", - "description": "APIs for the E-commmerce-ninjas Project", + "description": "APIs for the E-commerce-ninjas Project", "termsOfService": "https://github.com/atlp-rwanda/e-commerce-ninjas-bn/blob/develop/README.md", "contact": { "email": "e-commerce-ninjas@andela.com" @@ -26,7 +26,7 @@ }, { "name": "Admin User Routes", - "description": "Change User Status Endpoint | PUT Route" + "description": "" } ], "schemes": [ @@ -34,12 +34,10 @@ "https" ], "consumes": [ - "application/json", - "none" + "application/json" ], "produces": [ - "application/json", - "none" + "application/json" ], "paths": { "/": { @@ -429,6 +427,125 @@ } } } + }, + + "/api/users/admin-update-role/{id}": { + "put": { + "tags": [ + "Admin User Routes" + ], + "summary": "Update user role", + "description": "This endpoint allows admin to update a user's role", + "parameters": [ + { + "in":"path", + "name": "id", + "type": "string", + "description":"Pass the ID" + } + ], + "requestBody": { + "description": "User role update details", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": [ + "Admin", + "Buyer", + "Seller" + ] + } + }, + "required": [ + "role" + ] + } + } + } + }, + "responses": { + "200": { + "description": "User role updated successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + } + } + } + } + } + }, + "400": { + "description": "Invalid input", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string" + } + } + } + } + } + }, + "404": { + "description": "User not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string" + } + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string" + } + } + } + } + } + } + } + } } }, "components": {