Skip to content

Commit 5148214

Browse files
authored
Merge pull request #2312 from Real-Dev-Squad/develop
Dev to Main Sync
2 parents 0206c7a + 9a59db2 commit 5148214

37 files changed

+2145
-103
lines changed

config/custom-environment-variables.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ module.exports = {
3737
clientSecret: "GITHUB_CLIENT_SECRET",
3838
},
3939

40+
googleOauth: {
41+
clientId: "GOOGLE_CLIENT_ID",
42+
clientSecret: "GOOGLE_CLIENT_SECRET",
43+
},
44+
4045
githubAccessToken: "GITHUB_PERSONAL_ACCESS_TOKEN",
4146

4247
firestore: "FIRESTORE_CONFIG",

config/default.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ module.exports = {
3232
clientSecret: "<clientSecret>",
3333
},
3434

35+
googleOauth: {
36+
clientId: "<clientId>",
37+
clientSecret: "<clientSecret>",
38+
},
39+
3540
emailServiceConfig: {
3641
email: "<RDS_EMAIL>",
3742
password: "<EMAIL PASSWORD GENERATED AFTER 2FA>",

controllers/auth.js

Lines changed: 134 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,120 @@ const {
99
USER_DOES_NOT_EXIST_ERROR,
1010
} = require("../constants/errorMessages");
1111

12+
const googleAuthLogin = (req, res, next) => {
13+
const { redirectURL } = req.query;
14+
return passport.authenticate("google", {
15+
scope: ["email"],
16+
state: redirectURL,
17+
})(req, res, next);
18+
};
19+
20+
function handleRedirectUrl(req) {
21+
const rdsUiUrl = new URL(config.get("services.rdsUi.baseUrl"));
22+
let authRedirectionUrl = rdsUiUrl;
23+
let isMobileApp = false;
24+
let isV2FlagPresent = false;
25+
let devMode = false;
26+
27+
if ("state" in req.query) {
28+
try {
29+
const redirectUrl = new URL(req.query.state);
30+
if (redirectUrl.searchParams.get("isMobileApp") === "true") {
31+
isMobileApp = true;
32+
redirectUrl.searchParams.delete("isMobileApp");
33+
}
34+
35+
if (`.${redirectUrl.hostname}`.endsWith(`.${rdsUiUrl.hostname}`)) {
36+
// Matching *.realdevsquad.com
37+
authRedirectionUrl = redirectUrl;
38+
devMode = Boolean(redirectUrl.searchParams.get("dev"));
39+
} else {
40+
logger.error(`Malicious redirect URL provided URL: ${redirectUrl}, Will redirect to RDS`);
41+
}
42+
if (redirectUrl.searchParams.get("v2") === "true") {
43+
isV2FlagPresent = true;
44+
}
45+
} catch (error) {
46+
logger.error("Invalid redirect URL provided", error);
47+
}
48+
}
49+
return {
50+
authRedirectionUrl,
51+
isMobileApp,
52+
isV2FlagPresent,
53+
devMode,
54+
};
55+
}
56+
57+
const getAuthCookieOptions = () => {
58+
const rdsUiUrl = new URL(config.get("services.rdsUi.baseUrl"));
59+
return {
60+
domain: rdsUiUrl.hostname,
61+
expires: new Date(Date.now() + config.get("userToken.ttl") * 1000),
62+
httpOnly: true,
63+
secure: true,
64+
sameSite: "lax",
65+
};
66+
};
67+
68+
async function handleGoogleLogin(req, res, user, authRedirectionUrl) {
69+
try {
70+
if (!user.emails || user.emails.length === 0) {
71+
logger.error("Google login failed: No emails found in user data");
72+
return res.boom.unauthorized("No email found in Google account");
73+
}
74+
const primaryEmail = user.emails.find((email) => email.verified === true);
75+
if (!primaryEmail) {
76+
logger.error("Google login failed: No verified email found");
77+
return res.boom.unauthorized("No verified email found in Google account");
78+
}
79+
80+
const userData = {
81+
email: primaryEmail.value,
82+
created_at: Date.now(),
83+
updated_at: null,
84+
};
85+
86+
const userDataFromDB = await users.fetchUser({ email: userData.email });
87+
88+
if (userDataFromDB.userExists) {
89+
if (userDataFromDB.user.roles?.developer) {
90+
const errorMessage = encodeURIComponent("Google login is restricted for developer role.");
91+
const separator = authRedirectionUrl.search ? "&" : "?";
92+
return res.redirect(`${authRedirectionUrl}${separator}error=${errorMessage}`);
93+
}
94+
}
95+
96+
const { userId, incompleteUserDetails } = await users.addOrUpdate(userData);
97+
98+
const token = authService.generateAuthToken({ userId });
99+
100+
const cookieOptions = getAuthCookieOptions();
101+
102+
res.cookie(config.get("userToken.cookieName"), token, cookieOptions);
103+
104+
if (incompleteUserDetails) {
105+
authRedirectionUrl = "https://my.realdevsquad.com/new-signup";
106+
}
107+
108+
return res.redirect(authRedirectionUrl);
109+
} catch (err) {
110+
logger.error("Unexpected error during Google login", err);
111+
return res.boom.unauthorized("User cannot be authenticated");
112+
}
113+
}
114+
115+
const googleAuthCallback = (req, res, next) => {
116+
const { authRedirectionUrl } = handleRedirectUrl(req);
117+
return passport.authenticate("google", { session: false }, async (err, accessToken, user) => {
118+
if (err) {
119+
logger.error(err);
120+
return res.boom.unauthorized("User cannot be authenticated");
121+
}
122+
return await handleGoogleLogin(req, res, user, authRedirectionUrl);
123+
})(req, res, next);
124+
};
125+
12126
/**
13127
* Makes authentication call to GitHub statergy
14128
*
@@ -41,33 +155,7 @@ const githubAuthLogin = (req, res, next) => {
41155
*/
42156
const githubAuthCallback = (req, res, next) => {
43157
let userData;
44-
let isMobileApp = false;
45-
const rdsUiUrl = new URL(config.get("services.rdsUi.baseUrl"));
46-
let authRedirectionUrl = rdsUiUrl;
47-
let devMode = false;
48-
let isV2FlagPresent = false;
49-
50-
if ("state" in req.query) {
51-
try {
52-
const redirectUrl = new URL(req.query.state);
53-
if (redirectUrl.searchParams.get("isMobileApp") === "true") {
54-
isMobileApp = true;
55-
redirectUrl.searchParams.delete("isMobileApp");
56-
}
57-
58-
if (redirectUrl.searchParams.get("v2") === "true") isV2FlagPresent = true;
59-
60-
if (`.${redirectUrl.hostname}`.endsWith(`.${rdsUiUrl.hostname}`)) {
61-
// Matching *.realdevsquad.com
62-
authRedirectionUrl = redirectUrl;
63-
devMode = Boolean(redirectUrl.searchParams.get("dev"));
64-
} else {
65-
logger.error(`Malicious redirect URL provided URL: ${redirectUrl}, Will redirect to RDS`);
66-
}
67-
} catch (error) {
68-
logger.error("Invalid redirect URL provided", error);
69-
}
70-
}
158+
let { authRedirectionUrl, isMobileApp, isV2FlagPresent, devMode } = handleRedirectUrl(req);
71159
try {
72160
return passport.authenticate("github", { session: false }, async (err, accessToken, user) => {
73161
if (err) {
@@ -77,23 +165,33 @@ const githubAuthCallback = (req, res, next) => {
77165
userData = {
78166
github_id: user.username,
79167
github_display_name: user.displayName,
168+
email: user._json.email,
80169
github_created_at: Number(new Date(user._json.created_at).getTime()),
81170
github_user_id: user.id,
82171
created_at: Date.now(),
83172
updated_at: null,
84173
};
85174

175+
if (!userData.email) {
176+
const githubBaseUrl = config.get("githubApi.baseUrl");
177+
const res = await fetch(`${githubBaseUrl}/user/emails`, {
178+
headers: {
179+
Authorization: `token ${accessToken}`,
180+
},
181+
});
182+
const emails = await res.json();
183+
const primaryEmails = emails.filter((item) => item.primary);
184+
185+
if (primaryEmails.length > 0) {
186+
userData.email = primaryEmails[0].email;
187+
}
188+
}
189+
86190
const { userId, incompleteUserDetails, role } = await users.addOrUpdate(userData);
87191

88192
const token = authService.generateAuthToken({ userId });
89193

90-
const cookieOptions = {
91-
domain: rdsUiUrl.hostname,
92-
expires: new Date(Date.now() + config.get("userToken.ttl") * 1000),
93-
httpOnly: true,
94-
secure: true,
95-
sameSite: "lax",
96-
};
194+
const cookieOptions = getAuthCookieOptions();
97195
// respond with a cookie
98196
res.cookie(config.get("userToken.cookieName"), token, cookieOptions);
99197

@@ -232,6 +330,8 @@ const fetchDeviceDetails = async (req, res) => {
232330
module.exports = {
233331
githubAuthLogin,
234332
githubAuthCallback,
333+
googleAuthLogin,
334+
googleAuthCallback,
235335
signout,
236336
storeUserDeviceInfo,
237337
updateAuthStatus,

controllers/discordactions.js

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -119,26 +119,52 @@ const deleteGroupRole = async (req, res) => {
119119
};
120120

121121
/**
122-
* Gets all group-roles
122+
* Fetches all group roles or provides paginated results when ?dev=true is passed.
123123
*
124-
* @param res {Object} - Express response object
124+
* @param {Object} req - Express request object.
125+
* @param {Object} res - Express response object.
125126
*/
126-
127-
const getAllGroupRoles = async (req, res) => {
127+
const getPaginatedAllGroupRoles = async (req, res) => {
128128
try {
129-
const { groups } = await discordRolesModel.getAllGroupRoles();
129+
const { page = 0, size = 10, dev } = req.query;
130+
const limit = parseInt(size, 10) || 10;
131+
const offset = parseInt(page, 10) * limit;
132+
133+
if (limit < 1 || limit > 100) {
134+
return res.boom.badRequest("Invalid size. Must be between 1 and 100.");
135+
}
136+
130137
const discordId = req.userData?.discordId;
138+
if (dev === "true") {
139+
const { roles, total } = await discordRolesModel.getPaginatedGroupRolesByPage({ offset, limit });
140+
const groupsWithMembershipInfo = await discordRolesModel.enrichGroupDataWithMembershipInfo(discordId, roles);
141+
142+
const nextPage = offset + limit < total ? parseInt(page, 10) + 1 : null;
143+
const prevPage = page > 0 ? parseInt(page, 10) - 1 : null;
144+
145+
const baseUrl = `${req.baseUrl}${req.path}`;
146+
const next = nextPage !== null ? `${baseUrl}?page=${nextPage}&size=${limit}&dev=true` : null;
147+
const prev = prevPage !== null ? `${baseUrl}?page=${prevPage}&size=${limit}&dev=true` : null;
148+
149+
return res.json({
150+
message: "Roles fetched successfully!",
151+
groups: groupsWithMembershipInfo,
152+
links: { next, prev },
153+
});
154+
}
155+
156+
const { groups } = await discordRolesModel.getAllGroupRoles();
131157
const groupsWithMembershipInfo = await discordRolesModel.enrichGroupDataWithMembershipInfo(discordId, groups);
158+
132159
return res.json({
133160
message: "Roles fetched successfully!",
134161
groups: groupsWithMembershipInfo,
135162
});
136163
} catch (err) {
137-
logger.error(`Error while getting roles: ${err}`);
164+
logger.error(`Error while fetching paginated group roles: ${err}`);
138165
return res.boom.badImplementation(INTERNAL_SERVER_ERROR);
139166
}
140167
};
141-
142168
const getGroupsRoleId = async (req, res) => {
143169
try {
144170
const { discordId } = req.userData;
@@ -534,7 +560,7 @@ const getUserDiscordInvite = async (req, res) => {
534560
module.exports = {
535561
getGroupsRoleId,
536562
createGroupRole,
537-
getAllGroupRoles,
563+
getPaginatedAllGroupRoles,
538564
addGroupRoleToMember,
539565
deleteRole,
540566
updateDiscordImageForVerification,

controllers/userStatus.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages");
44
const dataAccess = require("../services/dataAccessLayer");
55
const userStatusModel = require("../models/userStatus");
66
const { userState, CANCEL_OOO } = require("../constants/userStatus");
7+
const ROLES = require("../constants/roles");
8+
const firestore = require("../utils/firestore");
9+
const usersCollection = firestore.collection("users");
710

811
/**
912
* Deletes a new User Status
@@ -107,6 +110,11 @@ const getAllUserStatus = async (req, res) => {
107110
const updateUserStatus = async (req, res) => {
108111
try {
109112
const userId = getUserIdBasedOnRoute(req);
113+
const userDoc = await usersCollection.doc(userId).get();
114+
if (!userDoc.exists) {
115+
return res.boom.notFound("The User doesn't exist.");
116+
}
117+
110118
if (userId) {
111119
const dataToUpdate = req.body;
112120
const updateStatus = await userStatusModel.updateUserStatus(userId, dataToUpdate);
@@ -240,6 +248,26 @@ const updateUserStatusController = async (req, res, next) => {
240248
}
241249
};
242250

251+
const updateUserStatuses = async (req, res, next) => {
252+
try {
253+
const { id: currentUserId, roles = {} } = req.userData;
254+
const isSelf = req.params.userId === currentUserId;
255+
const isSuperUser = roles[ROLES.SUPERUSER];
256+
257+
if (isSelf || isSuperUser) {
258+
if (isSelf && Object.keys(req.body).includes(CANCEL_OOO)) {
259+
return await cancelOOOStatus(req, res, next);
260+
}
261+
return await updateUserStatus(req, res, next);
262+
}
263+
264+
return res.boom.unauthorized("You are not authorized to perform this action.");
265+
} catch (err) {
266+
logger.error(`Error in updateUserStatusController: ${err}`);
267+
return res.boom.badImplementation("An unexpected error occurred.");
268+
}
269+
};
270+
243271
module.exports = {
244272
deleteUserStatus,
245273
getUserStatus,
@@ -250,4 +278,5 @@ module.exports = {
250278
getUserStatusControllers,
251279
batchUpdateUsersStatus,
252280
updateUserStatusController,
281+
updateUserStatuses,
253282
};

0 commit comments

Comments
 (0)