From 924962b224973c65c86821e934523a04404d4f67 Mon Sep 17 00:00:00 2001 From: solangeihirwe03 Date: Fri, 24 May 2024 16:13:37 +0200 Subject: [PATCH 1/8] [#187584914]added logout feature --- src/databases/models/tokens.ts | 69 +++++++++++++++++++ .../auth/controller/authControllers.ts | 15 ++++ .../auth/repository/authRepositories.ts | 18 +++++ src/modules/{user => auth}/test/user.spec.ts | 0 .../validation/userValidations.ts | 0 .../user/controller/userControllers.ts | 1 - .../user/repository/userRepositories.ts | 1 - src/routes/index.ts | 9 ++- src/routes/userRoutes.ts | 8 +++ 9 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 src/databases/models/tokens.ts create mode 100644 src/modules/auth/controller/authControllers.ts create mode 100644 src/modules/auth/repository/authRepositories.ts rename src/modules/{user => auth}/test/user.spec.ts (100%) rename src/modules/{user => auth}/validation/userValidations.ts (100%) delete mode 100644 src/modules/user/controller/userControllers.ts delete mode 100644 src/modules/user/repository/userRepositories.ts create mode 100644 src/routes/userRoutes.ts diff --git a/src/databases/models/tokens.ts b/src/databases/models/tokens.ts new file mode 100644 index 00000000..68469b70 --- /dev/null +++ b/src/databases/models/tokens.ts @@ -0,0 +1,69 @@ +import { Model, DataTypes, Sequelize } from "sequelize"; + +interface TokenAttributes { + id: number; + userId: number; + device: string; + accessToken: string; + createdAt: Date; + updatedAt: Date; + expiresAt: Date; +} + +module.exports = (sequelize: Sequelize) => { + class Tokens extends Model implements TokenAttributes { + declare id: number; + declare userId: number; + declare device: string; + declare accessToken: string; + declare createdAt: Date; + declare updatedAt: Date; + declare expiresAt: Date; + } + + Tokens.init( + { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + userId: { + type: new DataTypes.INTEGER(), + allowNull: false, + }, + device: { + type: new DataTypes.STRING(280), + allowNull: false, + }, + accessToken: { + type: new DataTypes.STRING(280), + allowNull: false, + }, + createdAt: { + field: "createdAt", + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + updatedAt: { + field: "updatedAt", + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + expiresAt: { + type: DataTypes.DATE, + allowNull: false, + }, + }, + { + sequelize, + tableName: "tokens", + timestamps: true, + modelName: "Tokens", + } + ); + + return Tokens; +}; diff --git a/src/modules/auth/controller/authControllers.ts b/src/modules/auth/controller/authControllers.ts new file mode 100644 index 00000000..5b58ac0d --- /dev/null +++ b/src/modules/auth/controller/authControllers.ts @@ -0,0 +1,15 @@ +import { invalidateToken } from "../repository/authRepositories"; +import { Request, Response } from "express"; + +export const logoutUser = async (req: Request, res: Response)=>{ + const authHeader = req.headers['authorization']; + if(!authHeader){ + return res.status(401).json({message: "Unauthorized"}); + } + const token = authHeader.split(' ')[1] + const success = await invalidateToken(token); + + if(!success)return res.status(401).json({message: "internal server error!"}); + + res.status(200).json({message: "successfully logged out"}) +} \ No newline at end of file diff --git a/src/modules/auth/repository/authRepositories.ts b/src/modules/auth/repository/authRepositories.ts new file mode 100644 index 00000000..acdcf503 --- /dev/null +++ b/src/modules/auth/repository/authRepositories.ts @@ -0,0 +1,18 @@ +import { Op, where } from "sequelize"; +import db from "../../../databases/models"; + +export const invalidateToken = async(token:string)=>{ + const accessToken = db; + + await accessToken.update({ + isValid: false + }, { + where:{ + token:{ + [Op.eq]:token + } + } + }); + return true; +} + diff --git a/src/modules/user/test/user.spec.ts b/src/modules/auth/test/user.spec.ts similarity index 100% rename from src/modules/user/test/user.spec.ts rename to src/modules/auth/test/user.spec.ts diff --git a/src/modules/user/validation/userValidations.ts b/src/modules/auth/validation/userValidations.ts similarity index 100% rename from src/modules/user/validation/userValidations.ts rename to src/modules/auth/validation/userValidations.ts diff --git a/src/modules/user/controller/userControllers.ts b/src/modules/user/controller/userControllers.ts deleted file mode 100644 index 70972d02..00000000 --- a/src/modules/user/controller/userControllers.ts +++ /dev/null @@ -1 +0,0 @@ -// user Controllers \ No newline at end of file diff --git a/src/modules/user/repository/userRepositories.ts b/src/modules/user/repository/userRepositories.ts deleted file mode 100644 index 1dd76dab..00000000 --- a/src/modules/user/repository/userRepositories.ts +++ /dev/null @@ -1 +0,0 @@ -// user repositories \ No newline at end of file diff --git a/src/routes/index.ts b/src/routes/index.ts index 0532cbff..f81e1ea3 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1 +1,8 @@ -// Use Routes +import { Router } from "express"; +import userRoutes from "./userRoutes"; + +const router = Router(); + +router.use("/user", userRoutes); + +export default router; \ No newline at end of file diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts new file mode 100644 index 00000000..ebc5362b --- /dev/null +++ b/src/routes/userRoutes.ts @@ -0,0 +1,8 @@ +import { logoutUser } from "../modules/auth/controller/authControllers"; +import { Router } from "express"; + +const userRoutes = Router(); + +userRoutes.post("/logout", logoutUser) + +export default userRoutes; \ No newline at end of file From d7d519c3c9c044beaabe169705d6b191934e01f6 Mon Sep 17 00:00:00 2001 From: solangeihirwe03 Date: Fri, 24 May 2024 16:38:15 +0200 Subject: [PATCH 2/8] [starts #187584914] added logout feature --- src/databases/models/tokens.ts | 19 ++++++++++--------- .../auth/controller/authControllers.ts | 4 ++-- .../auth/repository/authRepositories.ts | 2 +- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/databases/models/tokens.ts b/src/databases/models/tokens.ts index 68469b70..653face6 100644 --- a/src/databases/models/tokens.ts +++ b/src/databases/models/tokens.ts @@ -1,3 +1,4 @@ +/* eslint-disable require-jsdoc */ import { Model, DataTypes, Sequelize } from "sequelize"; interface TokenAttributes { @@ -26,42 +27,42 @@ module.exports = (sequelize: Sequelize) => { id: { type: DataTypes.INTEGER, autoIncrement: true, - primaryKey: true, + primaryKey: true }, userId: { type: new DataTypes.INTEGER(), - allowNull: false, + allowNull: false }, device: { type: new DataTypes.STRING(280), - allowNull: false, + allowNull: false }, accessToken: { type: new DataTypes.STRING(280), - allowNull: false, + allowNull: false }, createdAt: { field: "createdAt", type: DataTypes.DATE, allowNull: false, - defaultValue: DataTypes.NOW, + defaultValue: DataTypes.NOW }, updatedAt: { field: "updatedAt", type: DataTypes.DATE, allowNull: false, - defaultValue: DataTypes.NOW, + defaultValue: DataTypes.NOW }, expiresAt: { type: DataTypes.DATE, - allowNull: false, - }, + allowNull: false + } }, { sequelize, tableName: "tokens", timestamps: true, - modelName: "Tokens", + modelName: "Tokens" } ); diff --git a/src/modules/auth/controller/authControllers.ts b/src/modules/auth/controller/authControllers.ts index 5b58ac0d..4b225cd3 100644 --- a/src/modules/auth/controller/authControllers.ts +++ b/src/modules/auth/controller/authControllers.ts @@ -2,11 +2,11 @@ import { invalidateToken } from "../repository/authRepositories"; import { Request, Response } from "express"; export const logoutUser = async (req: Request, res: Response)=>{ - const authHeader = req.headers['authorization']; + const authHeader = req.headers["authorization"]; if(!authHeader){ return res.status(401).json({message: "Unauthorized"}); } - const token = authHeader.split(' ')[1] + const token = authHeader.split(" ")[1] const success = await invalidateToken(token); if(!success)return res.status(401).json({message: "internal server error!"}); diff --git a/src/modules/auth/repository/authRepositories.ts b/src/modules/auth/repository/authRepositories.ts index acdcf503..21839e5a 100644 --- a/src/modules/auth/repository/authRepositories.ts +++ b/src/modules/auth/repository/authRepositories.ts @@ -1,4 +1,4 @@ -import { Op, where } from "sequelize"; +import { Op} from "sequelize"; import db from "../../../databases/models"; export const invalidateToken = async(token:string)=>{ From 44d5be73733f0ca9a5dfe486c98f27d648ef4820 Mon Sep 17 00:00:00 2001 From: solangeihirwe03 Date: Thu, 30 May 2024 00:30:10 +0200 Subject: [PATCH 3/8] [finishes#187584914] logout feature --- README.md | 67 ++++---- src/middlewares/validation.ts | 17 +- .../auth/controller/authControllers.ts | 144 +++++++++++----- .../auth/repository/authRepositories.ts | 14 +- src/modules/auth/test/auth.spec.ts | 154 +++++++++++++----- src/modules/auth/test/user.spec.ts | 0 .../auth/validation/userValidations.ts | 0 src/routes/authRouter.ts | 45 ++++- src/routes/userRoutes.ts | 8 - src/types/index.d.ts | 1 + swagger.json | 86 +++++----- 11 files changed, 358 insertions(+), 178 deletions(-) delete mode 100644 src/modules/auth/test/user.spec.ts delete mode 100644 src/modules/auth/validation/userValidations.ts delete mode 100644 src/routes/userRoutes.ts diff --git a/README.md b/README.md index dcfc72f0..e14181d3 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ This is the backend for E-Commerce-Ninjas, written in Node.js with TypeScript. [![CircleCI](https://dl.circleci.com/status-badge/img/gh/atlp-rwanda/e-commerce-ninjas-bn/tree/develop.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/atlp-rwanda/e-commerce-ninjas-bn/tree/develop) [![codecov](https://codecov.io/gh/atlp-rwanda/e-commerce-ninjas-bn/graph/badge.svg?token=6ZWudFPM1S)](https://codecov.io/gh/atlp-rwanda/e-commerce-ninjas-bn) - ## HOSTED SERVER URL [https://e-commerce-ninjas-backend.onrender.com/](https://e-commerce-ninjas-backend.onrender.com/) @@ -22,7 +21,6 @@ This is the backend for E-Commerce-Ninjas, written in Node.js with TypeScript. [https://github.com/atlp-rwanda/e-commerce-ninjas-bn](https://github.com/atlp-rwanda/e-commerce-ninjas-bn) - ## COMPLETED FEATURES - Welcome Endpoint @@ -30,19 +28,18 @@ This is the backend for E-Commerce-Ninjas, written in Node.js with TypeScript. - Verification Email Endpoint - Resend verification Endpoint - Login Endpoint +- Logout Endpoint ## TABLE OF API ENDPOINTS SPECIFICATION AND DESCRIPTION - -| No | VERBS | ENDPOINTS | STATUS | ACCESS | DESCRIPTION | -|----|-------|------------------------------|-------------|--------|---------------------------| -| 1 | GET | / | 200 OK | public | Show welcome message | -| 2 | POST | /api/auth/register | 201 CREATED | public | create user account | -| 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 | -| 4 | POST | /api/auth/login | 200 OK | public | Login with Email and Password | - - +| No | VERBS | ENDPOINTS | STATUS | ACCESS | DESCRIPTION | +| --- | ----- | ----------------------------- | ----------- | ------ | ----------------------------- | +| 1 | GET | / | 200 OK | public | Show welcome message | +| 2 | POST | /api/auth/register | 201 CREATED | public | create user account | +| 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 | POST | /api/auth/logout | 200 OK | public | Logout user | ## INSTALLATION @@ -89,34 +86,34 @@ This is the backend for E-Commerce-Ninjas, written in Node.js with TypeScript. ## INITILIAZE SEQUELIZE CLI 1. Initialize Sequelize CLI: - ```sh - npx sequelize-cli init - ``` + ```sh + npx sequelize-cli init + ``` 2. Generate Seeder: - ```sh - npx sequelize-cli seed:generate --name name-of-your-seeder - ``` + ```sh + npx sequelize-cli seed:generate --name name-of-your-seeder + ``` 3. Generate Migrations: - ```sh - npx sequelize-cli migration:generate --name name-of-your-migration - ``` + ```sh + npx sequelize-cli migration:generate --name name-of-your-migration + ``` 4. Define Migration: - Edit the generated migration file to include the tables you want to create. + Edit the generated migration file to include the tables you want to create. 5. Define Seeder Data: - Edit the generated seeder file to include the data you want to insert. + Edit the generated seeder file to include the data you want to insert. 6. Run the Seeder: - ```sh - npm run createAllSeeders - ``` + ```sh + npm run createAllSeeders + ``` 7. Run the Migration: - ```sh - npm run createAllTables - ``` + ```sh + npm run createAllTables + ``` 8. Delete the Seeder: - ```sh - npm run deleteAllSeeders - ``` + ```sh + npm run deleteAllSeeders + ``` 9. Delete the Migration: - ```sh - npm run deleteAllTables - ``` \ No newline at end of file + ```sh + npm run deleteAllTables + ``` diff --git a/src/middlewares/validation.ts b/src/middlewares/validation.ts index 88717adf..4001d466 100644 --- a/src/middlewares/validation.ts +++ b/src/middlewares/validation.ts @@ -97,6 +97,21 @@ const verifyUserCredentials = async (req: Request, res: Response, next: NextFunc } +const isUserLoggedIn = (req: Request, res: Response, next: NextFunction) => { + const authHeader = req.headers["authorization"]; + console.log(authHeader) + if (!authHeader) { + return res.status(httpStatus.UNAUTHORIZED).json({ status: httpStatus.UNAUTHORIZED, message: "No Token provided" }); + } + (req as IRequest).token = authHeader.split(" ")[1]; + next(); +} -export { validation, isUserExist, isAccountVerified, verifyUserCredentials }; \ No newline at end of file +export { + validation, + isUserExist, + isAccountVerified, + verifyUserCredentials, + isUserLoggedIn, +}; \ No newline at end of file diff --git a/src/modules/auth/controller/authControllers.ts b/src/modules/auth/controller/authControllers.ts index 4b6dde4e..b7ab081d 100644 --- a/src/modules/auth/controller/authControllers.ts +++ b/src/modules/auth/controller/authControllers.ts @@ -10,51 +10,115 @@ import authRepositories from "../repository/authRepositories"; import { sendVerificationEmail } from "../../../services/sendEmail"; const registerUser = async (req: Request, res: Response): Promise => { - try { - const register: UsersAttributes = await userRepositories.createUser(req.body); - const token: string = generateToken(register.id); - const session = { userId: register.id, device: req.headers["user-device"], token: token, otp: null }; - await authRepositories.createSession(session); - await sendVerificationEmail(register.email, "Verification Email", `${process.env.SERVER_URL_PRO}/api/auth/verify-email/${token}`); - res.status(httpStatus.CREATED).json({ message: "Account created successfully. Please check email to verify account.", data: { user: register } }) - } catch (error) { - res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ status: httpStatus.INTERNAL_SERVER_ERROR, message: error.message }); - } - -} + try { + const register: UsersAttributes = await userRepositories.createUser( + req.body + ); + const token: string = generateToken(register.id); + const session = { + userId: register.id, + device: req.headers["user-device"], + token: token, + otp: null, + }; + await authRepositories.createSession(session); + await sendVerificationEmail( + register.email, + "Verification Email", + `${process.env.SERVER_URL_PRO}/api/auth/verify-email/${token}` + ); + res.status(httpStatus.CREATED).json({ + message: + "Account created successfully. Please check email to verify account.", + data: { user: register }, + }); + } catch (error) { + res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ + status: httpStatus.INTERNAL_SERVER_ERROR, + message: error.message, + }); + } +}; const sendVerifyEmail = async (req: any, res: Response) => { - try { - await sendVerificationEmail(req.user.email, "Verification Email", `${process.env.SERVER_URL_PRO}/api/auth/verify-email/${req.session.token}`); - res.status(httpStatus.OK).json({ status: httpStatus.OK, message: "Verification email sent successfully." }); - } catch (error) { - return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ status: httpStatus.INTERNAL_SERVER_ERROR, message: error.message }); - } -} + try { + await sendVerificationEmail( + req.user.email, + "Verification Email", + `${process.env.SERVER_URL_PRO}/api/auth/verify-email/${req.session.token}` + ); + res.status(httpStatus.OK).json({ + status: httpStatus.OK, + message: "Verification email sent successfully.", + }); + } catch (error) { + return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ + status: httpStatus.INTERNAL_SERVER_ERROR, + message: error.message, + }); + } +}; 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); - 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 }); - } -} - + try { + await authRepositories.destroySession(req.user.id, req.session.token); + 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, + }); + } +}; const loginUser = async (req: Request, res: Response) => { - try { - const userId = (req as IRequest).loginUserId; - const token = generateToken(userId); - const session = { userId, device: req.headers["user-device"], token: token, otp: null }; - await userRepositories.createSession(session); - res.status(httpStatus.OK).json({ message: "Logged in successfully", data: { token } }); - } - catch (err) { - return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ message: "Server error", data: err.message }); - } -} + try { + const userId = (req as IRequest).loginUserId; + const token = generateToken(userId); + const session = { + userId, + device: req.headers["user-device"], + token: token, + otp: null, + }; + await userRepositories.createSession(session); + res + .status(httpStatus.OK) + .json({ message: "Logged in successfully", data: { token } }); + } catch (err) { + return res + .status(httpStatus.INTERNAL_SERVER_ERROR) + .json({ message: "Server error", data: err.message }); + } +}; +const logoutUser = async (req: Request, res: Response) => { + try { + await authRepositories.invalidateToken((req as IRequest).token); + res.status(httpStatus.OK).json({ message: "Successfully logged out" }); + } catch (err) { + return res + .status(httpStatus.INTERNAL_SERVER_ERROR) + .json({ + status: httpStatus.INTERNAL_SERVER_ERROR, + message: "Server error", + }); + } +}; -export default { registerUser, sendVerifyEmail, verifyEmail, loginUser } \ No newline at end of file +export default { + registerUser, + sendVerifyEmail, + verifyEmail, + loginUser, + logoutUser, +}; diff --git a/src/modules/auth/repository/authRepositories.ts b/src/modules/auth/repository/authRepositories.ts index c913e782..94864ae3 100644 --- a/src/modules/auth/repository/authRepositories.ts +++ b/src/modules/auth/repository/authRepositories.ts @@ -27,4 +27,16 @@ 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 +const invalidateToken = async (token: string) => { + return await Session.destroy({ where: { token } }); +}; + +export default { + createUser, + createSession, + findUserByAttributes, + destroySession, + UpdateUserByAttributes, + findSessionByUserId, + invalidateToken, +}; \ No newline at end of file diff --git a/src/modules/auth/test/auth.spec.ts b/src/modules/auth/test/auth.spec.ts index be8746ed..943e05c2 100644 --- a/src/modules/auth/test/auth.spec.ts +++ b/src/modules/auth/test/auth.spec.ts @@ -9,8 +9,10 @@ import { isUserExist } from "../../../middlewares/validation"; import authRepositories from "../repository/authRepositories"; import Users from "../../../databases/models/users"; import Session from "../../../databases/models/session"; -import { sendVerificationEmail, transporter } from "../../../services/sendEmail"; - +import { + sendVerificationEmail, + transporter, +} from "../../../services/sendEmail"; chai.use(chaiHttp); const router = () => chai.request(app); @@ -19,6 +21,7 @@ let userId: number = 0; let verifyToken: string | null = null; describe("Authentication Test Cases", () => { + let token; afterEach(async () => { const tokenRecord = await Session.findOne({ where: { userId } }); @@ -32,14 +35,17 @@ describe("Authentication Test Cases", () => { .post("/api/auth/register") .send({ email: "ecommerceninjas45@gmail.com", - password: "userPassword@123" + 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"); userId = response.body.data.user.id; - expect(response.body).to.have.property("message", "Account created successfully. Please check email to verify account."); + expect(response.body).to.have.property( + "message", + "Account created successfully. Please check email to verify account." + ); done(error); }); }); @@ -55,9 +61,12 @@ describe("Authentication Test Cases", () => { expect(res.status).to.equal(httpStatus.OK); expect(res.body).to.be.an("object"); expect(res.body).to.have.property("status", httpStatus.OK); - expect(res.body).to.have.property("message", "Account verified successfully, now login."); + expect(res.body).to.have.property( + "message", + "Account verified successfully, now login." + ); done(err); - }) + }); }); it("should return validation error and 400", (done) => { @@ -65,7 +74,7 @@ describe("Authentication Test Cases", () => { .post("/api/auth/register") .send({ email: "user@example.com", - password: "userPassword" + password: "userPassword", }) .end((error, response) => { expect(response.status).to.equal(400); @@ -75,13 +84,12 @@ describe("Authentication Test Cases", () => { }); }); - it("Should be able to login a registered user", (done) => { router() .post("/api/auth/login") .send({ email: "ecommerceninjas45@gmail.com", - password: "userPassword@123" + password: "userPassword@123", }) .end((error, response) => { expect(response.status).to.equal(httpStatus.OK); @@ -89,17 +97,55 @@ describe("Authentication Test Cases", () => { 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 logout user", (done) => { + router() + .post(`/api/auth/logout`) + .set("Authorization", `Bearer ${token}`) + .end((err, res) => { + expect(res).to.have.status(httpStatus.OK); + expect(res.body).to.have.property("message", "Successfully logged out"); + done(err); + }); + }); + + it("Should return error on logout", (done) => { + sinon + .stub(authRepositories, "invalidateToken") + .throws(new Error("Database Error")); + router() + .post(`/api/auth/logout`) + .set("Authorization", `Bearer ${token}`) + .end((err, res) => { + expect(res).to.have.status(httpStatus.INTERNAL_SERVER_ERROR); + expect(res.body).to.have.property("message", "Server error"); + done(err); + }); + }); + + it("Should return 401 Unauthorized if logout request lacks valid token", (done) => { + router() + .post(`/api/auth/logout`) + .end((err, res) => { + expect(res).to.have.status(httpStatus.UNAUTHORIZED); + expect(res.body).to.have.property("message"); + done(err); + }); + }); + it("should return internal server error on login", (done) => { - sinon.stub(authRepositories, "createSession").throws(new Error("Database error")); + sinon + .stub(authRepositories, "createSession") + .throws(new Error("Database error")); router() .post("/api/auth/login") .send({ email: "ecommerceninjas45@gmail.com", - password: "userPassword@123" + password: "userPassword@123", }) .end((err, res) => { expect(res).to.have.status(httpStatus.INTERNAL_SERVER_ERROR); @@ -111,7 +157,7 @@ describe("Authentication Test Cases", () => { router() .post("/api/auth/login") .send({ - email: "user@example.com" + email: "user@example.com", }) .end((error, response) => { expect(response).to.have.status(httpStatus.BAD_REQUEST); @@ -125,12 +171,15 @@ describe("Authentication Test Cases", () => { .post("/api/auth/login") .send({ email: "fakeemail@gmail.com", - password: "userPassword@123" + password: "userPassword@123", }) .end((error, response) => { expect(response).to.have.status(httpStatus.BAD_REQUEST); expect(response.body).to.be.a("object"); - expect(response.body).to.have.property("message", "Invalid Email or Password"); + expect(response.body).to.have.property( + "message", + "Invalid Email or Password" + ); done(error); }); }); @@ -140,20 +189,21 @@ describe("Authentication Test Cases", () => { .post("/api/auth/login") .send({ email: "ecommerceninjas45@gmail.com", - password: "fakePassword@123" + password: "fakePassword@123", }) .end((error, response) => { expect(response).to.have.status(httpStatus.BAD_REQUEST); expect(response.body).to.be.a("object"); - expect(response.body).to.have.property("message", "Invalid Email or Password"); + expect(response.body).to.have.property( + "message", + "Invalid Email or Password" + ); done(error); }); }); - }); describe("isUserExist Middleware", () => { - before(() => { app.post("/auth/register", isUserExist, (req: Request, res: Response) => { res.status(200).json({ message: "success" }); @@ -170,7 +220,7 @@ describe("isUserExist Middleware", () => { .post("/api/auth/register") .send({ email: "ecommerceninjas45@gmail.com", - password: "userPassword@123" + password: "userPassword@123", }) .end((err, res) => { expect(res).to.have.status(httpStatus.BAD_REQUEST); @@ -188,7 +238,7 @@ describe("isUserExist Middleware", () => { password: "hashedPassword", isVerified: false, createdAt: new Date(), - updatedAt: new Date() + updatedAt: new Date(), }); sinon.stub(authRepositories, "findUserByAttributes").resolves(mockUser); @@ -197,37 +247,49 @@ describe("isUserExist Middleware", () => { .post("/api/auth/register") .send({ email: "user@example.com", - password: "userPassword@123" + password: "userPassword@123", }) .end((err, res) => { expect(res).to.have.status(httpStatus.BAD_REQUEST); expect(res.body).to.be.an("object"); expect(res.body).to.have.property("status", httpStatus.BAD_REQUEST); - expect(res.body).to.have.property("message", "Account already exists. Please verify your account"); + expect(res.body).to.have.property( + "message", + "Account already exists. Please verify your account" + ); done(err); }); }); - it("should return internal server error", (done) => { - sinon.stub(authRepositories, "findUserByAttributes").throws(new Error("Database error")); + sinon + .stub(authRepositories, "findUserByAttributes") + .throws(new Error("Database error")); router() .post("/auth/register") .send({ email: "usertesting@gmail.com" }) .end((err, res) => { expect(res).to.have.status(httpStatus.INTERNAL_SERVER_ERROR); expect(res.body).to.be.an("object"); - expect(res.body).to.have.property("status", httpStatus.INTERNAL_SERVER_ERROR); + expect(res.body).to.have.property( + "status", + httpStatus.INTERNAL_SERVER_ERROR + ); expect(res.body).to.have.property("message", "Database error"); done(err); }); }); it("should return internal server error on login", (done) => { - sinon.stub(authRepositories, "findUserByAttributes").throws(new Error("Database error")); + sinon + .stub(authRepositories, "findUserByAttributes") + .throws(new Error("Database error")); router() .post("/api/auth/login") - .send({ email: "ecommerceninjas45@gmail.com", password: "userPassword@123" }) + .send({ + email: "ecommerceninjas45@gmail.com", + password: "userPassword@123", + }) .end((err, res) => { expect(res).to.have.status(httpStatus.INTERNAL_SERVER_ERROR); done(err); @@ -247,14 +309,15 @@ describe("isUserExist Middleware", () => { done(err); }); }); - }); describe("POST /auth/register - Error Handling", () => { let registerUserStub: sinon.SinonStub; beforeEach(() => { - registerUserStub = sinon.stub(authRepositories, "createUser").throws(new Error("Test error")); + registerUserStub = sinon + .stub(authRepositories, "createUser") + .throws(new Error("Test error")); }); afterEach(() => { @@ -269,7 +332,7 @@ describe("POST /auth/register - Error Handling", () => { expect(res.status).to.equal(httpStatus.INTERNAL_SERVER_ERROR); expect(res.body).to.deep.equal({ status: httpStatus.INTERNAL_SERVER_ERROR, - message: "Test error" + message: "Test error", }); done(err); }); @@ -291,7 +354,7 @@ describe("isAccountVerified Middleware", () => { expect(res.status).to.equal(httpStatus.NOT_FOUND); expect(res.body).to.have.property("message", "Account not found."); done(err); - }) + }); }); it("should return 'Account already verified' if user is already verified", (done) => { @@ -301,7 +364,7 @@ describe("isAccountVerified Middleware", () => { password: "hashedPassword", isVerified: true, createdAt: new Date(), - updatedAt: new Date() + updatedAt: new Date(), }); sinon.stub(authRepositories, "findUserByAttributes").resolves(mockUser); @@ -311,12 +374,13 @@ describe("isAccountVerified Middleware", () => { .send({ email: "user@example.com" }) .end((err, res) => { expect(res.status).to.equal(httpStatus.BAD_REQUEST); - expect(res.body).to.have.property("message", "Account already verified."); + expect(res.body).to.have.property( + "message", + "Account already verified." + ); done(err); }); }); - - }); describe("Authentication Test Cases", () => { @@ -324,8 +388,14 @@ describe("Authentication Test Cases", () => { let findSessionByUserIdStub: sinon.SinonStub; beforeEach(() => { - findUserByAttributesStub = sinon.stub(authRepositories, "findUserByAttributes"); - findSessionByUserIdStub = sinon.stub(authRepositories, "findSessionByUserId"); + findUserByAttributesStub = sinon.stub( + authRepositories, + "findUserByAttributes" + ); + findSessionByUserIdStub = sinon.stub( + authRepositories, + "findSessionByUserId" + ); }); afterEach(() => { @@ -344,7 +414,10 @@ describe("Authentication Test Cases", () => { .send({ email: "user@example.com" }) .end((err, res) => { expect(res).to.have.status(httpStatus.OK); - expect(res.body).to.have.property("message", "Verification email sent successfully."); + expect(res.body).to.have.property( + "message", + "Verification email sent successfully." + ); done(err); }); }); @@ -353,7 +426,7 @@ describe("Authentication Test Cases", () => { const mockSession = { token: "testToken" }; findUserByAttributesStub.resolves(mockUser); - findSessionByUserIdStub.resolves(mockSession) + findSessionByUserIdStub.resolves(mockSession); findSessionByUserIdStub.resolves(null); router() .post("/api/auth/send-verify-email") @@ -392,5 +465,4 @@ describe("sendVerificationEmail", () => { expect(error).to.be.an("error"); } }); - -}); \ No newline at end of file +}); diff --git a/src/modules/auth/test/user.spec.ts b/src/modules/auth/test/user.spec.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/modules/auth/validation/userValidations.ts b/src/modules/auth/validation/userValidations.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/routes/authRouter.ts b/src/routes/authRouter.ts index 2486cdf4..dc599fd6 100644 --- a/src/routes/authRouter.ts +++ b/src/routes/authRouter.ts @@ -1,15 +1,42 @@ import { Router } from "express"; import authControllers from "../modules/auth/controller/authControllers"; -import { validation, isUserExist, isAccountVerified, verifyUserCredentials } from "../middlewares/validation"; -import { emailSchema, credentialSchema } from "../modules/auth/validation/authValidations"; - +import { + validation, + isUserExist, + isAccountVerified, + verifyUserCredentials, + isUserLoggedIn, +} from "../middlewares/validation"; +import { + emailSchema, + credentialSchema, +} from "../modules/auth/validation/authValidations"; const router: Router = Router(); -router.post("/register", validation(credentialSchema), isUserExist, authControllers.registerUser); -router.get("/verify-email/:token", isAccountVerified, authControllers.verifyEmail); -router.post("/send-verify-email", validation(emailSchema), isAccountVerified, authControllers.sendVerifyEmail); -router.post("/login", validation(credentialSchema), verifyUserCredentials, authControllers.loginUser); - +router.post( + "/register", + validation(credentialSchema), + isUserExist, + authControllers.registerUser +); +router.get( + "/verify-email/:token", + isAccountVerified, + authControllers.verifyEmail +); +router.post( + "/send-verify-email", + validation(emailSchema), + isAccountVerified, + authControllers.sendVerifyEmail +); +router.post( + "/login", + validation(credentialSchema), + verifyUserCredentials, + authControllers.loginUser +); +router.post("/logout", isUserLoggedIn, authControllers.logoutUser); -export default router; \ No newline at end of file +export default router; diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts deleted file mode 100644 index ebc5362b..00000000 --- a/src/routes/userRoutes.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { logoutUser } from "../modules/auth/controller/authControllers"; -import { Router } from "express"; - -const userRoutes = Router(); - -userRoutes.post("/logout", logoutUser) - -export default userRoutes; \ No newline at end of file diff --git a/src/types/index.d.ts b/src/types/index.d.ts index d04faddd..c2a56d68 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -13,4 +13,5 @@ export interface ILogin{ export interface IRequest extends Request{ loginUserId?: number; + token; } \ No newline at end of file diff --git a/swagger.json b/swagger.json index 4a94e02a..78fce4c9 100644 --- a/swagger.json +++ b/swagger.json @@ -25,24 +25,13 @@ "description": "Authentication Endpoint | POST Route" } ], - "schemes": [ - "http", - "https" - ], - "consumes": [ - "application/json", - "none" - ], - "produces": [ - "application/json", - "none" - ], + "schemes": ["http", "https"], + "consumes": ["application/json", "none"], + "produces": ["application/json", "none"], "paths": { "/": { "get": { - "tags": [ - "Initial Route" - ], + "tags": ["Initial Route"], "summary": "Handle Initial / Wrong GET Route", "description": "By using the initial/wrong GET endpoint, you will see a welcome message (Welcome to E-Commerce-Ninja-BackEnd)", "responses": { @@ -54,9 +43,7 @@ }, "/api/auth/register": { "post": { - "tags": [ - "Authentication Routes" - ], + "tags": ["Authentication Routes"], "summary": "Register a new user", "description": "This endpoint allows for registering a new user by providing the required details.", "requestBody": { @@ -76,10 +63,7 @@ "format": "password" } }, - "required": [ - "email", - "password" - ] + "required": ["email", "password"] } } } @@ -129,9 +113,7 @@ }, "/api/auth/send-verify-email": { "post": { - "tags": [ - "Authentication Routes" - ], + "tags": ["Authentication Routes"], "summary": "Resend verification email again", "description": "This endpoint allows user to request verification email again", "requestBody": { @@ -147,9 +129,7 @@ "format": "email" } }, - "required": [ - "email" - ] + "required": ["email"] } } } @@ -196,9 +176,7 @@ }, "/api/auth/verify-email/{token}": { "get": { - "tags": [ - "Authentication Routes" - ], + "tags": ["Authentication Routes"], "summary": "Verify user email", "description": "This endpoint allows user to verify their email", "parameters": [ @@ -251,9 +229,7 @@ }, "/api/auth/login": { "post": { - "tags": [ - "Authentication Routes" - ], + "tags": ["Authentication Routes"], "summary": "Login User", "description": "Login a registered user by providing Email and Password", "requestBody": { @@ -274,10 +250,7 @@ "example": "yourpassword" } }, - "required": [ - "email", - "password" - ] + "required": ["email", "password"] } } } @@ -291,9 +264,39 @@ } } } + }, + "/api/auth/logout": { + "post": { + "tags": ["Authentication Routes"], + "summary": "Logout User", + "description": "Logout a logged in user", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Logout successful" + }, + "401": { + "description": "No Token provided" + }, + "500": { + "description": "Server error" + } + } + } } }, "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + } + }, "schemas": { "User": { "type": "object", @@ -325,10 +328,7 @@ }, "gender": { "type": "string", - "enum": [ - "male", - "female" - ], + "enum": ["male", "female"], "nullable": true }, "birthDate": { @@ -369,4 +369,4 @@ } } } -} \ No newline at end of file +} From ec5b3782f5736470c3630df281a60cbbb3175bb5 Mon Sep 17 00:00:00 2001 From: solangeihirwe03 Date: Thu, 30 May 2024 17:18:45 +0200 Subject: [PATCH 4/8] [delivers##187584914] updated readme & swagger.json --- README.md | 2 +- package-lock.json | 2 +- .../auth/controller/authControllers.ts | 3 +- .../auth/repository/authRepositories.ts | 2 +- swagger.json | 292 ++++++++++-------- 5 files changed, 168 insertions(+), 133 deletions(-) diff --git a/README.md b/README.md index 518629b6..cfb17c3c 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ 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 | POST | /api/auth/logout | 200 OK | public | Logout user | +| 6 | POST | /api/auth/logout | 200 OK | private | Logout user | ======= - Admin change status Endpoint diff --git a/package-lock.json b/package-lock.json index 6dfb8df5..3cfffaa3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10812,4 +10812,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/modules/auth/controller/authControllers.ts b/src/modules/auth/controller/authControllers.ts index b7ab081d..0878c14d 100644 --- a/src/modules/auth/controller/authControllers.ts +++ b/src/modules/auth/controller/authControllers.ts @@ -5,9 +5,10 @@ import { generateToken } from "../../../helpers"; import httpStatus from "http-status"; import { UsersAttributes } from "../../../databases/models/users"; import { IRequest } from "../../../types"; - +import { decodeToken } from "../../../helpers"; import authRepositories from "../repository/authRepositories"; import { sendVerificationEmail } from "../../../services/sendEmail"; +import { isAccountVerified } from "../../../middlewares/validation"; const registerUser = async (req: Request, res: Response): Promise => { try { diff --git a/src/modules/auth/repository/authRepositories.ts b/src/modules/auth/repository/authRepositories.ts index 46bc259f..8fd6b45c 100644 --- a/src/modules/auth/repository/authRepositories.ts +++ b/src/modules/auth/repository/authRepositories.ts @@ -39,5 +39,5 @@ export default { destroySession, UpdateUserByAttributes, findSessionByUserId, - invalidateToken, + invalidateToken }; \ No newline at end of file diff --git a/swagger.json b/swagger.json index c453bd8c..8a049fcf 100644 --- a/swagger.json +++ b/swagger.json @@ -29,13 +29,24 @@ "description": "Change User Status Endpoint | PUT Route" } ], - "schemes": ["http", "https"], - "consumes": ["application/json", "none"], - "produces": ["application/json", "none"], + "schemes": [ + "http", + "https" + ], + "consumes": [ + "application/json", + "none" + ], + "produces": [ + "application/json", + "none" + ], "paths": { "/": { "get": { - "tags": ["Initial Route"], + "tags": [ + "Initial Route" + ], "summary": "Handle Initial / Wrong GET Route", "description": "By using the initial/wrong GET endpoint, you will see a welcome message (Welcome to E-Commerce-Ninja-BackEnd)", "responses": { @@ -47,7 +58,9 @@ }, "/api/auth/register": { "post": { - "tags": ["Authentication Routes"], + "tags": [ + "Authentication Routes" + ], "summary": "Register a new user", "description": "This endpoint allows for registering a new user by providing the required details.", "requestBody": { @@ -67,7 +80,10 @@ "format": "password" } }, - "required": ["email", "password"] + "required": [ + "email", + "password" + ] } } } @@ -117,7 +133,9 @@ }, "/api/auth/send-verify-email": { "post": { - "tags": ["Authentication Routes"], + "tags": [ + "Authentication Routes" + ], "summary": "Resend verification email again", "description": "This endpoint allows user to request verification email again", "requestBody": { @@ -133,7 +151,9 @@ "format": "email" } }, - "required": ["email"] + "required": [ + "email" + ] } } } @@ -180,7 +200,9 @@ }, "/api/auth/verify-email/{token}": { "get": { - "tags": ["Authentication Routes"], + "tags": [ + "Authentication Routes" + ], "summary": "Verify user email", "description": "This endpoint allows user to verify their email", "parameters": [ @@ -233,7 +255,9 @@ }, "/api/auth/login": { "post": { - "tags": ["Authentication Routes"], + "tags": [ + "Authentication Routes" + ], "summary": "Login User", "description": "Login a registered user by providing Email and Password", "requestBody": { @@ -254,7 +278,10 @@ "example": "yourpassword" } }, - "required": ["email", "password"] + "required": [ + "email", + "password" + ] } } } @@ -269,8 +296,6 @@ } } }, - - "/api/users/admin-update-user-status/{id}": { "put": { "tags": [ @@ -378,131 +403,140 @@ } } } - }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - } - }, - "examples": { - "serverError": { - "summary": "Server Error", - "value": { - "message": "Server error" - } - } - } - } - }, - "/api/auth/logout": { - "post": { - "tags": ["Authentication Routes"], - "summary": "Logout User", - "description": "Logout a logged in user", - "security": [ - { - "bearerAuth": [] } - ], - "responses": { - "200": { - "description": "Logout successful" - }, - "401": { - "description": "No Token provided" + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } }, - "500": { - "description": "Server error" + "examples": { + "serverError": { + "summary": "Server Error", + "value": { + "message": "Server error" + } + } } } } } - }, - "components": { - "securitySchemes": { - "bearerAuth": { - "type": "http", - "scheme": "bearer", - "bearerFormat": "JWT" - } }, - "schemas": { - "User": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "firstName": { - "type": "string", - "nullable": true - }, - "lastName": { - "type": "string", - "nullable": true - }, - "email": { - "type": "string" - }, - "password": { - "type": "string" - }, - "phone": { - "type": "integer", - "nullable": true - }, - "profilePicture": { - "type": "string", - "format": "uri" - }, - "gender": { - "type": "string", - "enum": ["male", "female"], - "nullable": true - }, - "birthDate": { - "type": "string", - "format": "date", - "nullable": true - }, - "language": { - "type": "string", - "nullable": true - }, - "currency": { - "type": "string", - "nullable": true - }, - "role": { - "type": "string", - "nullable": true - }, - "isVerified": { - "type": "boolean" - }, - "is2FAEnabled": { - "type": "boolean" - }, - "status": { - "type": "boolean" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" + "/api/auth/logout": { + "post": { + "tags": [ + "Authentication Routes" + ], + "summary": "Logout User", + "description": "Logout a logged in user", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Logout successful" + }, + "401": { + "description": "No Token provided" + }, + "500": { + "description": "Server error" + } + } + } + }, + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + } + }, + "schemas": { + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "firstName": { + "type": "string", + "nullable": true + }, + "lastName": { + "type": "string", + "nullable": true + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "integer", + "nullable": true + }, + "profilePicture": { + "type": "string", + "format": "uri" + }, + "gender": { + "type": "string", + "enum": [ + "male", + "female" + ], + "nullable": true + }, + "birthDate": { + "type": "string", + "format": "date", + "nullable": true + }, + "language": { + "type": "string", + "nullable": true + }, + "currency": { + "type": "string", + "nullable": true + }, + "role": { + "type": "string", + "nullable": true + }, + "isVerified": { + "type": "boolean" + }, + "is2FAEnabled": { + "type": "boolean" + }, + "status": { + "type": "boolean" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } } } } } } -} +} \ No newline at end of file From ea1127067b0c120407cc664f7f290328c8baaf74 Mon Sep 17 00:00:00 2001 From: solangeihirwe03 Date: Thu, 30 May 2024 17:18:45 +0200 Subject: [PATCH 5/8] [delivers##187584914] updated readme & swagger.json --- README.md | 2 +- package-lock.json | 2 +- src/middlewares/validation.ts | 2 +- .../auth/controller/authControllers.ts | 22 +- .../auth/repository/authRepositories.ts | 2 +- src/modules/auth/test/auth.spec.ts | 34 +- src/routes/authRouter.ts | 4 +- swagger.json | 292 ++++++++++-------- 8 files changed, 197 insertions(+), 163 deletions(-) diff --git a/README.md b/README.md index 518629b6..cfb17c3c 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ 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 | POST | /api/auth/logout | 200 OK | public | Logout user | +| 6 | POST | /api/auth/logout | 200 OK | private | Logout user | ======= - Admin change status Endpoint diff --git a/package-lock.json b/package-lock.json index 6dfb8df5..3cfffaa3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10812,4 +10812,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/middlewares/validation.ts b/src/middlewares/validation.ts index a2260f99..1a91a0ce 100644 --- a/src/middlewares/validation.ts +++ b/src/middlewares/validation.ts @@ -113,5 +113,5 @@ export { isUserExist, isAccountVerified, verifyUserCredentials, - isUserLoggedIn, + isUserLoggedIn }; \ No newline at end of file diff --git a/src/modules/auth/controller/authControllers.ts b/src/modules/auth/controller/authControllers.ts index b7ab081d..e738a336 100644 --- a/src/modules/auth/controller/authControllers.ts +++ b/src/modules/auth/controller/authControllers.ts @@ -5,7 +5,7 @@ import { generateToken } from "../../../helpers"; import httpStatus from "http-status"; import { UsersAttributes } from "../../../databases/models/users"; import { IRequest } from "../../../types"; - +// import { decodeToken } from "../../../helpers"; import authRepositories from "../repository/authRepositories"; import { sendVerificationEmail } from "../../../services/sendEmail"; @@ -19,7 +19,7 @@ const registerUser = async (req: Request, res: Response): Promise => { userId: register.id, device: req.headers["user-device"], token: token, - otp: null, + otp: null }; await authRepositories.createSession(session); await sendVerificationEmail( @@ -30,12 +30,12 @@ const registerUser = async (req: Request, res: Response): Promise => { res.status(httpStatus.CREATED).json({ message: "Account created successfully. Please check email to verify account.", - data: { user: register }, + data: { user: register } }); } catch (error) { res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ status: httpStatus.INTERNAL_SERVER_ERROR, - message: error.message, + message: error.message }); } }; @@ -49,12 +49,12 @@ const sendVerifyEmail = async (req: any, res: Response) => { ); res.status(httpStatus.OK).json({ status: httpStatus.OK, - message: "Verification email sent successfully.", + message: "Verification email sent successfully." }); } catch (error) { return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ status: httpStatus.INTERNAL_SERVER_ERROR, - message: error.message, + message: error.message }); } }; @@ -70,12 +70,12 @@ const verifyEmail = async (req: any, res: Response) => { ); res.status(httpStatus.OK).json({ status: httpStatus.OK, - message: "Account verified successfully, now login.", + message: "Account verified successfully, now login." }); } catch (error) { return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ status: httpStatus.INTERNAL_SERVER_ERROR, - message: error.message, + message: error.message }); } }; @@ -88,7 +88,7 @@ const loginUser = async (req: Request, res: Response) => { userId, device: req.headers["user-device"], token: token, - otp: null, + otp: null }; await userRepositories.createSession(session); res @@ -110,7 +110,7 @@ const logoutUser = async (req: Request, res: Response) => { .status(httpStatus.INTERNAL_SERVER_ERROR) .json({ status: httpStatus.INTERNAL_SERVER_ERROR, - message: "Server error", + message: "Server error" }); } }; @@ -120,5 +120,5 @@ export default { sendVerifyEmail, verifyEmail, loginUser, - logoutUser, + logoutUser }; diff --git a/src/modules/auth/repository/authRepositories.ts b/src/modules/auth/repository/authRepositories.ts index 46bc259f..8fd6b45c 100644 --- a/src/modules/auth/repository/authRepositories.ts +++ b/src/modules/auth/repository/authRepositories.ts @@ -39,5 +39,5 @@ export default { destroySession, UpdateUserByAttributes, findSessionByUserId, - invalidateToken, + invalidateToken }; \ No newline at end of file diff --git a/src/modules/auth/test/auth.spec.ts b/src/modules/auth/test/auth.spec.ts index 0d2a6292..f1f3707d 100644 --- a/src/modules/auth/test/auth.spec.ts +++ b/src/modules/auth/test/auth.spec.ts @@ -11,7 +11,7 @@ import Users from "../../../databases/models/users"; import Session from "../../../databases/models/session"; import { sendVerificationEmail, - transporter, + transporter } from "../../../services/sendEmail"; chai.use(chaiHttp); @@ -35,7 +35,7 @@ describe("Authentication Test Cases", () => { .post("/api/auth/register") .send({ email: "ecommerceninjas45@gmail.com", - password: "userPassword@123", + password: "userPassword@123" }) .end((error, response) => { expect(response.status).to.equal(httpStatus.CREATED); @@ -74,7 +74,7 @@ describe("Authentication Test Cases", () => { .post("/api/auth/register") .send({ email: "user@example.com", - password: "userPassword", + password: "userPassword" }) .end((error, response) => { expect(response.status).to.equal(400); @@ -89,7 +89,7 @@ describe("Authentication Test Cases", () => { .post("/api/auth/login") .send({ email: "ecommerceninjas45@gmail.com", - password: "userPassword@123", + password: "userPassword@123" }) .end((error, response) => { expect(response.status).to.equal(httpStatus.OK); @@ -104,7 +104,7 @@ describe("Authentication Test Cases", () => { it("Should be able to logout user", (done) => { router() - .post(`/api/auth/logout`) + .post("/api/auth/logout") .set("Authorization", `Bearer ${token}`) .end((err, res) => { expect(res).to.have.status(httpStatus.OK); @@ -118,7 +118,7 @@ describe("Authentication Test Cases", () => { .stub(authRepositories, "invalidateToken") .throws(new Error("Database Error")); router() - .post(`/api/auth/logout`) + .post("/api/auth/logout") .set("Authorization", `Bearer ${token}`) .end((err, res) => { expect(res).to.have.status(httpStatus.INTERNAL_SERVER_ERROR); @@ -129,7 +129,7 @@ describe("Authentication Test Cases", () => { it("Should return 401 Unauthorized if logout request lacks valid token", (done) => { router() - .post(`/api/auth/logout`) + .post("/api/auth/logout") .end((err, res) => { expect(res).to.have.status(httpStatus.UNAUTHORIZED); expect(res.body).to.have.property("message"); @@ -145,7 +145,7 @@ describe("Authentication Test Cases", () => { .post("/api/auth/login") .send({ email: "ecommerceninjas45@gmail.com", - password: "userPassword@123", + password: "userPassword@123" }) .end((err, res) => { expect(res).to.have.status(httpStatus.INTERNAL_SERVER_ERROR); @@ -157,7 +157,7 @@ describe("Authentication Test Cases", () => { router() .post("/api/auth/login") .send({ - email: "user@example.com", + email: "user@example.com" }) .end((error, response) => { expect(response).to.have.status(httpStatus.BAD_REQUEST); @@ -171,7 +171,7 @@ describe("Authentication Test Cases", () => { .post("/api/auth/login") .send({ email: "fakeemail@gmail.com", - password: "userPassword@123", + password: "userPassword@123" }) .end((error, response) => { expect(response).to.have.status(httpStatus.BAD_REQUEST); @@ -189,7 +189,7 @@ describe("Authentication Test Cases", () => { .post("/api/auth/login") .send({ email: "ecommerceninjas45@gmail.com", - password: "fakePassword@123", + password: "fakePassword@123" }) .end((error, response) => { expect(response).to.have.status(httpStatus.BAD_REQUEST); @@ -219,7 +219,7 @@ describe("isUserExist Middleware", () => { .post("/api/auth/register") .send({ email: "ecommerceninjas45@gmail.com", - password: "userPassword@123", + password: "userPassword@123" }) .end((err, res) => { expect(res).to.have.status(httpStatus.BAD_REQUEST); @@ -237,7 +237,7 @@ describe("isUserExist Middleware", () => { password: "hashedPassword", isVerified: false, createdAt: new Date(), - updatedAt: new Date(), + updatedAt: new Date() }); sinon.stub(authRepositories, "findUserByAttributes").resolves(mockUser); @@ -246,7 +246,7 @@ describe("isUserExist Middleware", () => { .post("/api/auth/register") .send({ email: "user@example.com", - password: "userPassword@123", + password: "userPassword@123" }) .end((err, res) => { expect(res).to.have.status(httpStatus.BAD_REQUEST); @@ -287,7 +287,7 @@ describe("isUserExist Middleware", () => { .post("/api/auth/login") .send({ email: "ecommerceninjas45@gmail.com", - password: "userPassword@123", + password: "userPassword@123" }) .end((err, res) => { expect(res).to.have.status(httpStatus.INTERNAL_SERVER_ERROR); @@ -331,7 +331,7 @@ describe("POST /auth/register - Error Handling", () => { expect(res.status).to.equal(httpStatus.INTERNAL_SERVER_ERROR); expect(res.body).to.deep.equal({ status: httpStatus.INTERNAL_SERVER_ERROR, - message: "Test error", + message: "Test error" }); done(err); }); @@ -363,7 +363,7 @@ describe("isAccountVerified Middleware", () => { password: "hashedPassword", isVerified: true, createdAt: new Date(), - updatedAt: new Date(), + updatedAt: new Date() }); sinon.stub(authRepositories, "findUserByAttributes").resolves(mockUser); diff --git a/src/routes/authRouter.ts b/src/routes/authRouter.ts index dc599fd6..c5d15444 100644 --- a/src/routes/authRouter.ts +++ b/src/routes/authRouter.ts @@ -5,11 +5,11 @@ import { isUserExist, isAccountVerified, verifyUserCredentials, - isUserLoggedIn, + isUserLoggedIn } from "../middlewares/validation"; import { emailSchema, - credentialSchema, + credentialSchema } from "../modules/auth/validation/authValidations"; const router: Router = Router(); diff --git a/swagger.json b/swagger.json index c453bd8c..8a049fcf 100644 --- a/swagger.json +++ b/swagger.json @@ -29,13 +29,24 @@ "description": "Change User Status Endpoint | PUT Route" } ], - "schemes": ["http", "https"], - "consumes": ["application/json", "none"], - "produces": ["application/json", "none"], + "schemes": [ + "http", + "https" + ], + "consumes": [ + "application/json", + "none" + ], + "produces": [ + "application/json", + "none" + ], "paths": { "/": { "get": { - "tags": ["Initial Route"], + "tags": [ + "Initial Route" + ], "summary": "Handle Initial / Wrong GET Route", "description": "By using the initial/wrong GET endpoint, you will see a welcome message (Welcome to E-Commerce-Ninja-BackEnd)", "responses": { @@ -47,7 +58,9 @@ }, "/api/auth/register": { "post": { - "tags": ["Authentication Routes"], + "tags": [ + "Authentication Routes" + ], "summary": "Register a new user", "description": "This endpoint allows for registering a new user by providing the required details.", "requestBody": { @@ -67,7 +80,10 @@ "format": "password" } }, - "required": ["email", "password"] + "required": [ + "email", + "password" + ] } } } @@ -117,7 +133,9 @@ }, "/api/auth/send-verify-email": { "post": { - "tags": ["Authentication Routes"], + "tags": [ + "Authentication Routes" + ], "summary": "Resend verification email again", "description": "This endpoint allows user to request verification email again", "requestBody": { @@ -133,7 +151,9 @@ "format": "email" } }, - "required": ["email"] + "required": [ + "email" + ] } } } @@ -180,7 +200,9 @@ }, "/api/auth/verify-email/{token}": { "get": { - "tags": ["Authentication Routes"], + "tags": [ + "Authentication Routes" + ], "summary": "Verify user email", "description": "This endpoint allows user to verify their email", "parameters": [ @@ -233,7 +255,9 @@ }, "/api/auth/login": { "post": { - "tags": ["Authentication Routes"], + "tags": [ + "Authentication Routes" + ], "summary": "Login User", "description": "Login a registered user by providing Email and Password", "requestBody": { @@ -254,7 +278,10 @@ "example": "yourpassword" } }, - "required": ["email", "password"] + "required": [ + "email", + "password" + ] } } } @@ -269,8 +296,6 @@ } } }, - - "/api/users/admin-update-user-status/{id}": { "put": { "tags": [ @@ -378,131 +403,140 @@ } } } - }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - } - }, - "examples": { - "serverError": { - "summary": "Server Error", - "value": { - "message": "Server error" - } - } - } - } - }, - "/api/auth/logout": { - "post": { - "tags": ["Authentication Routes"], - "summary": "Logout User", - "description": "Logout a logged in user", - "security": [ - { - "bearerAuth": [] } - ], - "responses": { - "200": { - "description": "Logout successful" - }, - "401": { - "description": "No Token provided" + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } }, - "500": { - "description": "Server error" + "examples": { + "serverError": { + "summary": "Server Error", + "value": { + "message": "Server error" + } + } } } } } - }, - "components": { - "securitySchemes": { - "bearerAuth": { - "type": "http", - "scheme": "bearer", - "bearerFormat": "JWT" - } }, - "schemas": { - "User": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "firstName": { - "type": "string", - "nullable": true - }, - "lastName": { - "type": "string", - "nullable": true - }, - "email": { - "type": "string" - }, - "password": { - "type": "string" - }, - "phone": { - "type": "integer", - "nullable": true - }, - "profilePicture": { - "type": "string", - "format": "uri" - }, - "gender": { - "type": "string", - "enum": ["male", "female"], - "nullable": true - }, - "birthDate": { - "type": "string", - "format": "date", - "nullable": true - }, - "language": { - "type": "string", - "nullable": true - }, - "currency": { - "type": "string", - "nullable": true - }, - "role": { - "type": "string", - "nullable": true - }, - "isVerified": { - "type": "boolean" - }, - "is2FAEnabled": { - "type": "boolean" - }, - "status": { - "type": "boolean" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" + "/api/auth/logout": { + "post": { + "tags": [ + "Authentication Routes" + ], + "summary": "Logout User", + "description": "Logout a logged in user", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Logout successful" + }, + "401": { + "description": "No Token provided" + }, + "500": { + "description": "Server error" + } + } + } + }, + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + } + }, + "schemas": { + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "firstName": { + "type": "string", + "nullable": true + }, + "lastName": { + "type": "string", + "nullable": true + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "integer", + "nullable": true + }, + "profilePicture": { + "type": "string", + "format": "uri" + }, + "gender": { + "type": "string", + "enum": [ + "male", + "female" + ], + "nullable": true + }, + "birthDate": { + "type": "string", + "format": "date", + "nullable": true + }, + "language": { + "type": "string", + "nullable": true + }, + "currency": { + "type": "string", + "nullable": true + }, + "role": { + "type": "string", + "nullable": true + }, + "isVerified": { + "type": "boolean" + }, + "is2FAEnabled": { + "type": "boolean" + }, + "status": { + "type": "boolean" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } } } } } } -} +} \ No newline at end of file From e7e67b197d4f21d8aeb771373f657106c6df4bcb Mon Sep 17 00:00:00 2001 From: solangeihirwe03 Date: Fri, 31 May 2024 04:08:22 +0200 Subject: [PATCH 6/8] [deliveres #187584914] logout features completed --- README.md | 11 +- src/helpers/index.ts | 2 +- src/middlewares/authorization.ts | 8 +- src/middlewares/validation.ts | 66 +++- .../auth/controller/authControllers.ts | 34 +- .../auth/repository/authRepositories.ts | 22 +- src/modules/auth/test/auth.spec.ts | 19 +- src/routes/authRouter.ts | 10 +- swagger.json | 326 ++++++++---------- 9 files changed, 253 insertions(+), 245 deletions(-) diff --git a/README.md b/README.md index 7abf7be7..b0104e37 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# TEAM NINJAS BACKEND +# E-COMMERCE WEB APPLICATION SERVER - TEAM NINJAS. -This is the backend for E-Commerce-Ninjas, written in Node.js with TypeScript. +Our e-commerce web application server, developed by Team Ninjas, facilitates smooth online shopping with features like user authentication, product cataloging, and secure payments. It's built to enhance the user experience with high performance and reliability. Suitable for any online marketplace looking to grow. [![Reviewed by Hound](https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg)](https://houndci.com) [![Maintainability](https://api.codeclimate.com/v1/badges/839fc3fa18d25362cd8b/maintainability)](https://codeclimate.com/github/atlp-rwanda/e-commerce-ninjas-bn/maintainability) @@ -30,7 +30,6 @@ This is the backend for E-Commerce-Ninjas, written in Node.js with TypeScript. - Login Endpoint - Admin Update Status Endpoint - Admin Update Role Endpoint --logout-feature - Logout Endpoint ## TABLE OF API ENDPOINTS SPECIFICATION AND DESCRIPTION @@ -43,9 +42,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 | -| 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 | +| 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 | ## INSTALLATION diff --git a/src/helpers/index.ts b/src/helpers/index.ts index a316da93..42da55a7 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -5,7 +5,7 @@ import bcrypt from "bcrypt" dotenv.config const generateToken = (id: number) => { - return jwt.sign({ id }, process.env.JWT_SECRET, { expiresIn: "1h" }); + return jwt.sign({ id }, process.env.JWT_SECRET, { expiresIn: "12h" }); }; const decodeToken = (token: string) => { diff --git a/src/middlewares/authorization.ts b/src/middlewares/authorization.ts index e863db1d..6931fdab 100644 --- a/src/middlewares/authorization.ts +++ b/src/middlewares/authorization.ts @@ -21,7 +21,7 @@ export const userAuthorization = function (roles: string[]) { } if (!token) { - res + return res .status(httpStatus.UNAUTHORIZED) .json({ status: httpStatus.UNAUTHORIZED, message: "Not authorized" }); } @@ -32,7 +32,7 @@ export const userAuthorization = function (roles: string[]) { decoded.id, token ); if (!session) { - res + return res .status(httpStatus.UNAUTHORIZED) .json({ status: httpStatus.UNAUTHORIZED, message: "Not authorized" }); } @@ -40,13 +40,13 @@ export const userAuthorization = function (roles: string[]) { const user = await authRepository.findUserByAttributes("id", decoded.id); if (!user) { - res + return res .status(httpStatus.UNAUTHORIZED) .json({ status: httpStatus.UNAUTHORIZED, message: "Not authorized" }); } if (!roles.includes(user.role)) { - res + return res .status(httpStatus.UNAUTHORIZED) .json({ status: httpStatus.UNAUTHORIZED, message: "Not authorized" }); } diff --git a/src/middlewares/validation.ts b/src/middlewares/validation.ts index 09e222f4..09936a5a 100644 --- a/src/middlewares/validation.ts +++ b/src/middlewares/validation.ts @@ -68,7 +68,7 @@ const isAccountVerified = async (req: any, res: Response, next: NextFunction) => return res.status(httpStatus.BAD_REQUEST).json({ message: "Account already verified." }); } - const session = await authRepositories.findSessionByUserId(user.id); + const session = await authRepositories.findSessionByAttributes("userId",user.id); if (!session) { return res.status(httpStatus.BAD_REQUEST).json({ message: "Invalid token." }); } @@ -81,21 +81,59 @@ const isAccountVerified = async (req: any, res: Response, next: NextFunction) => } } -const verifyUserCredentials = async (req: Request, res: Response, next: NextFunction) => { - try { - const user: UsersAttributes = await authRepositories.findUserByAttributes("email", req.body.email); - if (!user) { - return res.status(httpStatus.BAD_REQUEST).json({ message: "Invalid Email or Password", data: null }); - } - const passwordMatches = await comparePassword(req.body.password, user.password) - if (!passwordMatches) return res.status(httpStatus.BAD_REQUEST).json({ message: "Invalid Email or Password", data: null }); - (req as IRequest).loginUserId = user.id; - return next(); - } catch (error) { - res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ message: "Server error", data: error.message }) +const verifyUserCredentials = async ( + req: any, + res: Response, + next: NextFunction +) => { + try { + const user: UsersAttributes = await authRepositories.findUserByAttributes( + "email", + req.body.email + ); + if (!user) { + return res + .status(httpStatus.BAD_REQUEST) + .json({ message: "Invalid Email or Password" }); } -} + const passwordMatches = await comparePassword( + req.body.password, + user.password + ); + if (!passwordMatches) { + return res + .status(httpStatus.BAD_REQUEST) + .json({ message: "Invalid Email or Password" }); + } + + req.user = user; + + const device = req.headers["user-agent"]; + if (!device) { + return next(); + } + + const existingToken = await authRepositories.findTokenByDeviceIdAndUserId( + device, + user.id + ); + if (existingToken) { + return res + .status(httpStatus.OK) + .json({ + message: "Logged in successfully", + data: { token: existingToken }, + }); + } else { + return next(); + } + } catch (error) { + return res + .status(httpStatus.INTERNAL_SERVER_ERROR) + .json({ message: "Internal Server error", data: error.message }); + } +}; diff --git a/src/modules/auth/controller/authControllers.ts b/src/modules/auth/controller/authControllers.ts index d81f2d69..8a59c52d 100644 --- a/src/modules/auth/controller/authControllers.ts +++ b/src/modules/auth/controller/authControllers.ts @@ -5,7 +5,6 @@ import { generateToken } from "../../../helpers"; import httpStatus from "http-status"; import { UsersAttributes } from "../../../databases/models/users"; import { IRequest } from "../../../types"; -// import { decodeToken } from "../../../helpers"; import authRepositories from "../repository/authRepositories"; import { sendVerificationEmail } from "../../../services/sendEmail"; @@ -17,7 +16,7 @@ const registerUser = async (req: Request, res: Response): Promise => { const token: string = generateToken(register.id); const session = { userId: register.id, - device: req.headers["user-device"], + device: req.headers["user-agent"], token: token, otp: null }; @@ -60,23 +59,22 @@ 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); - 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 }); - } + try { + await authRepositories.destroySession(req.user.id, req.session.token) + 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 }); + } } -const loginUser = async (req: Request, res: Response) => { +const loginUser = async (req: any, res: Response) => { try { - const userId = (req as IRequest).loginUserId; - const token = generateToken(userId); + const token = generateToken(req.user.id); const session = { - userId, - device: req.headers["user-device"], + userId: req.user.id, + device: req.headers["user-agent"], token: token, otp: null }; @@ -87,20 +85,20 @@ const loginUser = async (req: Request, res: Response) => { } catch (err) { return res .status(httpStatus.INTERNAL_SERVER_ERROR) - .json({ message: "Server error", data: err.message }); + .json({ message: "Internal Server error", data: err.message }); } }; -const logoutUser = async (req: Request, res: Response) => { +const logoutUser = async (req: any, res: Response) => { try { - await authRepositories.invalidateToken((req as IRequest).token); + await authRepositories.destroySession(req.user.id, req.session.token) res.status(httpStatus.OK).json({ message: "Successfully logged out" }); } catch (err) { return res .status(httpStatus.INTERNAL_SERVER_ERROR) .json({ status: httpStatus.INTERNAL_SERVER_ERROR, - message: "Server error" + message: "Internal Server error" }); } }; diff --git a/src/modules/auth/repository/authRepositories.ts b/src/modules/auth/repository/authRepositories.ts index e738a841..d11daf9d 100644 --- a/src/modules/auth/repository/authRepositories.ts +++ b/src/modules/auth/repository/authRepositories.ts @@ -3,8 +3,8 @@ import Users from "../../../databases/models/users"; import Session from "../../../databases/models/session"; -const createUser = async (body: any) => { - return await Users.create(body); +const createUser = async (body: any) => { + return await Users.create({ ...body, role:'buyer' }); }; const findUserByAttributes = async (key: string, value: any) => { @@ -28,8 +28,17 @@ const createSession = async (body: any) => { return await Session.create(body); }; -const findSessionByUserId = async( userId:number ) => { - return await Session.findOne({ where: { userId } }); +const findSessionByAttributes = async( key:string, value: any ) => { + return await Session.findOne({ where: { [key]:value } }); +} + +const findSessionByUserIdAndToken = async (userId: number, token: string) => { + return await Session.findOne({ where: { token, userId } }); +}; + +const findTokenByDeviceIdAndUserId = async (device: string, userId: number)=>{ + const session = await Session.findOne({ where: {device, userId} }); + return session.token; } const destroySession = async (userId: number, token:string) =>{ @@ -42,6 +51,7 @@ export default { findUserByAttributes, destroySession, updateUserByAttributes, - findSessionByUserId, - findSessionByUserIdAndToken + findSessionByAttributes, + findSessionByUserIdAndToken, + findTokenByDeviceIdAndUserId, }; diff --git a/src/modules/auth/test/auth.spec.ts b/src/modules/auth/test/auth.spec.ts index f1f3707d..14a6f095 100644 --- a/src/modules/auth/test/auth.spec.ts +++ b/src/modules/auth/test/auth.spec.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Request, Response } from "express"; +import { Request, response, Response } from "express"; import chai, { expect } from "chai"; import chaiHttp from "chai-http"; import sinon from "sinon"; @@ -115,28 +115,19 @@ describe("Authentication Test Cases", () => { it("Should return error on logout", (done) => { sinon - .stub(authRepositories, "invalidateToken") + .stub(authRepositories, "destroySession") .throws(new Error("Database Error")); router() .post("/api/auth/logout") .set("Authorization", `Bearer ${token}`) .end((err, res) => { + console.log(res) expect(res).to.have.status(httpStatus.INTERNAL_SERVER_ERROR); expect(res.body).to.have.property("message", "Server error"); done(err); }); }); - it("Should return 401 Unauthorized if logout request lacks valid token", (done) => { - router() - .post("/api/auth/logout") - .end((err, res) => { - expect(res).to.have.status(httpStatus.UNAUTHORIZED); - expect(res.body).to.have.property("message"); - done(err); - }); - }); - it("should return internal server error on login", (done) => { sinon .stub(authRepositories, "createSession") @@ -393,7 +384,7 @@ describe("Authentication Test Cases", () => { ); findSessionByUserIdStub = sinon.stub( authRepositories, - "findSessionByUserId" + "findSessionByAttributes" ); }); @@ -464,4 +455,4 @@ describe("sendVerificationEmail", () => { expect(error).to.be.an("error"); } }); -}); +}); \ No newline at end of file diff --git a/src/routes/authRouter.ts b/src/routes/authRouter.ts index c5d15444..f1143de1 100644 --- a/src/routes/authRouter.ts +++ b/src/routes/authRouter.ts @@ -4,13 +4,13 @@ import { validation, isUserExist, isAccountVerified, - verifyUserCredentials, - isUserLoggedIn + verifyUserCredentials } from "../middlewares/validation"; import { emailSchema, credentialSchema } from "../modules/auth/validation/authValidations"; +import { userAuthorization } from "../middlewares/authorization"; const router: Router = Router(); @@ -37,6 +37,10 @@ router.post( verifyUserCredentials, authControllers.loginUser ); -router.post("/logout", isUserLoggedIn, authControllers.logoutUser); +router.post( + "/logout", + userAuthorization(["buyer", "seller", "admin"]), + authControllers.logoutUser +); export default router; diff --git a/swagger.json b/swagger.json index aae7fa76..1cf4797b 100644 --- a/swagger.json +++ b/swagger.json @@ -29,22 +29,13 @@ "description": "" } ], - "schemes": [ - "http", - "https" - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "schemes": ["http", "https"], + "consumes": ["application/json"], + "produces": ["application/json"], "paths": { "/": { "get": { - "tags": [ - "Initial Route" - ], + "tags": ["Initial Route"], "summary": "Handle Initial / Wrong GET Route", "description": "By using the initial/wrong GET endpoint, you will see a welcome message (Welcome to E-Commerce-Ninja-BackEnd)", "responses": { @@ -56,9 +47,7 @@ }, "/api/auth/register": { "post": { - "tags": [ - "Authentication Routes" - ], + "tags": ["Authentication Routes"], "summary": "Register a new user", "description": "This endpoint allows for registering a new user by providing the required details.", "requestBody": { @@ -78,10 +67,7 @@ "format": "password" } }, - "required": [ - "email", - "password" - ] + "required": ["email", "password"] } } } @@ -131,9 +117,7 @@ }, "/api/auth/send-verify-email": { "post": { - "tags": [ - "Authentication Routes" - ], + "tags": ["Authentication Routes"], "summary": "Resend verification email again", "description": "This endpoint allows user to request verification email again", "requestBody": { @@ -149,9 +133,7 @@ "format": "email" } }, - "required": [ - "email" - ] + "required": ["email"] } } } @@ -198,9 +180,7 @@ }, "/api/auth/verify-email/{token}": { "get": { - "tags": [ - "Authentication Routes" - ], + "tags": ["Authentication Routes"], "summary": "Verify user email", "description": "This endpoint allows user to verify their email", "parameters": [ @@ -253,9 +233,7 @@ }, "/api/auth/login": { "post": { - "tags": [ - "Authentication Routes" - ], + "tags": ["Authentication Routes"], "summary": "Login User", "description": "Login a registered user by providing Email and Password", "requestBody": { @@ -276,10 +254,7 @@ "example": "yourpassword" } }, - "required": [ - "email", - "password" - ] + "required": ["email", "password"] } } } @@ -294,10 +269,36 @@ } } }, + "/api/auth/logout": { + "post": { + "tags": ["Authentication Routes"], + "summary": "Logout User", + "description": "Logout a logged in user", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Logout successful" + }, + "401": { + "description": "No Token provided" + }, + "500": { + "description": "Server error" + } + } + } + }, "/api/users/admin-update-user-status/{id}": { "put": { - "tags": [ - "Admin User Routes" + "tags": ["Admin User Routes"], + "security": [ + { + "bearerAuth": [] + } ], "summary": "Change User Status", "description": "This endpoint allows an admin to change User account Status by providing the userId.", @@ -324,9 +325,7 @@ "type": "string" } }, - "required": [ - "status" - ] + "required": ["status"] } } } @@ -401,46 +400,45 @@ } } } - } - } - }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - } }, - "examples": { - "serverError": { - "summary": "Server Error", - "value": { - "message": "Server error" + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + }, + "examples": { + "serverError": { + "summary": "Server Error", + "value": { + "message": "Server error" + } + } + } } } } } } }, - + "/api/users/admin-update-role/{id}": { "put": { - "tags": [ - "Admin User Routes" - ], + "tags": ["Admin User Routes"], "summary": "Update user role", "description": "This endpoint allows admin to update a user's role", "parameters": [ { - "in":"path", + "in": "path", "name": "id", "type": "string", - "description":"Pass the ID" + "description": "Pass the ID" } ], "requestBody": { @@ -453,20 +451,19 @@ "properties": { "role": { "type": "string", - "enum": [ - "Admin", - "Buyer", - "Seller" - ] + "enum": ["Admin", "Buyer", "Seller"] } }, - "required": [ - "role" - ] + "required": ["role"] } } } }, + "security": [ + { + "bearerAuth": [] + } + ], "responses": { "200": { "description": "User role updated successfully", @@ -546,114 +543,85 @@ } } } - }, - "/api/auth/logout": { - "post": { - "tags": [ - "Authentication Routes" - ], - "summary": "Logout User", - "description": "Logout a logged in user", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Logout successful" - }, - "401": { - "description": "No Token provided" - }, - "500": { - "description": "Server error" - } + }, + "components": { + "schemas": { + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "firstName": { + "type": "string", + "nullable": true + }, + "lastName": { + "type": "string", + "nullable": true + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "integer", + "nullable": true + }, + "profilePicture": { + "type": "string", + "format": "uri" + }, + "gender": { + "type": "string", + "enum": ["male", "female"], + "nullable": true + }, + "birthDate": { + "type": "string", + "format": "date", + "nullable": true + }, + "language": { + "type": "string", + "nullable": true + }, + "currency": { + "type": "string", + "nullable": true + }, + "role": { + "type": "string", + "nullable": true + }, + "isVerified": { + "type": "boolean" + }, + "is2FAEnabled": { + "type": "boolean" + }, + "status": { + "type": "boolean" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" } } + } }, - "components": { - "securitySchemes": { - "bearerAuth": { - "type": "http", - "scheme": "bearer", - "bearerFormat": "JWT" - } - }, - "schemas": { - "User": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "firstName": { - "type": "string", - "nullable": true - }, - "lastName": { - "type": "string", - "nullable": true - }, - "email": { - "type": "string" - }, - "password": { - "type": "string" - }, - "phone": { - "type": "integer", - "nullable": true - }, - "profilePicture": { - "type": "string", - "format": "uri" - }, - "gender": { - "type": "string", - "enum": [ - "male", - "female" - ], - "nullable": true - }, - "birthDate": { - "type": "string", - "format": "date", - "nullable": true - }, - "language": { - "type": "string", - "nullable": true - }, - "currency": { - "type": "string", - "nullable": true - }, - "role": { - "type": "string", - "nullable": true - }, - "isVerified": { - "type": "boolean" - }, - "is2FAEnabled": { - "type": "boolean" - }, - "status": { - "type": "boolean" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - } - } - } + "securitySchemes": { + "BearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" } } } -} \ No newline at end of file +} From e508e93b4a3a9b43d13f1691b40a4f68c7b26f0c Mon Sep 17 00:00:00 2001 From: solangeihirwe03 Date: Fri, 31 May 2024 04:10:32 +0200 Subject: [PATCH 7/8] [deliveres #187584914] logout features completed --- src/middlewares/validation.ts | 3 +-- src/modules/auth/controller/authControllers.ts | 1 - src/modules/auth/repository/authRepositories.ts | 2 +- src/modules/auth/test/auth.spec.ts | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/middlewares/validation.ts b/src/middlewares/validation.ts index 09936a5a..64ac79e7 100644 --- a/src/middlewares/validation.ts +++ b/src/middlewares/validation.ts @@ -5,7 +5,6 @@ import { UsersAttributes } from "../databases/models/users"; import Joi from "joi"; import httpStatus from "http-status"; import { comparePassword, decodeToken } from "../helpers"; -import { IRequest } from "../types"; const validation = (schema: Joi.ObjectSchema | Joi.ArraySchema) => async (req: Request, res: Response, next: NextFunction) => { try { @@ -123,7 +122,7 @@ const verifyUserCredentials = async ( .status(httpStatus.OK) .json({ message: "Logged in successfully", - data: { token: existingToken }, + data: { token: existingToken } }); } else { return next(); diff --git a/src/modules/auth/controller/authControllers.ts b/src/modules/auth/controller/authControllers.ts index 8a59c52d..70f9767e 100644 --- a/src/modules/auth/controller/authControllers.ts +++ b/src/modules/auth/controller/authControllers.ts @@ -4,7 +4,6 @@ import userRepositories from "../repository/authRepositories"; import { generateToken } from "../../../helpers"; import httpStatus from "http-status"; import { UsersAttributes } from "../../../databases/models/users"; -import { IRequest } from "../../../types"; import authRepositories from "../repository/authRepositories"; import { sendVerificationEmail } from "../../../services/sendEmail"; diff --git a/src/modules/auth/repository/authRepositories.ts b/src/modules/auth/repository/authRepositories.ts index d11daf9d..2eea1d7c 100644 --- a/src/modules/auth/repository/authRepositories.ts +++ b/src/modules/auth/repository/authRepositories.ts @@ -4,7 +4,7 @@ import Users from "../../../databases/models/users"; import Session from "../../../databases/models/session"; const createUser = async (body: any) => { - return await Users.create({ ...body, role:'buyer' }); + return await Users.create({ ...body, role:"buyer" }); }; const findUserByAttributes = async (key: string, value: any) => { diff --git a/src/modules/auth/test/auth.spec.ts b/src/modules/auth/test/auth.spec.ts index 14a6f095..dcae4919 100644 --- a/src/modules/auth/test/auth.spec.ts +++ b/src/modules/auth/test/auth.spec.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Request, response, Response } from "express"; +import { Request, Response } from "express"; import chai, { expect } from "chai"; import chaiHttp from "chai-http"; import sinon from "sinon"; From a3531b44955ecf8c362a3384e0b806d3fdd6ceaf Mon Sep 17 00:00:00 2001 From: solangeihirwe03 Date: Fri, 31 May 2024 11:00:38 +0200 Subject: [PATCH 8/8] [delivers #187584914] finished logout feature --- .../auth/controller/authControllers.ts | 3 +-- src/modules/auth/test/auth.spec.ts | 24 ++++++++++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/modules/auth/controller/authControllers.ts b/src/modules/auth/controller/authControllers.ts index 34474d91..6979a87a 100644 --- a/src/modules/auth/controller/authControllers.ts +++ b/src/modules/auth/controller/authControllers.ts @@ -6,11 +6,10 @@ import httpStatus from "http-status"; import { UsersAttributes } from "../../../databases/models/users"; import authRepositories from "../repository/authRepositories"; import { sendVerificationEmail } from "../../../services/sendEmail"; -import { isAccountVerified } from "../../../middlewares/validation"; const registerUser = async (req: Request, res: Response): Promise => { try { - const register: UsersAttributes = await userRepositories.createUser( + const register: UsersAttributes = await authRepositories.createUser( req.body ); const token: string = generateToken(register.id); diff --git a/src/modules/auth/test/auth.spec.ts b/src/modules/auth/test/auth.spec.ts index dcae4919..2a820019 100644 --- a/src/modules/auth/test/auth.spec.ts +++ b/src/modules/auth/test/auth.spec.ts @@ -113,6 +113,25 @@ describe("Authentication Test Cases", () => { }); }); + + it("Should be able to login a registered user", (done) => { + router() + .post("/api/auth/login") + .send({ + email: "ecommerceninjas45@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 return error on logout", (done) => { sinon .stub(authRepositories, "destroySession") @@ -121,13 +140,12 @@ describe("Authentication Test Cases", () => { .post("/api/auth/logout") .set("Authorization", `Bearer ${token}`) .end((err, res) => { - console.log(res) expect(res).to.have.status(httpStatus.INTERNAL_SERVER_ERROR); - expect(res.body).to.have.property("message", "Server error"); + expect(res.body).to.have.property("message", "Internal Server error"); done(err); }); }); - + it("should return internal server error on login", (done) => { sinon .stub(authRepositories, "createSession")