diff --git a/README.md b/README.md index b0104e37..689f669b 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Our e-commerce web application server, developed by Team Ninjas, facilitates smo - Admin Update Status Endpoint - Admin Update Role Endpoint - Logout Endpoint +- Update User Profile Endpoint ## TABLE OF API ENDPOINTS SPECIFICATION AND DESCRIPTION @@ -45,6 +46,7 @@ Our e-commerce web application server, developed by Team Ninjas, facilitates smo | 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 | | 8 | POST | /api/auth/logout | 200 OK | private | Logout user | +| 9 | PUT | /api/users/user-update-profile/:id | 200 OK | private | Update User Profile Endpoint | ## INSTALLATION @@ -121,4 +123,4 @@ Our e-commerce web application server, developed by Team Ninjas, facilitates smo 9. Delete the Migration: ```sh npm run deleteAllTables - ``` + ``` \ No newline at end of file diff --git a/src/__test__/BUILD.txt b/src/__test__/BUILD.txt new file mode 100644 index 00000000..0099cd3b --- /dev/null +++ b/src/__test__/BUILD.txt @@ -0,0 +1,11 @@ +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/src/__test__/testImage.jpg b/src/__test__/testImage.jpg new file mode 100644 index 00000000..858b5e4e Binary files /dev/null and b/src/__test__/testImage.jpg differ diff --git a/src/helpers/multer.ts b/src/helpers/multer.ts index ed2a8ca4..826fa6a5 100644 --- a/src/helpers/multer.ts +++ b/src/helpers/multer.ts @@ -1,14 +1,14 @@ -// import { Request } from "express"; -// import multer from "multer"; -// import path from "path"; +import { Request } from "express"; +import multer from "multer"; +import path from "path"; -// export default multer({ -// storage:multer.diskStorage({}), -// fileFilter:(req:Request,file:Express.Multer.File,cb)=>{ -// const ext = path.extname(file.originalname); -// if(ext!== ".png" && ext!== ".jpg" && ext!== ".jpeg"){ -// return cb(new Error("Only images are allowed")); -// } -// cb(null,true); -// } -// }) \ No newline at end of file +export default multer({ + storage:multer.diskStorage({}), + fileFilter:(req:Request,file:Express.Multer.File,cb)=>{ + const ext = path.extname(file.originalname); + if(ext!== ".png" && ext!== ".jpg" && ext!== ".jpeg"){ + return cb(new Error("Only images are allowed")); + } + cb(null,true); + } +}) \ No newline at end of file diff --git a/src/helpers/uploadImage.ts b/src/helpers/uploadImage.ts index 2a8872dc..067d619d 100644 --- a/src/helpers/uploadImage.ts +++ b/src/helpers/uploadImage.ts @@ -1,18 +1,18 @@ -// /* eslint-disable @typescript-eslint/no-explicit-any */ -// import { v2 as cloudinary } from "cloudinary"; +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { v2 as cloudinary } from "cloudinary"; -// cloudinary.config({ -// cloud_name: process.env.CLOUD_NAME, -// api_key: process.env.API_KEY, -// api_secret: process.env.API_SECRET -// }); +cloudinary.config({ + cloud_name: process.env.CLOUD_NAME, + api_key: process.env.API_KEY, + api_secret: process.env.API_SECRET + }); -// export const uploadImages = async (fileToUpload: any): Promise<{ public_id: string; secure_url: string }> => { -// const result = await cloudinary.uploader.upload(fileToUpload.path); -// return { -// public_id: result.public_id, -// secure_url: result.secure_url -// }; -// }; +export const uploadImages = async (fileToUpload: any): Promise<{ public_id: string; secure_url: string }> => { + const result = await cloudinary.uploader.upload(fileToUpload.path); + return { + public_id: result.public_id, + secure_url: result.secure_url + }; + }; -// export default uploadImages; \ No newline at end of file +export default uploadImages; diff --git a/src/modules/auth/test/auth.spec.ts b/src/modules/auth/test/auth.spec.ts index 2a820019..b055d6c9 100644 --- a/src/modules/auth/test/auth.spec.ts +++ b/src/modules/auth/test/auth.spec.ts @@ -1,4 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable comma-dangle */ +/* eslint quotes: "off" */ import { Request, Response } from "express"; import chai, { expect } from "chai"; import chaiHttp from "chai-http"; diff --git a/src/modules/user/controller/userControllers.ts b/src/modules/user/controller/userControllers.ts index 40f1e77c..78e65b94 100644 --- a/src/modules/user/controller/userControllers.ts +++ b/src/modules/user/controller/userControllers.ts @@ -1,6 +1,7 @@ // user Controllers import { Request, Response } from "express"; - +import uploadImages from "../../../helpers/uploadImage"; +import userRepositories from "../repository/userRepositories"; import authRepositories from "../../auth/repository/authRepositories"; import httpStatus from "http-status"; @@ -30,6 +31,26 @@ const updateUserStatus = async (req: Request, res: Response): Promise => { } }; +const getUserDetails = async(req:Request,res:Response)=>{ + try { + const Users = await authRepositories.findUserByAttributes("id", req.params.id); + res.status(httpStatus.OK).json({status: httpStatus.OK,Users}); + } catch (error) { + res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ status: httpStatus.INTERNAL_SERVER_ERROR, message: error.message }) + } +} + +const updateUserProfile = async (req: Request, res: Response) => { + try { + const upload = await uploadImages(req.file); + const userData = { ...req.body, profilePicture:upload.secure_url }; + const updatedUser = await userRepositories.updateUserProfile(userData, Number(req.params.id)); + res.status(httpStatus.OK).json({status:httpStatus.OK, data:updatedUser}); + } catch (error) { + res.status(httpStatus.INTERNAL_SERVER_ERROR).json({status:httpStatus.INTERNAL_SERVER_ERROR, error: error.message}); + } +} + -export default { updateUserStatus,updateUserRole }; \ No newline at end of file +export default { updateUserStatus,updateUserRole,getUserDetails ,updateUserProfile}; \ No newline at end of file diff --git a/src/modules/user/repository/userRepositories.ts b/src/modules/user/repository/userRepositories.ts index 8b137891..661514f2 100644 --- a/src/modules/user/repository/userRepositories.ts +++ b/src/modules/user/repository/userRepositories.ts @@ -1 +1,9 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import Users from "../../../databases/models/users"; +const updateUserProfile = async (user: any, id:number) => { + await Users.update({...user},{where:{id},returning:true}) +const updateUser = await Users.findOne({where:{id}}) + return updateUser; +} +export default {updateUserProfile}; diff --git a/src/modules/user/test/user.spec.ts b/src/modules/user/test/user.spec.ts index 34823c9a..305953da 100644 --- a/src/modules/user/test/user.spec.ts +++ b/src/modules/user/test/user.spec.ts @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable comma-dangle */ +/* eslint quotes: "off" */ import chai, { expect } from "chai"; import chaiHttp from "chai-http"; import sinon, { SinonStub } from "sinon"; @@ -8,6 +10,12 @@ import app from "../../../index"; import Users from "../../../databases/models/users"; import authRepositories from "../../auth/repository/authRepositories" import { UsersAttributes } from "../../../databases/models/users"; +import path from "path"; +import fs from 'fs' +import uploadImages from "../../../helpers/uploadImage"; +import { v2 as cloudinary } from "cloudinary"; +const imagePath = path.join(__dirname, '../../../__test__/testImage.jpg'); +const imageBuffer = fs.readFileSync(imagePath) chai.use(chaiHttp); @@ -139,11 +147,11 @@ describe("Admin update User roles", () => { where: {} }) }) - after(async () => { - await Users.destroy({ - where: {} - }) - }) + // after(async () => { + // await Users.destroy({ + // where: {} + // }) + // }) let userIdd: number = null; @@ -217,4 +225,141 @@ describe("Admin update User roles", () => { }) -}); \ No newline at end of file +}); + +describe("updateUserProfile", () => { + let profileId :number = null; +let token + +it("should register a new user", (done) => { +router() + .post("/api/auth/register") + .send({ + email: "salt23@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"); + profileId = 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 be able to login a registered user", (done) => { + router() + .post("/api/auth/login") + .send({ + email: "salt23@gmail.com", + password: "userPassword@123" + }) + .end((error, response) => { + expect(response.status).to.equal(httpStatus.OK); + expect(response.body).to.be.a("object"); + expect(response.body).to.have.property("data"); + expect(response.body.message).to.be.a("string"); + expect(response.body.data).to.have.property("token"); + token = response.body.data.token; + done(error); + }); +}); + + +it("Should be able to get", (done) => { +router() + .get(`/api/users/user-get-profile/${profileId}`) + .end((error, response) => { + expect(response).to.have.status(200); + expect(response.body).to.be.a("object"); + done(error); + }); +}); + +it("should update profile ", (done) => { + router().put(`/api/users/user-update-profile/${profileId}`) + .set("Authorization", `Bearer ${token}`) + .field('firstName', 'MANISHIMWE') + .field('lastName', 'Salton Joseph') + .field('phone', '787312593') + .field('gender', 'male') + .field('birthDate', '1943-02-04') + .field('language', 'english') + .field('currency', 'USD') + .attach("profilePicture",imageBuffer,'testImage.jpg') + .end((error, response) => { + + expect(response.status).to.equal(200); + done(error); + }); +}); +it("should return error when user id is invalid", (done) => { +router().put("/api/users/user-update-profile/-1") +.set("Authorization", `Bearer ${token}`) +.send({ + "firstName": "MANISHIMWE", + "lastName": "Salton Joseph", + "phone": "787312593", + "gender": "male", + "birthDate": "1943-02-04T00:00:00.000Z", + "language": "english", + "currency": "USD" +}).end((error, response) => { + + expect(response.status).to.equal(500); + done(error); +}); +}); + + +describe('uploadImages', () => { + let uploadStub: sinon.SinonStub; + + beforeEach(() => { + uploadStub = sinon.stub(cloudinary.uploader, 'upload'); + }); + + afterEach(() => { + uploadStub.restore(); + }); + + it('should upload an image and return the public_id and secure_url', async () => { + const fileToUpload = { path: 'path/to/file.jpg' }; + const mockResult = { + public_id: 'mock_public_id', + secure_url: 'https://mock_secure_url.com', + }; + + uploadStub.resolves(mockResult); + + const result = await uploadImages(fileToUpload); + + expect(uploadStub.calledOnceWith(fileToUpload.path)).to.be.true; + expect(result).to.deep.equal(mockResult); + }); + + it('should handle errors from the upload process', async () => { + const fileToUpload = { path: 'path/to/file.jpg' }; + const mockError = new Error('Upload failed'); + + uploadStub.rejects(mockError); + + try { + await uploadImages(fileToUpload); + expect.fail('Expected error was not thrown'); + } catch (error) { + expect(error).to.be.an('error'); + expect(error.message).to.equal('Upload failed'); + } + }); + + + after(async () => { + await Users.destroy({ + where: {} + }) + }); + +}); +}) \ No newline at end of file diff --git a/src/modules/user/validation/userValidations.ts b/src/modules/user/validation/userValidations.ts index 106ca645..3da2fac8 100644 --- a/src/modules/user/validation/userValidations.ts +++ b/src/modules/user/validation/userValidations.ts @@ -1,4 +1,16 @@ import Joi from "joi"; +interface User { + firstName: string; + lastName: string; + email: string; + phone: number; + profilePicture?: string; + gender: "male" | "female" | "other"; + birthDate: string; + language: string; + currency: string; + role: "buyer" | "seller" | "admin"; +} export const statusSchema = Joi.object({ status: Joi.string().valid("enabled", "disabled").required().messages({ @@ -16,3 +28,49 @@ export const roleSchema = Joi.object({ "any.only": "Only Admin, Buyer and Seller are allowed." }) }); +const userSchema = Joi.object({ + firstName: Joi.string().messages({ + "string.base": "firstName should be a type of text", + "string.empty": "firstName cannot be an empty field", + "any.required": "firstName is required" + }), + lastName: Joi.string().messages({ + "string.base": "lastName should be a type of text", + "string.empty": "lastName cannot be an empty field", + "any.required": "lastName is required" + }), + phone: Joi.number().messages({ + "number.base": "phone number should be a type of number", + "any.required": "phone number is required" + }), + profilePicture: Joi.string().uri().optional().messages({ + "string.base": "profilePicture should be a type of text", + "string.uri": "profilePicture must be a valid URI" + }), + gender: Joi.string().valid("male", "female", "other").messages({ + "string.base": "gender should be a type of text", + "any.only": "gender must be one of [male, female, other]", + "any.required": "gender is required" + }), + birthDate: Joi.date().iso().messages({ + "date.base": "birthDate should be a valid date", + "date.iso": "birthDate must be in ISO format", + "any.required": "birthDate is required" + }), + language: Joi.string().messages({ + "string.base": "language should be a type of text", + "string.empty": "language cannot be an empty field", + "any.required": "language is required" + }), + currency: Joi.string().messages({ + "string.base": "currency should be a type of text", + "string.empty": "currency cannot be an empty field", + "any.required": "currency is required" + }), + role: Joi.string().valid("buyer", "seller", "admin").messages({ + "string.base": "role should be a type of text", + "any.only": "role must be one of [buyer, seller, admin]", + "any.required": "role is required" + }) +}); +export {userSchema}; diff --git a/src/routes/userRouter.ts b/src/routes/userRouter.ts index ec66fdda..d22c1d30 100644 --- a/src/routes/userRouter.ts +++ b/src/routes/userRouter.ts @@ -1,12 +1,16 @@ import { Router } from "express"; import userControllers from "../modules/user/controller/userControllers"; import {isUserExist, validation} from "../middlewares/validation"; -import { statusSchema,roleSchema } from "../modules/user/validation/userValidations"; - +import { statusSchema,roleSchema , userSchema} from "../modules/user/validation/userValidations"; +import upload from "../helpers/multer"; +import { userAuthorization } from "../middlewares/authorization"; 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); +router.get("/user-get-profile/:id",userControllers.getUserDetails) +router.put("/user-update-profile/:id",userAuthorization(["buyer", "seller", "admin"]),upload.single("profilePicture"),validation(userSchema),userControllers.updateUserProfile) + export default router; \ No newline at end of file diff --git a/swagger.json b/swagger.json index 6c9dbd59..9ccd4193 100644 --- a/swagger.json +++ b/swagger.json @@ -12,8 +12,8 @@ "name": "ISC" } }, - "host-live": "localhost:5000", - "host": "https://e-commerce-ninjas-bn.onrender.com", + "host": "localhost:5000", + "host-live": "https://e-commerce-ninjas-bn.onrender.com", "basePath": "/api", "tags": [ { @@ -27,6 +27,10 @@ { "name": "Admin User Routes", "description": "" + }, + { + "name": "Update Profile Route", + "description": " Endpoint PUT Route" } ], "schemes": ["http", "https"], @@ -560,6 +564,76 @@ } } } + }, + "/api/users/user-update-profile/{id}":{ + "put":{ + "tags": ["Update Profile Route"], + "summary": "Update a UserProfile", + "description": "By the use of initial/wrong GET endpoint, you will be able to see welcome message (Welcome to E-Commerce-Ninja-BackEnd)", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. (Welcome to E-Commerce-Ninja-BackEnd.)" + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "description": "UserProfile ID", + "required": true + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "firstName": { "type": "string", + "example": "MANISHIMWE" }, + "lastName": { "type": "string", + "example": "Salton" }, + "phone": { + "type": "number", + "example": "0787312593" + }, + "profilePicture":{ "type": "string" , + "example": "https://www.pexels.com/photo/bicycle-under-building-wall-with-painted-pixel-eyes-15997337/"}, + "gender" :{ "type": "string" , + "example": "male"}, + "birthDate":{ "type": "number", + "example": "2-2-2024" + }, + "language" :{ "type": "string", + "example": "english" }, + "currency" :{ "type": "string", + "example": "USD" } + } + } + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { "type": "string" }, + "message": { "type": "string" } + } + } + } + } + } } }, "components": {