From d82b8343f9480a1523ab09b5fc89f12387e86b91 Mon Sep 17 00:00:00 2001 From: Elvis MUGISHA <99681768+Y-elv@users.noreply.github.com> Date: Mon, 10 Jun 2024 15:32:23 +0200 Subject: [PATCH] Ft 2 fa v2 187584919 (#60) * Ft login v google 187584916 (#47) * login via google * Ft-login via google * login via google * ft login via google * ft login with google * Logout feature (#26) (#46) * Logout feature (#26) * [#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 * fixing bugs * rebased --------- Co-authored-by: Solange Duhimbaze Ihirwe <159579750+solangeihirwe03@users.noreply.github.com> * [Finishes #187584924] Seller Create/Add a product (#48) * [Delivers #187584924] Seller Create/Add a product * updated ReaderMe file * Rebased on develop * Fixed login fetaure (#59) * Ft delete items seller #187584926 (#52) * rebase * rebase * [delivers #187584926] seller delete item * [Delivers #187584924] Seller Create/Add a product * [start #187584926] seller delete item --------- Co-authored-by: Solangeihirwe Co-authored-by: AimePazzo <108128511+AimePazzo@users.noreply.github.com> * [starts #187584911] Seller statistics per timeframe (#54) * [starts #187584911] Seller statistics per timeframe * [finishes #187584911] Seller statistics per timeframe * Seller-statistics updated * rebase1 * rebasing * rebasing * resolving circle CI issues --------- Co-authored-by: AimePazzo <108128511+AimePazzo@users.noreply.github.com> Co-authored-by: Niyonshuti Jean De Dieu <152473876+Jadowacu1@users.noreply.github.com> Co-authored-by: MANISHIMWESalton <149325279+MANISHIMWESalton@users.noreply.github.com> Co-authored-by: Solange Duhimbaze Ihirwe <159579750+solangeihirwe03@users.noreply.github.com> Co-authored-by: Mr. David <128073754+ProgrammerDATCH@users.noreply.github.com> Co-authored-by: Solangeihirwe --- .DS_Store | Bin 0 -> 8196 bytes README.md | 2 +- package.json | 2 +- .../20240523180022-create-sessions.ts | 4 + src/databases/models/session.ts | 6 + src/databases/seeders/20240520202759-users.ts | 15 +- src/helpers/index.ts | 12 +- src/index.spec.ts | 1 - src/index.ts | 8 +- src/middlewares/validation.ts | 300 +++++------ .../auth/controller/authControllers.ts | 40 +- .../auth/repository/authRepositories.ts | 30 +- src/modules/auth/test/auth.spec.ts | 279 ++++++++-- .../auth/validation/authValidations.ts | 34 +- src/modules/user/test/user.spec.ts | 18 +- .../user/validation/userValidations.ts | 1 - src/routes/authRouter.ts | 28 +- src/routes/index.ts | 1 + src/services/sendEmail.ts | 14 +- swagger.json | 502 +++++++++++------- 20 files changed, 819 insertions(+), 478 deletions(-) create mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..fdc5832269c8b68b5a2af9d9b27a1ba558416afd GIT binary patch literal 8196 zcmeHMJ#W-N5S_gb;&4QO1X2nLOCSmgME-$^E}~13lpq54p$p*-pK>{*NGbdW5QH>= zil2Z4LZL%K&{CvC6ciLtrK3XdW_K^%J=-A_LdmSOJC^5-=lOZPd*dS_H9Kt05KR+N zfX?qCgnHnKJ`RxceGbO``k!D-!aj5t!%7!XiVki?1 zf5>py$lAnM6Hdy6ld_qWtx%NCj<`_6NkzuWC<=%ISp~RupQNi?v(u_uzt1c-<6hqc zR6pdSdU3tp=*A6rgLltgeZN`xc)bYOIwZ2c>}J+)a|Qf5;J4Q8IKHVJUIO-gE|*^m z^H;VF7ssD6$n$%7&+g&UG%?Cc%QU2hmz{!U7U6REeD3u~kFo3Y;*?1-%Hv}e!xP}c zOa^qH)_peR8(`!4T`mjljoK}v#~3DMQiSn%n8mjS9uH_8vt@~5@L8iij~*lS^#T|b zAYE>oZ!SKy&#Dleviv*R<7+mF08C?gh%ZyCbQd$J?J=ELdIY8!jLUKN%jx;YM!y_f z%I+YX$Lr(^q{Q59VO9^ulq)^^NU=L3QsRa?lzuZ zPah5Sce{bB%jC-MorAD0ua)ZF)|TN_cx{*GVel^RsV6U)cNtn?jdEEM?Y>VvE~o8p zZ%6N+jdK}fy>31#>2-&5N)rW+ssd$mW7Ay!&lP|FKdQGc_azF50#HCz>WlSRJ}8ib ze@2DqchI>pE-_YzV9>v`V0AeTyZ?tFjzd5>CbBj$7H81@`GYw=|`whH_K D(#|}- literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 37fde1fb..113a8120 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,8 @@ Our e-commerce web application server, developed by Team Ninjas, facilitates smo - Admin get users Endpoint - Admin get user Endpoint - Logout Endpoint +- Update User Profile Endpoint - Get User Profile Endpoint -- User Update Profile Endpoint - Seller create shop Endpoint - Seller create product Endpoint - Seller update product Endpoint diff --git a/package.json b/package.json index 59352816..5848cf9c 100644 --- a/package.json +++ b/package.json @@ -116,4 +116,4 @@ "eslint-plugin-import": "^2.29.1", "lint-staged": "^15.2.2" } -} +} \ No newline at end of file diff --git a/src/databases/migrations/20240523180022-create-sessions.ts b/src/databases/migrations/20240523180022-create-sessions.ts index 177cc3a0..71ee14ed 100644 --- a/src/databases/migrations/20240523180022-create-sessions.ts +++ b/src/databases/migrations/20240523180022-create-sessions.ts @@ -31,6 +31,10 @@ export default { type: new DataTypes.STRING(280), allowNull: true }, + otpExpiration: { + type: DataTypes.DATE, + allowNull: true + }, createdAt: { allowNull: false, type: DataTypes.DATE, diff --git a/src/databases/models/session.ts b/src/databases/models/session.ts index 71833d88..96bf5674 100644 --- a/src/databases/models/session.ts +++ b/src/databases/models/session.ts @@ -9,6 +9,7 @@ export interface SessionAttributes { device: string; token: string; otp: string; + otpExpiration: Date; createdAt: Date; updatedAt: Date; } @@ -19,6 +20,7 @@ class Session extends Model implements SessionAttributes { declare device: string; declare token: string; declare otp: string; + declare otpExpiration: Date; declare createdAt: Date; declare updatedAt: Date; @@ -51,6 +53,10 @@ Session.init( type: new DataTypes.STRING(280), allowNull: true }, + otpExpiration: { + type: DataTypes.DATE, + allowNull: true + }, createdAt: { field: "createdAt", type: DataTypes.DATE, diff --git a/src/databases/seeders/20240520202759-users.ts b/src/databases/seeders/20240520202759-users.ts index a6e4ebbb..06b9667b 100644 --- a/src/databases/seeders/20240520202759-users.ts +++ b/src/databases/seeders/20240520202759-users.ts @@ -28,7 +28,8 @@ const userOne = { role: "admin", status: "enabled", isVerified: true, -}; + is2FAEnabled: false +} const userTwo = { id: userTwoId, createdAt: new Date(), @@ -46,7 +47,8 @@ const userTwo = { role: "buyer", status: "enabled", isVerified: true, -}; + is2FAEnabled: false +} const userThree = { id: userThreeId, @@ -65,7 +67,8 @@ const userThree = { role: "buyer", status: "enabled", isVerified: true, -}; + is2FAEnabled: true +} const userFour = { id: userFourId, @@ -84,7 +87,8 @@ const userFour = { role: "seller", status: "enabled", isVerified: true, -}; + is2FAEnabled: false +} const userFive = { id: userFiveId, @@ -103,6 +107,7 @@ const userFive = { role: "seller", status: "enabled", isVerified: true, + is2FAEnabled: false }; const userSix = { @@ -122,6 +127,7 @@ const userSix = { role: "seller", status: "enabled", isVerified: true, + is2FAEnabled: false }; const userSeven = { @@ -141,6 +147,7 @@ const userSeven = { role: "seller", status: "enabled", isVerified: true, + is2FAEnabled: false }; export const up = (queryInterface: QueryInterface) => diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 01464234..63a644c4 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -22,4 +22,14 @@ const hashPassword = (password: string)=>{ return bcrypt.hashSync(password, 10); } -export { generateToken, decodeToken, comparePassword, hashPassword } \ No newline at end of file +const generateRandomCode = (): string => { + return Math.floor(100000 + Math.random() * 900000).toString(); +}; + +const generateOTP = () => { + const otp = generateRandomCode(); + const expirationTime = new Date(Date.now() + 5 * 60 * 1000); + return { otp, expirationTime }; +}; + + export { generateToken, decodeToken, comparePassword, hashPassword, generateRandomCode,generateOTP } \ No newline at end of file diff --git a/src/index.spec.ts b/src/index.spec.ts index 3eed0716..897fed98 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -13,7 +13,6 @@ import authRepositories from "./modules/auth/repository/authRepositories"; chai.use(chaiHttp); chai.use(sinonChai); - const router = () => chai.request(app); describe("Initial configuration", () => { diff --git a/src/index.ts b/src/index.ts index c239fe34..884c634a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,6 @@ import Document from "../swagger.json"; import router from "./routes"; import httpStatus from "http-status"; - dotenv.config(); const app: Express = express(); @@ -24,11 +23,14 @@ app.use("/api", router); app.get("**", (req: Request, res: Response) => { res .status(httpStatus.OK) - .json({ status: true, message: "Welcome to the e-Commerce Ninjas BackEnd." }); + .json({ + status: true, + message: "Welcome to the e-Commerce Ninjas BackEnd." + }); }); app.listen(PORT, () => { console.log(`Server is running on the port ${PORT}`); }); -export default app; \ No newline at end of file +export default app; diff --git a/src/middlewares/validation.ts b/src/middlewares/validation.ts index e2c47090..1026ecbc 100644 --- a/src/middlewares/validation.ts +++ b/src/middlewares/validation.ts @@ -7,11 +7,12 @@ import { NextFunction, Request, Response } from "express"; import authRepositories from "../modules/auth/repository/authRepositories"; import Users, { usersAttributes } from "../databases/models/users"; import httpStatus from "http-status"; -import { comparePassword, decodeToken, hashPassword } from "../helpers"; +import { comparePassword, decodeToken, generateOTP, generateRandomCode, hashPassword } from "../helpers"; import productRepositories from "../modules/product/repositories/productRepositories"; import Shops from "../databases/models/shops"; import Products from "../databases/models/products"; import { ExtendRequest } from "../types"; +import { sendEmail } from "../services/sendEmail"; const validation = (schema: Joi.ObjectSchema | Joi.ArraySchema) => @@ -150,58 +151,53 @@ const isAccountVerified = async ( } }; -const verifyUserCredentials = async ( - req: any, - res: Response, - next: NextFunction -) => { +const verifyUserCredentials = async (req: ExtendRequest, res: Response, next: NextFunction) => { try { - const user: usersAttributes = await authRepositories.findUserByAttributes( - "email", - req.body.email - ); + const user = await authRepositories.findUserByAttributes("email", req.body.email); if (!user) { - return res - .status(httpStatus.BAD_REQUEST) - .json({ message: "Invalid Email or Password" }); + return res.status(httpStatus.BAD_REQUEST).json({ message: "Invalid Email or Password" }); } + if (user.is2FAEnabled) { + const {otp,expirationTime} = generateOTP(); + const device: any = req.headers["user-device"] || null; + + const session = { + userId: user.id, + device, + otp: otp, + otpExpiration: expirationTime + }; + + await authRepositories.createSession(session); + await sendEmail(user.email, "E-Commerce Ninja Login", `Dear ${user.lastName || user.email}\n\nUse This Code To Confirm Your Account: ${otp}`); + + const isTokenExist = await authRepositories.findTokenByDeviceIdAndUserId(device, user.id); + if (isTokenExist) { + return res.status(httpStatus.OK).json({ + message: "Check your Email for OTP Confirmation", + UserId: { userId: user.id }, + data: { token: isTokenExist } + }); + } - const passwordMatches = await comparePassword( - req.body.password, - user.password - ); + return res.status(httpStatus.OK).json({ + message: "Check your Email for OTP Confirmation", + UserId: { userId: user.id } + }); + } + const passwordMatches = await comparePassword(req.body.password, user.password); if (!passwordMatches) { - return res - .status(httpStatus.BAD_REQUEST) - .json({ message: "Invalid Email or Password" }); + return res.status(httpStatus.BAD_REQUEST).json({ message: "Invalid Email or Password" }); } req.user = user; - - const device = req.headers["user-device"]; - 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(); - } + return next(); } catch (error) { - return res - .status(httpStatus.INTERNAL_SERVER_ERROR) - .json({ message: "Internal Server error", data: error.message }); + return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ message: "Internal Server error", data: error.message }); } }; + const verifyUser = async (req: any, res: Response, next: NextFunction) => { try { let user: any = null; @@ -210,97 +206,68 @@ const verifyUser = async (req: any, res: Response, next: NextFunction) => { user = await authRepositories.findUserByAttributes("id", decodedToken.id); } if (req?.body?.email) { - user = await authRepositories.findUserByAttributes( - "email", - req.body.email - ); + user = await authRepositories.findUserByAttributes("email", req.body.email); } if (!user) { - return res - .status(httpStatus.NOT_FOUND) - .json({ status: httpStatus.NOT_FOUND, message: "Account not found." }); + return res.status(httpStatus.NOT_FOUND).json({ status: httpStatus.NOT_FOUND, message: "Account not found." }); } if (!user.isVerified) { - return res.status(httpStatus.BAD_REQUEST).json({ - status: httpStatus.BAD_REQUEST, - message: "Account is not verified.", - }); + return res.status(httpStatus.BAD_REQUEST).json({ status: httpStatus.BAD_REQUEST, message: "Account is not verified." }); } req.user = user; next(); + } catch (error) { - return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ - status: httpStatus.INTERNAL_SERVER_ERROR, - message: error.message, - }); + return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ status: httpStatus.INTERNAL_SERVER_ERROR, message: error.message }); } }; const isSessionExist = async (req: any, res: Response, next: NextFunction) => { try { - const session = await authRepositories.findSessionByAttributes( - "userId", - req.user.id - ); + const session = await authRepositories.findSessionByAttributes("userId", req.user.id); if (!session) { - return res - .status(httpStatus.BAD_REQUEST) - .json({ status: httpStatus.BAD_REQUEST, message: "Invalid token." }); + return res.status(httpStatus.BAD_REQUEST).json({ status: httpStatus.BAD_REQUEST, message: "Invalid token." }); } - const destroy = await authRepositories.destroySession( - req.user.id, - session.token - ); + const destroy = await authRepositories.destroySessionByAttribute("userId", req.user.id, "token", session.token); if (destroy) { const hashedPassword = await hashPassword(req.body.newPassword); req.user.password = hashedPassword; - next(); + next() } + } catch (error) { - return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ - status: httpStatus.INTERNAL_SERVER_ERROR, - message: error.message, - }); + return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ status: httpStatus.INTERNAL_SERVER_ERROR, message: error.message }); } -}; +} -const isProductExist = async(req: any, res: Response, next: NextFunction) => { - try { - const shop = await productRepositories.findShopByAttributes(Shops,"userId", req.user.id); - if(!shop){ - return res.status(httpStatus.NOT_FOUND).json({ status: httpStatus.NOT_FOUND, message: "Not shop found." }); - } - const isProductAvailable = await productRepositories.findByModelsAndAttributes(Products,"name","shopId", req.body.name,shop.id); - if(isProductAvailable){ - return res.status(httpStatus.BAD_REQUEST).json({ status: httpStatus.BAD_REQUEST, message: "Please update the quantities." }); - } - req.shop = shop; - next(); - } catch (error) { - return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ status: httpStatus.INTERNAL_SERVER_ERROR, message: error.message }); +const isProductExist = async (req: any, res: Response, next: NextFunction) => { + try { + const shop = await productRepositories.findShopByAttributes(Shops, "userId", req.user.id); + if (!shop) { + return res.status(httpStatus.NOT_FOUND).json({ status: httpStatus.NOT_FOUND, message: "Not shop found." }); } + const isProductAvailable = await productRepositories.findByModelsAndAttributes(Products, "name", "shopId", req.body.name, shop.id); + if (isProductAvailable) { + return res.status(httpStatus.BAD_REQUEST).json({ status: httpStatus.BAD_REQUEST, message: "Please update the quantities." }); + } + req.shop = shop; + next(); + } catch (error) { + return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ status: httpStatus.INTERNAL_SERVER_ERROR, message: error.message }); + } } -const credential = async ( - req: ExtendRequest, - res: Response, - next: NextFunction -) => { +const credential = async (req: ExtendRequest, res: Response, next: NextFunction) => { try { let user: usersAttributes = null; if (req.user.id) { user = await authRepositories.findUserByAttributes("id", req.user.id); } - const compareUserPassword = await comparePassword( - req.body.oldPassword, - user.password - ); + const compareUserPassword = await comparePassword(req.body.oldPassword, user.password); if (!compareUserPassword) { - return res - .status(httpStatus.BAD_REQUEST) - .json({ status: httpStatus.BAD_REQUEST, message: "Invalid password." }); + return res.status(httpStatus.BAD_REQUEST).json({ status: httpStatus.BAD_REQUEST, message: "Invalid password." }); } const hashedPassword = await hashPassword(req.body.newPassword); @@ -308,61 +275,35 @@ const credential = async ( req.user = user; next(); } catch (error) { - return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ - status: httpStatus.INTERNAL_SERVER_ERROR, - message: error.message, - }); + return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ status: httpStatus.INTERNAL_SERVER_ERROR, message: error.message }); } }; + const isShopExist = async (req: any, res: Response, next: NextFunction) => { try { - const shop = await productRepositories.findShopByAttributes( - Shops, - "userId", - req.user.id - ); + const shop = await productRepositories.findShopByAttributes(Shops, "userId", req.user.id) if (shop) { - return res.status(httpStatus.BAD_REQUEST).json({ - status: httpStatus.BAD_REQUEST, - message: "Already have a shop.", - data: { shop: shop }, - }); + return res.status(httpStatus.BAD_REQUEST).json({ status: httpStatus.BAD_REQUEST, message: "Already have a shop.", data: { shop: shop } }); } return next(); } catch (error) { - return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ - status: httpStatus.INTERNAL_SERVER_ERROR, - message: error.message, - }); + return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ status: httpStatus.INTERNAL_SERVER_ERROR, message: error.message }); } -}; +} -const isSellerShopExist = async ( - req: any, - res: Response, - next: NextFunction -) => { +const isSellerShopExist = async (req: any, res: Response, next: NextFunction) => { try { - const shop = await productRepositories.findShopByAttributes( - Shops, - "userId", - req.user.id - ); + const shop = await productRepositories.findShopByAttributes(Shops, "userId", req.user.id) if (!shop) { - return res - .status(httpStatus.NOT_FOUND) - .json({ status: httpStatus.NOT_FOUND, message: "Shop not found" }); + return res.status(httpStatus.NOT_FOUND).json({ status: httpStatus.NOT_FOUND, message: "Shop not found" }); } req.shop = shop; return next(); } catch (error) { - return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ - status: httpStatus.INTERNAL_SERVER_ERROR, - message: error.message, - }); + return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ status: httpStatus.INTERNAL_SERVER_ERROR, message: error.message }); } -}; +} const transformFilesToBody = ( req: Request, @@ -380,57 +321,64 @@ const transformFilesToBody = ( next(); }; +const verifyOtp = async (req:ExtendRequest, res:Response, next:NextFunction) => { + try { + const user = await authRepositories.findUserByAttributes("id", req.params.id); + if (!user) { + return res.status(httpStatus.NOT_FOUND).json({ + status: httpStatus.NOT_FOUND, + message: "User not found." + }); + } + + const sessionData = await authRepositories.findSessionByUserIdOtp(user.id, req.body.otp); + + if (!sessionData || !sessionData.otp) { + return res.status(httpStatus.BAD_REQUEST).json({ + status: httpStatus.BAD_REQUEST, + message: "Invalid or expired code." + }); + } + + if (new Date() > sessionData.otpExpiration) { + await authRepositories.destroySessionByAttribute("userId", user.id, "otp", req.body.otp); + return res.status(httpStatus.BAD_REQUEST).json({ + status: httpStatus.BAD_REQUEST, + message: "OTP expired." + }); + } + await authRepositories.destroySessionByAttribute("userId", user.id, "otp", req.body.otp); + req.user = user; + next(); + } catch (error) { + res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ + status: httpStatus.INTERNAL_SERVER_ERROR, + message: error.message + }); + } +}; + + + const isUserVerified = async (req: any, res: Response, next: NextFunction) => { const user: usersAttributes = await authRepositories.findUserByAttributes( "email", req.body.email ); - if (!user) - return res - .status(httpStatus.BAD_REQUEST) - .json({ message: "Invalid Email or Password" }); - if (user.isVerified === false) - return res.status(httpStatus.UNAUTHORIZED).json({ - status: httpStatus.UNAUTHORIZED, - message: "Your account is not verified yet", - }); + if (!user) return res.status(httpStatus.BAD_REQUEST).json({ message: "Invalid Email or Password" }); + if (user.isVerified === false) return res.status(httpStatus.UNAUTHORIZED).json({ status: httpStatus.UNAUTHORIZED, message: "Your account is not verified yet" }) req.user = user; return next(); -}; +} const isUserEnabled = async (req: any, res: Response, next: NextFunction) => { - if (req.user.status !== "enabled") - return res.status(httpStatus.UNAUTHORIZED).json({ - status: httpStatus.UNAUTHORIZED, - message: "Your account is disabled", - }); + if (req.user.status !== "enabled") return res.status(httpStatus.UNAUTHORIZED).json({ status: httpStatus.UNAUTHORIZED, message: "Your account is disabled" }) return next(); -}; +} const isGoogleEnabled = async (req: any, res: Response, next: NextFunction) => { - if (req.user.isGoogleAccount) - return res.status(httpStatus.UNAUTHORIZED).json({ - status: httpStatus.UNAUTHORIZED, - message: "This is google account, please login with google", - }); + if (req.user.isGoogleAccount) return res.status(httpStatus.UNAUTHORIZED).json({ status: httpStatus.UNAUTHORIZED, message: "This is google account, please login with google" }) return next(); -}; - -export { - validation, - isUserExist, - isAccountVerified, - verifyUserCredentials, - isUsersExist, - isProductExist, - isShopExist, - transformFilesToBody, - credential, - isSessionExist, - verifyUser, - isGoogleEnabled, - isUserEnabled, - isUserVerified, - isSellerShopExist, -}; +} +export { validation, isUserExist, isAccountVerified, verifyUserCredentials, isUsersExist, isProductExist, isShopExist, transformFilesToBody, credential, isSessionExist, verifyUser, isGoogleEnabled, isUserEnabled, isUserVerified, isSellerShopExist, verifyOtp }; \ No newline at end of file diff --git a/src/modules/auth/controller/authControllers.ts b/src/modules/auth/controller/authControllers.ts index 23638a94..b84af734 100644 --- a/src/modules/auth/controller/authControllers.ts +++ b/src/modules/auth/controller/authControllers.ts @@ -59,7 +59,12 @@ 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.destroySessionByAttribute( + "userId", + req.user.id, + "token", + 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) { @@ -67,7 +72,6 @@ const verifyEmail = async (req: any, res: Response) => { } } - const loginUser = async (req: any, res: Response) => { try { const token = generateToken(req.user.id); @@ -90,8 +94,13 @@ const loginUser = async (req: any, res: Response) => { const logoutUser = async (req: any, res: Response) => { try { - await authRepositories.destroySession(req.user.id, req.session.token) - res.status(httpStatus.OK).json({status: httpStatus.OK, message: "Successfully logged out" }); + await authRepositories.destroySessionByAttribute( + "userId", + req.user.id, + "token", + req.session.token + ); + res.status(httpStatus.OK).json({ message: "Successfully logged out" }); } catch (err) { return res .status(httpStatus.INTERNAL_SERVER_ERROR) @@ -127,6 +136,26 @@ const resetPassword = async (req: any, res: Response): Promise => { } }; +const updateUser2FA = async (req: any, res: Response) => { + try { + const user = await authRepositories.updateUserByAttributes( + "is2FAEnabled", + true, + "id", + req.user.id + ); + res.status(httpStatus.OK).json({ + status: httpStatus.OK, + message: "2FA enabled successfully.", + data: { user: user } + }); + } catch (error) { + return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ + status: httpStatus.INTERNAL_SERVER_ERROR, + message: error.message + }); + } +}; export default { registerUser, sendVerifyEmail, @@ -134,5 +163,6 @@ export default { loginUser, forgetPassword, resetPassword, - logoutUser + logoutUser, + updateUser2FA }; \ No newline at end of file diff --git a/src/modules/auth/repository/authRepositories.ts b/src/modules/auth/repository/authRepositories.ts index 7e9fbeba..d51dfd34 100644 --- a/src/modules/auth/repository/authRepositories.ts +++ b/src/modules/auth/repository/authRepositories.ts @@ -2,6 +2,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import Users from "../../../databases/models/users"; import Session from "../../../databases/models/session"; +import { Op } from "sequelize"; const createUser = async (body: any) => { return await Users.create({ ...body, role:"buyer" }); @@ -41,17 +42,36 @@ const findTokenByDeviceIdAndUserId = async (device: string, userId: string)=>{ return session.token; } -const destroySession = async (userId: string, token:string) =>{ - return await Session.destroy({ where: {userId, token } }); -} +const destroySessionByAttribute = async ( + destroyKey: string, + destroyValue: any, + key: string, + value: string +) => { + return await Session.destroy({ + where: { [destroyKey]: destroyValue, [key]: value }, + }); +}; + +const findSessionByUserIdOtp = async (userId:string, otp:string) => { + return await Session.findOne({ + where: { + userId: userId, + otp: otp, + otpExpiration: { [Op.gt]: new Date() } + } + }); + } + export default { createUser, createSession, findUserByAttributes, - destroySession, - updateUserByAttributes, findSessionByAttributes, findSessionByUserIdAndToken, findTokenByDeviceIdAndUserId, + updateUserByAttributes, + destroySessionByAttribute, + findSessionByUserIdOtp }; \ 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 96024e56..d39a65ab 100644 --- a/src/modules/auth/test/auth.spec.ts +++ b/src/modules/auth/test/auth.spec.ts @@ -7,7 +7,7 @@ import chaiHttp from "chai-http"; import sinon from "sinon"; import httpStatus from "http-status"; import app from "../../.."; -import { isSessionExist, isUserExist, verifyUser, verifyUserCredentials } from "../../../middlewares/validation"; +import { isSessionExist, isUserExist, verifyOtp, verifyUser, verifyUserCredentials } from "../../../middlewares/validation"; import authRepositories from "../repository/authRepositories"; import Users from "../../../databases/models/users"; import Session from "../../../databases/models/session"; @@ -19,12 +19,14 @@ import googleAuth from "../../../services/googleAuth"; import { VerifyCallback } from "jsonwebtoken"; import passport from "passport"; import authControllers from "../controller/authControllers"; +import * as helpers from "../../../helpers" chai.use(chaiHttp); const router = () => chai.request(app); let userId: string; let verifyToken: string | null = null; +let otp: string | null = null; describe("Authentication Test Cases", () => { let token; @@ -185,7 +187,7 @@ describe("Authentication Test Cases", () => { it("Should return error on logout", (done) => { sinon - .stub(authRepositories, "destroySession") + .stub(authRepositories, "destroySessionByAttribute") .throws(new Error("Database Error")); router() .post("/api/auth/logout") @@ -507,52 +509,6 @@ describe("sendVerificationEmail", () => { } }); }); -describe("verifyUserCredentials Middleware", () => { - let req: Partial; - let res: Partial; - let next: NextFunction; - - beforeEach(() => { - req = { - body: { - email: "user@example.com", - password: "Password@123" - }, - headers: {} - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub() - }; - }); - - afterEach(() => { - sinon.restore(); - }); - - it("should return 400 if the user is not found", async () => { - sinon.stub(authRepositories, "findUserByAttributes").resolves(null); - - await verifyUserCredentials(req as Request, res as Response, next); - - expect(res.status).to.have.been.calledWith(httpStatus.BAD_REQUEST); - expect(res.json).to.have.been.calledWith({ - message: "Invalid Email or Password" - }); - }); - - it("should return 500 if there is an internal server error", async () => { - sinon.stub(authRepositories, "findUserByAttributes").throws(new Error("Internal Server Error")); - - await verifyUserCredentials(req as Request, res as Response, next); - - expect(res.status).to.have.been.calledWith(httpStatus.INTERNAL_SERVER_ERROR); - expect(res.json).to.have.been.calledWith({ - message: "Internal Server error", - data: "Internal Server Error" - }); - }); -}); describe("Passport Configuration", () => { it("should serialize and deserialize user correctly", () => { @@ -874,7 +830,7 @@ describe("Google Authentication", () => { } as Partial; const error = new Error("Unexpected error"); - sinon.stub(authRepositories, "destroySession").throws(error); + sinon.stub(authRepositories, "destroySessionByAttribute").throws(error); await authControllers.verifyEmail(req as Request, res as Response); @@ -937,4 +893,227 @@ describe("Google Authentication", () => { sinon.restore(); }); - }); \ No newline at end of file + }); + + describe("updateUser2FA", () => { + let req; + let res; + let token: string = null; + before((done) => { + router() + .post("/api/auth/login") + .send({ + email: "buyer@gmail.com", + password:"Password@123" + }) + .end((error, response) => { + token = response.body.data.token; + done(error); + }); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should enable 2FA for the user and return success message", (done) => { + router() + .put("/api/auth/enable-2f") + .set("Authorization", `Bearer ${token}`) + .send({ is2FAEnabled: true }) + .end((error, response) => { + expect(response.body).to.have.property("status", httpStatus.OK); + expect(response.body).to.have.property( + "message", + "2FA enabled successfully." + ); + expect(response.body).to.have.property("data"); + done(error); + }); + }); + + it("should return internal server error message if updating 2FA fails", (done) => { + const errorMessage = "Failed to enable 2FA"; + sinon + .stub(authRepositories, "updateUserByAttributes") + .throws(new Error(errorMessage)); + router() + .put("/api/auth/enable-2f") + .set("Authorization", `Bearer ${token}`) + .send({ is2FAEnabled: true }) + .end((error, response) => { + expect(response.body).to.have.property( + "status", + httpStatus.INTERNAL_SERVER_ERROR + ); + expect(response.body).to.have.property("message", errorMessage); + done(error); + }); + }); + }); + + describe("verifyUserCredentials Middleware", () => { + let req; + let res; + let next; + + beforeEach(() => { + req = { + body: { + email: "user@example.com", + password: "Password@123" + }, + headers: {} + }; + res = { + status: sinon.stub().returnsThis(), + json: sinon.stub() + }; + next = sinon.stub(); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should return 400 if the user is not found", (done) => { + sinon.stub(authRepositories, "findUserByAttributes").resolves(null); + router() + .post("/api/auth/login") + .send({ + email: "example@gmail.com", + password:"Password@123" + }) + .end((error, response) => { + expect(response.status).to.equal(400); + expect(response.body.message).to.equal("Invalid Email or Password"); + done(error) + }) + }); + + it("should return 400 if the password does not match", async () => { + const user: any = { + id: 1, + email: req.body.email, + password: "hashedPassword", + is2FAEnabled: false + }; + sinon.stub(authRepositories, "findUserByAttributes").resolves(user); + sinon.stub(helpers, "comparePassword").resolves(false); + + await verifyUserCredentials(req, res, next); + + expect(res.status).to.have.been.calledWith(httpStatus.BAD_REQUEST); + expect(res.json).to.have.been.calledWith({ + message: "Invalid Email or Password" + }); + expect(next).not.to.have.been.called; + }); + + it("should send OTP email if 2FA is enabled", (done) => { + router() + .post("/api/auth/login") + .send({ + email: "buyer@gmail.com", + password:"Password@123" + }) + .end((error, response) => { + expect(response.status).to.equal(httpStatus.OK); + expect(response.body.message).to.equal("Check your Email for OTP Confirmation"); + userId = response.body.UserId.userId + done(error) + }) + }); + +}) + +describe("verifyOtp", () => { + const validUUID = "123e4567-e89b-12d3-a456-426614174000"; + let findUserStub, findSessionStub, destroySessionStub; + afterEach(async() => { + if (findUserStub) findUserStub.restore(); + if (findSessionStub) findSessionStub.restore(); + if (destroySessionStub) destroySessionStub.restore(); + const otpRecord = await Session.findOne({ where: {userId}}) + if (otpRecord) { + otp=otpRecord.dataValues.otp + } + }); + it("should send otp when user is has enabled 2FA", (done) => { + router() + .post("/api/auth/login") + .send({ + email: "buyer1@gmail.com", + password:"Password@123" + }) + .end((error, response) => { + expect(response.status).to.equal(httpStatus.OK); + expect(response.body.message).to.equal("Check your Email for OTP Confirmation"); + userId = response.body.UserId.userId + done(error); + }); + }) + it("should return 400 if sessionData is null or has no OTP", async () => { + const user = Users.build({ id: validUUID }); + findUserStub = sinon.stub(authRepositories, "findUserByAttributes").resolves(user); + findSessionStub = sinon.stub(authRepositories, "findSessionByUserIdOtp").resolves(null); // Simulate no session data + + const res = await chai.request(app) + .post(`/api/auth/verify-otp/${validUUID}`) + .send({ otp: "123456" }); + + expect(res).to.have.status(httpStatus.BAD_REQUEST); + expect(res.body.message).to.equal("Invalid or expired code."); + }); + + it("should return 400 if OTP is expired", async () => { + const user = Users.build({ id: validUUID }); + const sessionData = Session.build({ otp: "123456", otpExpiration: new Date(Date.now() - 1000) }); + findUserStub = sinon.stub(authRepositories, "findUserByAttributes").resolves(user); + findSessionStub = sinon.stub(authRepositories, "findSessionByUserIdOtp").resolves(sessionData); + destroySessionStub = sinon.stub(authRepositories, "destroySessionByAttribute").resolves(); + + const res = await chai.request(app) + .post(`/api/auth/verify-otp/${validUUID}`) + .send({ otp: "123456" }); + expect(res).to.have.status(httpStatus.BAD_REQUEST); + expect(res.body.message).to.equal("OTP expired."); + }); + + it("should return 200 and proceed if OTP is valid and not expired", (done) => { + + router() + .post(`/api/auth/verify-otp/${userId}`) + .send({ otp: otp }) + .end((error, response)=>{ + expect(response.status).to.equal(httpStatus.OK); + expect(response.body).to.be.an("object"); + done(error); + }) + }); + + + it("should return 404 if user is not found", async () => { + findUserStub = sinon.stub(authRepositories, "findUserByAttributes").resolves(null); + + const res = await chai.request(app) + .post(`/api/auth/verify-otp/${validUUID}`) + .send({ otp: "123456" }); + + expect(res).to.have.status(httpStatus.NOT_FOUND); + expect(res.body.message).to.equal("User not found."); + + }); + + it("should return 500 if there is a server error", async () => { + findUserStub = sinon.stub(authRepositories, "findUserByAttributes").rejects(new Error("Internal Server Error")); + + const res = await chai.request(app) + .post(`/api/auth/verify-otp/${validUUID}`) + .send({ otp: "123456" }); + + expect(res).to.have.status(httpStatus.INTERNAL_SERVER_ERROR); + expect(res.body.message).to.equal("Internal Server Error"); + + }); +}); \ No newline at end of file diff --git a/src/modules/auth/validation/authValidations.ts b/src/modules/auth/validation/authValidations.ts index 39741d63..470afcef 100644 --- a/src/modules/auth/validation/authValidations.ts +++ b/src/modules/auth/validation/authValidations.ts @@ -1,8 +1,8 @@ import Joi from "joi"; interface User { - email: string; - password: string; + email: string; + password: string; } const credentialSchema = Joi.object({ @@ -22,15 +22,29 @@ const credentialSchema = Joi.object({ }); const emailSchema = Joi.object({ - email: Joi.string().email().required().messages({ - "string.base": "email should be a type of text", - "string.email": "email must be a valid email", - "string.empty": "email cannot be an empty field", - "any.required": "email is required" - }) + email: Joi.string().email().required().messages({ + "string.base": "email should be a type of text", + "string.email": "email must be a valid email", + "string.empty": "email cannot be an empty field", + "any.required": "email is required" + }) +}); +const otpSchema = Joi.object({ + otp: Joi.number().integer().required().messages({ + "number.base": "OTP must be a 6-digit number", + "number.empty": "OTP cannot be an empty field", + "any.required": "OTP is required" + }) }); +const is2FAenabledSchema = Joi.object({ + is2FAEnabled: Joi.boolean().required().messages({ + "boolean.base": "2FAenabled must be a boolean", + "boolean.empty": "2FAenabled cannot be an empty field", + "any.required": "2FAenabled is required" + }) +}); const resetPasswordSchema = Joi.object({ newPassword: Joi.string().min(8).pattern(new RegExp("^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^a-zA-Z0-9]).{8,}$")).required().messages({ @@ -42,6 +56,4 @@ const resetPasswordSchema = Joi.object({ }) }); - - -export { credentialSchema, emailSchema, resetPasswordSchema }; \ No newline at end of file +export { credentialSchema, emailSchema, otpSchema, is2FAenabledSchema,resetPasswordSchema }; \ No newline at end of file diff --git a/src/modules/user/test/user.spec.ts b/src/modules/user/test/user.spec.ts index 50cb1eed..78829ea9 100644 --- a/src/modules/user/test/user.spec.ts +++ b/src/modules/user/test/user.spec.ts @@ -221,7 +221,7 @@ describe("Admin update User roles", () => { }); }); - it("Should be able to login a registered user", (done) => { + it("Should login admin", (done) => { router() .post("/api/auth/login") .send({ @@ -239,13 +239,17 @@ describe("Admin update User roles", () => { }); }); - it("Should notify if no role is specified", async () => { - const response = await router() - .put(`/api/user/admin-update-role/${userIdd}`) - .set("authorization", `Bearer ${token}`); + it("Should notify if no role is specified", (done) => { - expect(response.status).to.equal(httpStatus.BAD_REQUEST); - expect(response.body).to.have.property("message"); + router() + .put(`/api/user/admin-update-role/${userIdd}`) + .set("authorization", `Bearer ${token}`) + .end((error, response) => { + expect(response.status).to.equal(httpStatus.BAD_REQUEST); + expect(response.body).to.be.an("object"); + expect(response.body).to.have.property("message", "The 'role' parameter is required."); + done(error); + }); }); it("Should notify if the role is other than ['admin', 'buyer', 'seller']", async () => { diff --git a/src/modules/user/validation/userValidations.ts b/src/modules/user/validation/userValidations.ts index a64f4024..4670f291 100644 --- a/src/modules/user/validation/userValidations.ts +++ b/src/modules/user/validation/userValidations.ts @@ -20,7 +20,6 @@ export const statusSchema = Joi.object({ }) }); - export const roleSchema = Joi.object({ role: Joi.string().valid("admin", "buyer", "seller").required().messages({ "any.required": "The 'role' parameter is required.", diff --git a/src/routes/authRouter.ts b/src/routes/authRouter.ts index a0ce83b9..dbd1237f 100644 --- a/src/routes/authRouter.ts +++ b/src/routes/authRouter.ts @@ -4,22 +4,24 @@ import { validation, isUserExist, isAccountVerified, - verifyUserCredentials, + isUserEnabled, verifyUser, isSessionExist, isGoogleEnabled, - isUserEnabled, - isUserVerified + isUserVerified, + verifyOtp, + verifyUserCredentials } from "../middlewares/validation"; import { emailSchema, credentialSchema, + otpSchema, + is2FAenabledSchema, resetPasswordSchema } from "../modules/auth/validation/authValidations"; import { userAuthorization } from "../middlewares/authorization"; import googleAuth from "../services/googleAuth"; - const router: Router = Router(); router.post( @@ -56,11 +58,21 @@ router.post( ); router.get("/google", googleAuth.googleVerify); -router.get( - "/google/callback", - googleAuth.authenticateWithGoogle); +router.get("/google/callback", googleAuth.authenticateWithGoogle); +router.post( + "/verify-otp/:id", + validation(otpSchema), + verifyOtp, + authControllers.loginUser +); +router.put( + "/enable-2f", + validation(is2FAenabledSchema), + userAuthorization(["admin", "buyer", "seller"]), + authControllers.updateUser2FA +); router.post("/forget-password", validation(emailSchema), verifyUser, authControllers.forgetPassword); router.put("/reset-password/:token", validation(resetPasswordSchema), verifyUser, isSessionExist, authControllers.resetPassword); -export default router; \ No newline at end of file +export default router; diff --git a/src/routes/index.ts b/src/routes/index.ts index ddf502f1..f0781a1e 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -3,6 +3,7 @@ import authRouter from "./authRouter"; import userRouter from "./userRouter"; import productRouter from "./productRouter"; + const router: Router = Router(); router.use("/auth", authRouter); diff --git a/src/services/sendEmail.ts b/src/services/sendEmail.ts index dec768cf..a4567220 100644 --- a/src/services/sendEmail.ts +++ b/src/services/sendEmail.ts @@ -5,13 +5,13 @@ import dotenv from "dotenv"; dotenv.config(); const transporter = nodemailer.createTransport({ - host: process.env.SMTP_HOST, - port: Number(process.env.SMTP_HOST_PORT), - secure: true, - auth: { - user: process.env.MAIL_ID, - pass: process.env.MP - } + host: process.env.SMTP_HOST, + port: Number(process.env.SMTP_HOST_PORT), + secure: true, + auth: { + user: process.env.MAIL_ID, + pass: process.env.MP + } }); const sendEmail = async(email: string, subject: string, message: string) => { diff --git a/swagger.json b/swagger.json index 045d2001..3c44e96e 100644 --- a/swagger.json +++ b/swagger.json @@ -523,9 +523,305 @@ } } }, + "/api/auth/enable-2f": { + "put": { + "tags": [ + "Authentication Routes" + ], + "summary": "Allow User to Enable 2FA", + "description": "Allow User to Enable 2FA", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "is2FAEnabled": { + "type": "boolean", + "format": "is2FAEnabled", + "example": true + } + }, + "required": [ + "is2FAEnabled" + ] + } + } + } + }, + "responses": { + "200": { + "description": "2FA Enabled successful" + }, + "400": { + "description": "2FA Enabled Failed / Bad request" + } + } + } + }, + "/api/auth/verify-otp/{id}": { + "post": { + "tags": [ + "Authentication Routes" + ], + "summary": "Verify The OTP Sent", + "description": "The Verification of OTP Sent to SMS ", + "parameters": [ + { + "in": "path", + "name": "id", + "type": "string", + "description": "Pass the ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "otp": { + "type": "string", + "format": "otp", + "example": "otp" + } + }, + "required": [ + "OTP" + ] + } + } + } + }, + "responses": { + "200": { + "description": "Verification of OTP successful" + }, + "400": { + "description": "Invalid Code / Bad request" + } + } + } + }, + "/api/auth/forget-password": { + "post": { + "tags": [ + "Authentication Routes" + ], + "summary": "Request password reset", + "description": "This endpoint allows a user to request a password reset link by providing their email address.", + "requestBody": { + "description": "Email address to request password reset", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email" + } + }, + "required": [ + "email" + ] + } + } + } + }, + "responses": { + "200": { + "description": "Password reset email sent successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "integer" + }, + "error": { + "type": "string" + } + } + } + } + } + }, + "404": { + "description": "User not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "integer" + }, + "error": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/api/auth/reset-password/{token}": { + "post": { + "summary": "Reset password", + "description": "Resets the user's password using the provided token and new password.", + "tags": [ + "Authentication Routes" + ], + "parameters": [ + { + "in": "path", + "name": "token", + "description": "Token received in the password reset email", + "required": true, + "type": "string" + } + ], + "requestBody": { + "description": "New password details", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "newPassword": { + "type": "string", + "format": "password", + "description": "The new password" + } + }, + "required": [ + "newPassword" + ] + } + } + } + }, + "responses": { + "200": { + "description": "Password reset successfully" + }, + "400": { + "description": "bad request" + }, + "500": { + "description": "Expired token or internal server error" + } + } + } + }, + "/api/user/change-password": { + "put": { + "summary": "User update password", + "description": "Updates the password for a user with the specified ID.", + "tags": [ + "User Routes" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "oldPassword": { + "type": "string", + "description": "Old password of the user" + }, + "newPassword": { + "type": "string", + "description": "New password of the user. It must be at least 8 characters long and contain both letters and numbers." + }, + "confirmPassword": { + "type": "string", + "description": "Confirmation of the new password. It must match the new password." + } + }, + "required": [ + "oldPassword", + "newPassword", + "confirmPassword" + ] + } + } + } + }, + "responses": { + "200": { + "description": "Password changed successfully" + }, + "400": { + "description": "Bad request. Invalid input or passwords do not match." + }, + "401": { + "description": "Unauthorized. Old password is incorrect." + }, + "404": { + "description": "User not found." + }, + "500": { + "description": "Internal server error" + } + } + } + }, "/api/user/user-get-profile": { "get": { - "tags": ["User Routes"], + "tags": [ + "User Routes" + ], "security": [ { "bearerAuth": [] @@ -1021,7 +1317,7 @@ } } }, - "/api/user/admin-update-role/{id}": { + "/api/user/admin-update-user-role/{id}": { "put": { "tags": ["Admin Routes"], "summary": "Update user role", @@ -1696,202 +1992,11 @@ } } }, - "/api/auth/forget-password": { - "post": { - "tags": ["Authentication Routes"], - "summary": "Request password reset", - "description": "This endpoint allows a user to request a password reset link by providing their email address.", - "requestBody": { - "description": "Email address to request password reset", - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "email": { - "type": "string", - "format": "email" - } - }, - "required": ["email"] - } - } - } - }, - "responses": { - "200": { - "description": "Password reset email sent successfully", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - } - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { - "type": "integer" - }, - "error": { - "type": "string" - } - } - } - } - } - }, - "404": { - "description": "User not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { - "type": "integer" - }, - "error": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "/api/auth/reset-password/{token}": { - "post": { - "summary": "Reset password", - "description": "Resets the user's password using the provided token and new password.", - "tags": ["Authentication Routes"], - "parameters": [ - { - "in": "path", - "name": "token", - "description": "Token received in the password reset email", - "required": true, - "type": "string" - } - ], - "requestBody": { - "description": "New password details", - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "newPassword": { - "type": "string", - "format": "password", - "description": "The new password" - } - }, - "required": ["newPassword"] - } - } - } - }, - "responses": { - "200": { - "description": "Password reset successfully" - }, - "400": { - "description": "bad request" - }, - "500": { - "description": "Expired token or internal server error" - } - } - } - }, - "/api/user/change-password": { - "put": { - "summary": "User update password", - "description": "Updates the password for a user with the specified ID.", - "tags": ["User Routes"], - "security": [ - { - "bearerAuth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "oldPassword": { - "type": "string", - "description": "Old password of the user" - }, - "newPassword": { - "type": "string", - "description": "New password of the user. It must be at least 8 characters long and contain both letters and numbers." - }, - "confirmPassword": { - "type": "string", - "description": "Confirmation of the new password. It must match the new password." - } - }, - "required": ["oldPassword", "newPassword", "confirmPassword"] - } - } - } - }, - "responses": { - "200": { - "description": "Password changed successfully" - }, - "400": { - "description": "Bad request. Invalid input or passwords do not match." - }, - "401": { - "description": "Unauthorized. Old password is incorrect." - }, - "404": { - "description": "User not found." - }, - "500": { - "description": "Internal server error" - } - } - } - }, "/api/shop/seller-statistics": { "post": { - "tags": ["Seller Routes"], + "tags": [ + "Seller Routes" + ], "summary": "Seller Statistics", "description": "View Seller's revenue, orders and best selling products in specific timeframe.", "security": [ @@ -1917,7 +2022,10 @@ "example": "2024-12-31" } }, - "required": ["startDate", "endDate"] + "required": [ + "startDate", + "endDate" + ] } } }