Skip to content

Commit

Permalink
Logout feature (#26)
Browse files Browse the repository at this point in the history
* [#187584914]added logout feature

* [starts #187584914] added logout feature

* [finishes#187584914] logout feature

* [delivers##187584914] updated readme & swagger.json

* [delivers##187584914] updated readme & swagger.json

* [deliveres #187584914] logout features completed

* [deliveres #187584914] logout features completed

* [delivers #187584914] finished logout feature
  • Loading branch information
solangeihirwe03 authored and MANISHIMWESalton committed Jun 1, 2024
1 parent 869e4e0 commit 589f5f0
Show file tree
Hide file tree
Showing 11 changed files with 354 additions and 37 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -121,4 +123,4 @@ Our e-commerce web application server, developed by Team Ninjas, facilitates smo
9. Delete the Migration:
```sh
npm run deleteAllTables
```
```
11 changes: 11 additions & 0 deletions src/__test__/BUILD.txt
Original file line number Diff line number Diff line change
@@ -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
Binary file added src/__test__/testImage.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 13 additions & 13 deletions src/helpers/multer.ts
Original file line number Diff line number Diff line change
@@ -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);
// }
// })
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);
}
})
30 changes: 15 additions & 15 deletions src/helpers/uploadImage.ts
Original file line number Diff line number Diff line change
@@ -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;
export default uploadImages;
21 changes: 20 additions & 1 deletion src/modules/user/controller/userControllers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Request, Response } from "express";
import httpStatus from "http-status";
import uploadImages from "../../../helpers/uploadImage";
import userRepositories from "../repository/userRepositories";
import authRepositories from "../../auth/repository/authRepositories";

Expand Down Expand Up @@ -58,7 +59,25 @@ const updateUserStatus = async (req: Request, res: Response): Promise<void> => {
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ status: httpStatus.INTERNAL_SERVER_ERROR, message: error.message });
}
};
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, adminGetUsers , adminGetUser };
export default { updateUserStatus, updateUserRole, adminGetUsers , adminGetUser,updateUserProfile ,getUserDetails};
9 changes: 7 additions & 2 deletions src/modules/user/repository/userRepositories.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import Users from "../../../databases/models/users";


Expand All @@ -8,5 +9,9 @@ const getAllUsers = async () => {
const getUserById = async (id: number) => {
return Users.findByPk(id);
}

export default { getAllUsers, getUserById}
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 { getAllUsers, getUserById,updateUserProfile}
146 changes: 145 additions & 1 deletion src/modules/user/test/user.spec.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -9,6 +11,12 @@ import Users from "../../../databases/models/users";
import authRepositories from "../../auth/repository/authRepositories"
import { isUsersExist } from "../../../middlewares/validation";
import { Op } from "sequelize";
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);
Expand Down Expand Up @@ -353,4 +361,140 @@ describe("Admin Controllers", () => {
done(error)
});
});
});
});
describe("updateUserProfile", () => {
let profileId :number = null;
let token

it("should register a new user", (done) => {
router()
.post("/api/auth/register")
.send({
email: "[email protected]",
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: "[email protected]",
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/user/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/user/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/user/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: {}
})
});

});
})
58 changes: 58 additions & 0 deletions src/modules/user/validation/userValidations.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -16,3 +28,49 @@ export const roleSchema = Joi.object({
"any.only": "Only Admin, Buyer and Seller are allowed."
})
});
const userSchema = Joi.object<User>({
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};
Loading

0 comments on commit 589f5f0

Please sign in to comment.