diff --git a/controllers/stocks.js b/controllers/stocks.js index 1f914098b..5913af001 100644 --- a/controllers/stocks.js +++ b/controllers/stocks.js @@ -44,10 +44,42 @@ const fetchStocks = async (req, res) => { * @param req {Object} - Express request object * @param res {Object} - Express response object */ +/** + * @deprecated + * WARNING: This API endpoint is being deprecated and will be removed in future. + * Please use the updated API endpoint: `/stocks/:userId` for retrieving user stocks details. + * + * This API is kept temporarily for backward compatibility. + */ const getSelfStocks = async (req, res) => { try { const { id: userId } = req.userData; const userStocks = await stocks.fetchUserStocks(userId); + + res.set( + "X-Deprecation-Warning", + "WARNING: This endpoint is being deprecated and will be removed in the future. Please use `/stocks/:userId` route to get the user stocks details." + ); + return res.json({ + message: userStocks.length > 0 ? "User stocks returned successfully!" : "No stocks found", + userStocks, + }); + } catch (err) { + logger.error(`Error while getting user stocks ${err}`); + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); + } +}; + +/** + * Fetches all the stocks of the authenticated user + * + * @param req {Object} - Express request object + * @param res {Object} - Express response object + */ +const getUserStocks = async (req, res) => { + try { + const userStocks = await stocks.fetchUserStocks(req.params.userId); + return res.json({ message: userStocks.length > 0 ? "User stocks returned successfully!" : "No stocks found", userStocks, @@ -62,4 +94,5 @@ module.exports = { addNewStock, fetchStocks, getSelfStocks, + getUserStocks, }; diff --git a/controllers/users.js b/controllers/users.js index 6c8cb3a0d..88abb1eda 100644 --- a/controllers/users.js +++ b/controllers/users.js @@ -39,11 +39,12 @@ const discordDeveloperRoleId = config.get("discordDeveloperRoleId"); const verifyUser = async (req, res) => { const userId = req.userData.id; + const devFeatureFlag = req.query.dev === "true"; try { if (!req.userData?.profileURL) { return res.boom.serverUnavailable("ProfileURL is Missing"); } - await userQuery.addOrUpdate({ profileStatus: "PENDING" }, userId); + await userQuery.addOrUpdate({ profileStatus: "PENDING" }, userId, devFeatureFlag); } catch (error) { logger.error(`Error while verifying user: ${error}`); return res.boom.serverUnavailable(SOMETHING_WENT_WRONG); @@ -500,7 +501,7 @@ const updateSelf = async (req, res) => { const { roles } = discordMember; if (roles && roles.includes(discordDeveloperRoleId)) { if (req.body.disabledRoles && devFeatureFlag) { - const updatedUser = await userQuery.addOrUpdate({ disabled_roles: rolesToDisable }, userId); + const updatedUser = await userQuery.addOrUpdate({ disabled_roles: rolesToDisable }, userId, devFeatureFlag); if (updatedUser) { return res .status(200) @@ -514,7 +515,7 @@ const updateSelf = async (req, res) => { } } - const updatedUser = await userQuery.addOrUpdate(req.body, userId); + const updatedUser = await userQuery.addOrUpdate(req.body, userId, devFeatureFlag); if (!updatedUser.isNewUser) { // Success criteria, user finished the sign-up process. @@ -711,9 +712,9 @@ const getUserImageForVerification = async (req, res) => { const updateUser = async (req, res) => { try { const { id: profileDiffId, message } = req.body; - const devFeatureFlag = req.query.dev; + const devFeatureFlag = req.query.dev === "true"; let profileDiffData; - if (devFeatureFlag === "true") { + if (devFeatureFlag) { profileDiffData = await profileDiffsQuery.fetchProfileDiffUnobfuscated(profileDiffId); } else { profileDiffData = await profileDiffsQuery.fetchProfileDiff(profileDiffId); @@ -728,7 +729,7 @@ const updateUser = async (req, res) => { await profileDiffsQuery.updateProfileDiff({ approval: profileDiffStatus.APPROVED }, profileDiffId); - await userQuery.addOrUpdate(profileDiff, userId); + await userQuery.addOrUpdate(profileDiff, userId, devFeatureFlag); const meta = { approvedBy: req.userData.id, @@ -749,9 +750,9 @@ const updateUser = async (req, res) => { const generateChaincode = async (req, res) => { try { const { id } = req.userData; - + const devFeatureFlag = req.query.dev === "true"; const chaincode = await chaincodeQuery.storeChaincode(id); - await userQuery.addOrUpdate({ chaincode }, id); + await userQuery.addOrUpdate({ chaincode }, id, devFeatureFlag); return res.json({ chaincode, message: "Chaincode returned successfully", @@ -766,7 +767,8 @@ const profileURL = async (req, res) => { try { const userId = req.userData.id; const { profileURL } = req.body; - await userQuery.addOrUpdate({ profileURL }, userId); + const devFeatureFlag = req.query.dev === "true"; + await userQuery.addOrUpdate({ profileURL }, userId, devFeatureFlag); return res.json({ message: "updated profile URL!!", }); @@ -958,6 +960,7 @@ const setInDiscordScript = async (req, res) => { const updateRoles = async (req, res) => { try { const result = await dataAccess.retrieveUsers({ id: req.params.id }); + const devFeatureFlag = req.query.dev === "true"; if (result?.userExists) { const dataToUpdate = req.body; const roles = req?.userData?.roles; @@ -966,7 +969,7 @@ const updateRoles = async (req, res) => { const response = await getRoleToUpdate(result.user, dataToUpdate); if (response.updateRole) { - await userQuery.addOrUpdate(response.newUserRoles, result.user.id); + await userQuery.addOrUpdate(response.newUserRoles, result.user.id, devFeatureFlag); if (dataToUpdate?.archived) { const body = { reason: reason || "", diff --git a/middlewares/userAuthorization.ts b/middlewares/userAuthorization.ts new file mode 100644 index 000000000..376f2dda2 --- /dev/null +++ b/middlewares/userAuthorization.ts @@ -0,0 +1,9 @@ +import { NextFunction } from "express"; +import { CustomRequest, CustomResponse } from "../types/global"; + +export const userAuthorization = (req: CustomRequest, res: CustomResponse, next: NextFunction) => { + if (req.params.userId === req.userData.id) { + return next(); + } + res.boom.forbidden("Unauthorized access"); +}; diff --git a/models/stocks.js b/models/stocks.js index 9726af2df..6ab0db2cf 100644 --- a/models/stocks.js +++ b/models/stocks.js @@ -6,7 +6,7 @@ const userStocksModel = firestore.collection("user-stocks"); * Adds Stocks * * @param stockData { Object }: stock data object to be stored in DB - * @return {Promise<{stockId: string}>} + * @return {Promise<{id: string, stockData: Object}>} */ const addStock = async (stockData) => { try { diff --git a/models/users.js b/models/users.js index ce840a354..7e2514b38 100644 --- a/models/users.js +++ b/models/users.js @@ -84,7 +84,7 @@ const archiveUsers = async (usersData) => { * @param userId { String }: User Id String to be used to update the user * @return {Promise<{isNewUser: boolean, userId: string}|{isNewUser: boolean, userId: string}>} */ -const addOrUpdate = async (userData, userId = null) => { +const addOrUpdate = async (userData, userId = null, devFeatureFlag) => { try { // userId exists Update user if (userId !== null) { @@ -96,14 +96,24 @@ const addOrUpdate = async (userData, userId = null) => { if ("id" in userData) { delete userData.id; } - await userModel.doc(userId).set( - { - ...user.data(), - ...userData, - updated_at: Date.now(), - }, - { merge: true } - ); + if (devFeatureFlag) { + await userModel.doc(userId).set( + { + ...userData, + updated_at: Date.now(), + }, + { merge: true } + ); + } else { + await userModel.doc(userId).set( + { + ...user.data(), + ...userData, + updated_at: Date.now(), + }, + { merge: true } + ); + } const logData = { type: logType.USER_DETAILS_UPDATED, diff --git a/routes/stocks.js b/routes/stocks.js index f9818f357..7b83cb0b1 100644 --- a/routes/stocks.js +++ b/routes/stocks.js @@ -2,12 +2,15 @@ const express = require("express"); const router = express.Router(); const authenticate = require("../middlewares/authenticate"); const authorizeRoles = require("../middlewares/authorizeRoles"); -const { addNewStock, fetchStocks, getSelfStocks } = require("../controllers/stocks"); +const { addNewStock, fetchStocks, getSelfStocks, getUserStocks } = require("../controllers/stocks"); const { createStock } = require("../middlewares/validators/stocks"); const { SUPERUSER } = require("../constants/roles"); +const { devFlagMiddleware } = require("../middlewares/devFlag"); +const { userAuthorization } = require("../middlewares/userAuthorization"); router.get("/", fetchStocks); router.post("/", authenticate, authorizeRoles([SUPERUSER]), createStock, addNewStock); -router.get("/user/self", authenticate, getSelfStocks); +router.get("/user/self", authenticate, getSelfStocks); // this route will soon be deprecated, please use `/stocks/:userId` route. +router.get("/:userId", devFlagMiddleware, authenticate, userAuthorization, getUserStocks); module.exports = router; diff --git a/test/integration/stocks.test.ts b/test/integration/stocks.test.ts new file mode 100644 index 000000000..2bffd5dfd --- /dev/null +++ b/test/integration/stocks.test.ts @@ -0,0 +1,97 @@ +import chai from "chai"; +import chaiHttp from "chai-http"; +import app from "../../server"; +import authService from "../../services/authService"; +import addUser from "../utils/addUser"; +import cleanDb from "../utils/cleanDb"; +import stocks from "../../models/stocks"; +import sinon from "sinon"; +import config from "config"; + +const cookieName: string = config.get("userToken.cookieName"); +chai.use(chaiHttp); +const { expect } = chai; + +describe("GET /stocks/:userId", function () { + let jwt: string; + let userId: string; + let userStock; + const stockData = { name: "EURO", quantity: 2, price: 10 }; + + beforeEach(async function () { + userId = await addUser(); + jwt = authService.generateAuthToken({ userId }); + const { id } = await stocks.addStock(stockData); + userStock = { stockId: id, stockName: "EURO", quantity: 1, orderValue: 10, initialStockValue: 2 }; + }); + + afterEach(async function () { + await cleanDb(); + sinon.restore(); + }); + + it("Should return user stocks when stocks are available", async function () { + await stocks.updateUserStocks(userId, userStock); + + const res = await chai.request(app).get(`/stocks/${userId}?dev=true`).set("cookie", `${cookieName}=${jwt}`); + + expect(res).to.have.status(200); + expect(res.body).to.be.an("object"); + expect(res.body.message).to.equal("User stocks returned successfully!"); + expect(res.body.userStocks).to.be.an("array"); + expect(res.body.userStocks.map(({ id, ...rest }) => rest)).to.deep.equal([{ ...userStock, userId }]); + }); + + it("Should return empty object when no stocks are found", function (done) { + chai + .request(app) + .get(`/stocks/${userId}?dev=true`) + .set("cookie", `${cookieName}=${jwt}`) + .end((err, res) => { + if (err) return done(err); + + expect(res).to.have.status(200); + expect(res.body).to.be.an("object"); + expect(res.body.message).to.equal("No stocks found"); + expect(res.body.userStocks).to.be.an("array"); + + return done(); + }); + }); + + it("Should return 403 for unauthorized access", function (done) { + const userId = "anotherUser123"; + + chai + .request(app) + .get(`/stocks/${userId}?dev=true`) + .set("cookie", `${cookieName}=${jwt}`) + .end((err, res) => { + if (err) return done(err); + + expect(res).to.have.status(403); + expect(res.body).to.be.an("object"); + expect(res.body.message).to.equal("Unauthorized access"); + + return done(); + }); + }); + + it("Should return 500 when an internal server error occurs", function (done) { + sinon.stub(stocks, "fetchUserStocks").throws(new Error("Database error")); + + chai + .request(app) + .get(`/stocks/${userId}?dev=true`) + .set("cookie", `${cookieName}=${jwt}`) + .end((err, res) => { + if (err) return done(err); + + expect(res).to.have.status(500); + expect(res.body).to.be.an("object"); + expect(res.body.message).to.equal("An internal server error occurred"); + + return done(); + }); + }); +}); diff --git a/test/unit/middlewares/userAuthorization.test.ts b/test/unit/middlewares/userAuthorization.test.ts new file mode 100644 index 000000000..0815cbc32 --- /dev/null +++ b/test/unit/middlewares/userAuthorization.test.ts @@ -0,0 +1,70 @@ +import * as sinon from "sinon"; +import chai from "chai"; +const { expect } = chai; +const { userAuthorization } = require("../../../middlewares/userAuthorization"); + +describe("userAuthorization Middleware", function () { + let req; + let res; + let next; + + beforeEach(function () { + req = { + params: {}, + userData: {}, + }; + res = { + boom: { + forbidden: sinon.spy((message) => { + res.status = 403; + res.message = message; + }), + }, + }; + next = sinon.spy(); + }); + + it("should call next() if userId matches userData.id", function () { + req.params.userId = "123"; + req.userData.id = "123"; + + userAuthorization(req, res, next); + + expect(next.calledOnce).to.be.true; + expect(res.boom.forbidden.notCalled).to.be.true; + }); + + it("should call res.boom.forbidden() if userId does not match userData.id", function () { + req.params.userId = "123"; + req.userData.id = "456"; + + userAuthorization(req, res, next); + + expect(res.boom.forbidden.calledOnce).to.be.true; + expect(res.status).to.equal(403); + expect(res.message).to.equal("Unauthorized access"); + expect(next.notCalled).to.be.true; + }); + + it("should call res.boom.forbidden() if userData.id is missing", function () { + req.params.userId = "123"; + + userAuthorization(req, res, next); + + expect(res.boom.forbidden.calledOnce).to.be.true; + expect(res.status).to.equal(403); + expect(res.message).to.equal("Unauthorized access"); + expect(next.notCalled).to.be.true; + }); + + it("should call res.boom.forbidden() if userId is missing", function () { + req.userData.id = "123"; + + userAuthorization(req, res, next); + + expect(res.boom.forbidden.calledOnce).to.be.true; + expect(res.status).to.equal(403); + expect(res.message).to.equal("Unauthorized access"); + expect(next.notCalled).to.be.true; + }); +}); diff --git a/test/unit/models/users.test.js b/test/unit/models/users.test.js index c66eec4c1..60858a405 100644 --- a/test/unit/models/users.test.js +++ b/test/unit/models/users.test.js @@ -117,6 +117,31 @@ describe("users", function () { }); }); + describe("addOrUpdate-Dev Feature Flag", function () { + it("should update the user collection when userId is passed", async function () { + const userData1 = userDataArray[0]; + const userData2 = userDataArray[1]; + const updatedUserData = {}; + + Object.assign(updatedUserData, userData1, userData2); + + // Add the user the first time + const { isNewUser, userId } = await users.addOrUpdate(userData1, null, true); + + // Update the user with same data + const { isNewUser: updatedIsNewUserFlag } = await users.addOrUpdate(userData2, userId, true); + + const data = (await userModel.doc(userId).get()).data(); + + Object.keys(updatedUserData).forEach((key) => { + expect(updatedUserData[key]).to.deep.equal(data[key]); + }); + + expect(isNewUser).to.equal(true); + expect(updatedIsNewUserFlag).to.equal(false); + }); + }); + describe("fetch user details based on discord id", function () { let [userId0] = [];