diff --git a/controllers/auth.js b/controllers/auth.js index 0b7443310..54de41e79 100644 --- a/controllers/auth.js +++ b/controllers/auth.js @@ -26,6 +26,7 @@ const githubAuth = (req, res, next) => { userData = { github_id: user.username, github_display_name: user.displayName, + github_user_id: user.id, }; const { userId, incompleteUserDetails } = await users.addOrUpdate(userData); diff --git a/controllers/users.js b/controllers/users.js index 6f8904e6d..ba4d3330b 100644 --- a/controllers/users.js +++ b/controllers/users.js @@ -1,3 +1,5 @@ +const axios = require("axios"); +const firestore = require("../utils/firestore"); const chaincodeQuery = require("../models/chaincodes"); const userQuery = require("../models/users"); const profileDiffsQuery = require("../models/profileDiffs"); @@ -566,6 +568,74 @@ const filterUsers = async (req, res) => { } }; +// one time script function to perform the migration - adding github_user_id field to the document +const addGithubId = async (req, res) => { + const usersNotFound = []; + let countUserFound = 0; + let countUserNotFound = 0; + try { + // Fetch user data from GitHub API for each document in the users collection + // divided by 500 because firestore api guarantee that we can process in batch of 500. + const usersSnapshot = await firestore.collection("users").get(); + const totalUsers = usersSnapshot.docs.length; + const batchCount = Math.ceil(totalUsers / 500); + // Create batch write operations for each batch of documents + for (let i = 0; i < batchCount; i++) { + const batchDocs = usersSnapshot.docs.slice(i * 500, (i + 1) * 500); + const batchWrite = firestore.batch(); + const batchWrites = []; + for (const userDoc of batchDocs) { + const githubUsername = userDoc.data().github_id; + const username = userDoc.data().username; + const userId = userDoc.id; + batchWrite.update(userDoc.ref, { github_user_id: null }); + batchWrites.push( + axios + .get(`https://api.github.com/users/${githubUsername}`, { + headers: { + "Content-Type": "application/json", + }, + auth: { + username: config.get("githubOauth.clientId"), + password: config.get("githubOauth.clientSecret"), + }, + }) + .then((response) => { + const githubUserId = response.data.id; + batchWrite.update(userDoc.ref, { github_user_id: `${githubUserId}` }); + countUserFound++; + }) + .catch((error) => { + countUserNotFound++; + const invalidUsers = { userId, username, githubUsername }; + usersNotFound.push(invalidUsers); + if (error.response && error.response.status === 404) { + logger.error("GitHub user not found", error); + } else { + logger.error("An error occurred at axios.get:", error); + } + }) + ); + } + await Promise.all(batchWrites); + await batchWrite.commit(); + } + + return res.status(200).json({ + message: "Result of migration", + data: { + totalUsers: totalUsers, + usersUpdated: countUserFound, + usersNotUpdated: countUserNotFound, + invalidUsersDetails: usersNotFound, + }, + }); + } catch (error) { + logger.error(`Error while Updating all users: ${error}`); + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); + } +}; + const nonVerifiedDiscordUsers = async (req, res) => { const data = await dataAccess.retrieveDiscordUsers(); return res.json(data); @@ -641,6 +711,7 @@ module.exports = { addDefaultArchivedRole, getUserSkills, filterUsers, + addGithubId, verifyUserImage, getUserImageForVerification, nonVerifiedDiscordUsers, diff --git a/models/users.js b/models/users.js index d4d3c01fc..1ec82a399 100644 --- a/models/users.js +++ b/models/users.js @@ -45,15 +45,17 @@ const addOrUpdate = async (userData, userId = null) => { } // userId is null, Add or Update user - const user = await userModel.where("github_id", "==", userData.github_id).limit(1).get(); - if (!user.empty) { - await userModel.doc(user.docs[0].id).set(userData, { merge: true }); - - return { - isNewUser: false, - userId: user.docs[0].id, - incompleteUserDetails: user.docs[0].data().incompleteUserDetails, - }; + if (userData.github_user_id) { + const user = await userModel.where("github_user_id", "==", userData.github_user_id).limit(1).get(); + if (!user.empty) { + await userModel.doc(user.docs[0].id).set(userData, { merge: true }); + + return { + isNewUser: false, + userId: user.docs[0].id, + incompleteUserDetails: user.docs[0].data().incompleteUserDetails, + }; + } } // Add new user diff --git a/routes/users.js b/routes/users.js index 35f2e5f7a..d4aa671ab 100644 --- a/routes/users.js +++ b/routes/users.js @@ -50,4 +50,7 @@ router.patch("/rejectDiff", authenticate, authorizeRoles([SUPERUSER]), users.rej router.patch("/:userId", authenticate, authorizeRoles([SUPERUSER]), users.updateUser); router.get("/suggestedUsers/:skillId", authenticate, authorizeRoles([SUPERUSER]), users.getSuggestedUsers); +// WARNING!! - One time Script/Route to do migration. +router.post("/migrate", authenticate, authorizeRoles([SUPERUSER]), users.addGithubId); + module.exports = router; diff --git a/test/fixtures/user/migration.js b/test/fixtures/user/migration.js new file mode 100644 index 000000000..fb1813b5f --- /dev/null +++ b/test/fixtures/user/migration.js @@ -0,0 +1,38 @@ +const githubUserInfo = require("../auth/githubUserInfo")(); + +/** + * User info for GitHub auth response + * Multiple responses can be added to the array if required + * + * @return {Object} + */ +module.exports = () => { + return [ + { + username: "ankur", + first_name: "Ankur", + last_name: "Narkhede", + yoe: 0, + img: "./img.png", + linkedin_id: "ankurnarkhede", + github_id: githubUserInfo[0].username, + github_display_name: githubUserInfo[0].displayName, + isMember: true, + phone: "1234567890", + email: "abc@gmail.com", + roles: { + member: true, + }, + tokens: { + githubAccessToken: "githubAccessToken", + }, + status: "active", + profileURL: "https://abcde.com", + picture: { + publicId: "profile/mtS4DhUvNYsKqI7oCWVB/aenklfhtjldc5ytei3ar", + url: "https://res.cloudinary.com/realdevsquad/image/upload/v1667685133/profile/mtS4DhUvNYsKqI7oCWVB/aenklfhtjldc5ytei3ar.jpg", + }, + incompleteUserDetails: false, + }, + ]; +}; diff --git a/test/fixtures/user/user.js b/test/fixtures/user/user.js index a88d923e1..205b1e5a1 100644 --- a/test/fixtures/user/user.js +++ b/test/fixtures/user/user.js @@ -17,6 +17,7 @@ module.exports = () => { yoe: 0, img: "./img.png", linkedin_id: "ankurnarkhede", + github_user_id: githubUserInfo[0].id, github_id: githubUserInfo[0].username, github_display_name: githubUserInfo[0].displayName, isMember: true, @@ -44,6 +45,7 @@ module.exports = () => { last_name: "Bhandarkar", yoe: 0, img: "./img.png", + github_user_id: "23654780", github_id: "whydonti", linkedin_id: "nikhil-bhandarkar", twitter_id: "whatifi", @@ -60,6 +62,7 @@ module.exports = () => { last_name: "Gajjewar", yoe: 0, img: "./img.png", + github_user_id: "23654788", github_id: "cartmanishere", linkedin_id: "pranav-gajjewar", twitter_id: "PGajjewar", @@ -80,6 +83,7 @@ module.exports = () => { yoe: 3, img: "./img.png", linkedin_id: "sagarbajpai", + github_user_id: "23654790", github_id: "sagarbajpai", github_display_name: "Sagar Bajpai", phone: "1234567890", @@ -105,6 +109,7 @@ module.exports = () => { yoe: 10, img: "./img.png", linkedin_id: "ankushdharkar", + github_user_id: "23654700", github_id: "ankushdharkar", github_display_name: "Ankush Dharkar", phone: "1234567890", @@ -131,6 +136,7 @@ module.exports = () => { yoe: 0, img: "./img.png", linkedin_id: "ankitabannore", + github_user_id: "23654725", github_id: "Ankita2002-Fr", github_display_name: "Ankita Bannore", isMember: true, @@ -155,6 +161,7 @@ module.exports = () => { last_name: "Chaudhari", yoe: 0, img: "./img.png", + github_user_id: "23654583", github_id: "mehulkchaudhari", linkedin_id: "mehulkchaudhari", twitter_id: "mehulkchaudhari", @@ -181,6 +188,7 @@ module.exports = () => { yoe: 0, img: "./img.png", linkedin_id: "ankurnarkhede", + github_user_id: "23654236", github_id: "ankur1234", github_display_name: "ankur-xyz", phone: "1234567890", @@ -189,6 +197,7 @@ module.exports = () => { { username: "ritvik", github_id: "RitvikJamwal75", + github_user_id: "23654123", first_name: "Ritvik", yoe: 1, picture: { @@ -220,6 +229,7 @@ module.exports = () => { linkedin_id: "tanishqsingla", github_id: "tanishqsingla", github_display_name: "Tanishq Singla", + github_user_id: "26207583", phone: "1234567890", email: "ts@gmail.com", tokens: { @@ -244,6 +254,7 @@ module.exports = () => { linkedin_id: "darthvader", github_id: "darthvader", github_display_name: "Darth Vader", + github_user_id: "3188964", phone: "1234567890", email: "dv@gmail.com", tokens: { @@ -265,6 +276,7 @@ module.exports = () => { yoe: 1, img: "./img.png", linkedin_id: "testuser1", + github_user_id: "23654734", github_id: "testuser1", github_display_name: "Test User", phone: "1234567890", @@ -289,6 +301,7 @@ module.exports = () => { yoe: 1, img: "./img.png", linkedin_id: "testuser1", + github_user_id: "23654735", github_id: "testuser1", github_display_name: "Test User", phone: "1234567890", @@ -317,6 +330,7 @@ module.exports = () => { incompleteUserDetails: false, status: "active", last_name: "Singh", + github_user_id: "23654736", github_display_name: "Ram Singh", website: "Ramsingh123.github.io/portfolio", designation: "SDE", @@ -329,5 +343,26 @@ module.exports = () => { twitter_id: "ramsingh123", linkedin_id: "ramsingh123", }, + { + username: "testuser2", + first_name: "test2", + last_name: "user2", + yoe: 1, + img: "./img.png", + linkedin_id: "testuser1", + github_id: "testuser1", + github_display_name: "Test User", + phone: "1234567890", + email: "tu@gmail.com", + chaincode: "12345", + + roles: { + member: true, + }, + picture: { + publicId: "profile/mtS4DhUvNYsKqI7oCWVB/aenklfhtjldc5ytei3ar", + url: "https://res.cloudinary.com/realdevsquad/image/upload/v1667685133/profile/mtS4DhUvNYsKqI7oCWVB/aenklfhtjldc5ytei3ar.jpg", + }, + }, ]; }; diff --git a/test/integration/contributions.test.js b/test/integration/contributions.test.js index 1a00d09d8..cb37922b9 100644 --- a/test/integration/contributions.test.js +++ b/test/integration/contributions.test.js @@ -23,6 +23,7 @@ describe("Contributions", function () { yoe: 0, img: "./img.png", github_id: "prakashchoudhary07", + github_user_id: "12345678", username: "prakash", }; // Adding user @@ -30,10 +31,12 @@ describe("Contributions", function () { // Creating second user user.username = "userWithNoPrs"; user.github_id = "userWithNoPrs"; + user.github_user_id = "userWithNoPrs"; await userModel.addOrUpdate(user); // Creating second user user.username = "userNoTask"; user.github_id = "userNoTask"; + user.github_user_id = "userNoTask"; await userModel.addOrUpdate(user); // Creating task for user const task = { diff --git a/test/integration/tasks.test.js b/test/integration/tasks.test.js index 1ea555198..0d9bfbcc2 100644 --- a/test/integration/tasks.test.js +++ b/test/integration/tasks.test.js @@ -320,6 +320,7 @@ describe("Tasks", function () { const { userId: assignedUser } = await userModel.addOrUpdate({ github_id: "prakashchoudhary07", username: "user1", + github_user_id: "12345678", }); const assignedTask = [ { diff --git a/test/integration/users.test.js b/test/integration/users.test.js index 3252dc717..7acb6086c 100644 --- a/test/integration/users.test.js +++ b/test/integration/users.test.js @@ -1,4 +1,5 @@ const chai = require("chai"); +const axios = require("axios"); const { expect } = chai; const chaiHttp = require("chai-http"); @@ -31,9 +32,11 @@ const userAlreadyNotMember = userData[13]; const userAlreadyArchived = userData[5]; const userAlreadyUnArchived = userData[4]; const nonSuperUser = userData[0]; +const userWithoutGithubUserId = userData[14]; const cookieName = config.get("userToken.cookieName"); const { userPhotoVerificationData } = require("../fixtures/user/photo-verification"); +const githubUserInfo = require("../fixtures/auth/githubUserInfo")(); const Sinon = require("sinon"); const { INTERNAL_SERVER_ERROR } = require("../../constants/errorMessages"); const photoVerificationModel = firestore.collection("photo-verification"); @@ -1250,6 +1253,83 @@ describe("Users", function () { }); }); + describe("POST /users/migrate", function () { + let fetchStub; + + beforeEach(async function () { + await addUser(userWithoutGithubUserId); + fetchStub = Sinon.stub(axios, "get"); + }); + + afterEach(async function () { + Sinon.restore(); + }); + + it("Should add github_user_id to the user", async function () { + fetchStub.resolves({ + data: githubUserInfo[0]._json, + }); + const usersMigrateResponse = await chai + .request(app) + .post(`/users/migrate`) + .set("Cookie", `${cookieName}=${superUserAuthToken}`); + expect(usersMigrateResponse).to.have.status(200); + expect(usersMigrateResponse.body).to.deep.equal({ + message: "Result of migration", + data: { + totalUsers: 3, + usersUpdated: 3, + usersNotUpdated: 0, + invalidUsersDetails: [], + }, + }); + const usersResponse = await chai.request(app).get(`/users`).set("cookie", `${cookieName}=${superUserAuthToken}`); + expect(usersResponse).to.have.status(200); + usersResponse.body.users.forEach((document) => { + expect(document).to.have.property(`github_user_id`); + }); + }); + it("Should return details of users with invalid github username", async function () { + fetchStub.rejects({ response: { status: 404 } }); + const usersMigrateResponse = await chai + .request(app) + .post(`/users/migrate`) + .set("Cookie", `${cookieName}=${superUserAuthToken}`); + expect(usersMigrateResponse).to.have.status(200); + expect(usersMigrateResponse.body.message).to.be.equal("Result of migration"); + expect(usersMigrateResponse.body).to.have.property("data"); + expect(usersMigrateResponse.body.data).to.have.all.keys( + "totalUsers", + "usersUpdated", + "usersNotUpdated", + "invalidUsersDetails" + ); + expect(usersMigrateResponse.body.data.totalUsers).to.be.equal(3); + expect(usersMigrateResponse.body.data.usersUpdated).to.be.equal(0); + expect(usersMigrateResponse.body.data.usersNotUpdated).to.be.equal(3); + usersMigrateResponse.body.data.invalidUsersDetails.forEach((document) => { + expect(document).to.have.all.keys("userId", "username", "githubUsername"); + }); + }); + it("Should return unauthorized error when not logged in", function (done) { + chai + .request(app) + .post(`/users/migrate`) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(401); + expect(res.body).to.eql({ + statusCode: 401, + error: "Unauthorized", + message: "Unauthenticated User", + }); + return done(); + }); + }); + }); + describe("PATCH /users/:id/temporary/data", function () { it("Should make the user a member", function (done) { addUser(userRoleUpdate).then((userRoleUpdateId) => { diff --git a/test/unit/models/users.test.js b/test/unit/models/users.test.js index 64e63eaf9..49f56e65b 100644 --- a/test/unit/models/users.test.js +++ b/test/unit/models/users.test.js @@ -92,6 +92,43 @@ describe("users", function () { expect(user.last_name).to.equal(userData.last_name); expect(userExists).to.equal(true); }); + + it("should add the github_user_id to the user collection", async function () { + const userData = userDataArray[0]; + userData.github_user_id = "12345678"; + + const { isNewUser, userId } = await users.addOrUpdate(userData); + + const data = (await userModel.doc(userId).get()).data(); + + expect(data.github_user_id).to.equal(userData.github_user_id); + expect(isNewUser).to.equal(true); + }); + + it("should update the github_id in the user collection", async function () { + const userData = userDataArray[0]; + userData.github_id = "Yash Sinha"; + + // Add the user the first time + const { userId } = await users.addOrUpdate(userData); + + // Update the user with same data and new github_user_id + userData.github_id = "Ankush Dharkar"; + await users.addOrUpdate(userData, userId); + + const data = (await userModel.doc(userId).get()).data(); + + expect(data.github_id).to.equal(userData.github_id); + }); + + it("should be stored correctly in the database", async function () { + const userData = { ...userDataArray[0], github_id: "my_github_id" }; + + const { userId } = await users.addOrUpdate(userData); + const data = (await userModel.doc(userId).get()).data(); + + expect(data.github_id).to.equal("my_github_id"); + }); }); describe("user image verification", function () { let userId, discordId, profileImageUrl, discordImageUrl;