Skip to content

Commit 77c7423

Browse files
Merge pull request #2143 from OneCommunityGlobal/Sohail-community-members-skill-filter
Sohail: HGN Questionnaire Dashboard: Community Members (Filtered Members View)
2 parents f2d4936 + 21798c7 commit 77c7423

2 files changed

Lines changed: 33 additions & 31 deletions

File tree

package-lock.json

Lines changed: 0 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/controllers/communityController.js

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,42 @@ const FormResponse = require('../models/hgnFormResponse');
33
const communityMemberController = function () {
44
const getCommunityMembers = async function (req, res) {
55
try {
6-
const query = {};
7-
const { search, skills, sortOrder = 'asc' } = req.query;
6+
const { search, skills } = req.query;
7+
8+
// Validate sortOrder against an allowlist to prevent injection
9+
const sortOrder = req.query.sortOrder === 'desc' ? 'desc' : 'asc';
810

11+
const query = {};
912
if (search) {
10-
query['userInfo.name'] = { $regex: search, $options: 'i' };
13+
// Escape regex special characters to prevent ReDoS
14+
const escapedSearch = search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
15+
query['userInfo.name'] = { $regex: escapedSearch, $options: 'i' };
1116
}
12-
const formResponses = await FormResponse.find(query).sort({
13-
'userInfo.name': sortOrder === 'asc' ? 1 : -1,
14-
});
17+
18+
// Use .lean() to get plain JS objects so Object.entries() works correctly on subdocuments
19+
const formResponses = await FormResponse.find(query)
20+
.lean()
21+
.sort({ 'userInfo.name': sortOrder === 'asc' ? 1 : -1 });
1522

1623
const skillFilters = skills ? skills.split(',').map((s) => s.trim().toLowerCase()) : [];
17-
const structuredMembers = formResponses.map((member) => {
18-
const { userInfo, frontend, backend, general } = member;
1924

20-
const extractSkills = (section) =>
21-
Object.entries(section || {}).reduce((acc, [key, val]) => {
22-
const num = parseFloat(val);
23-
if (key.toLowerCase() !== 'overall' && !Number.isNaN(num)) {
24-
acc[key] = num;
25-
}
25+
// Extract skill keys that have a numeric rating value, excluding 'overall' and internal fields
26+
const extractSkills = (section) => {
27+
if (!section || typeof section !== 'object') return {};
28+
return Object.entries(section).reduce((acc, [key, val]) => {
29+
if (key.toLowerCase() === 'overall' || key.startsWith('$') || key.startsWith('_')) {
2630
return acc;
27-
}, {});
31+
}
32+
const num = parseFloat(val);
33+
if (!Number.isNaN(num)) {
34+
acc[key] = num;
35+
}
36+
return acc;
37+
}, {});
38+
};
2839

40+
const structuredMembers = formResponses.map((member) => {
41+
const { userInfo, frontend, backend, general } = member;
2942
return {
3043
_id: member._id,
3144
name: userInfo?.name || 'N/A',
@@ -42,12 +55,11 @@ const communityMemberController = function () {
4255
const filteredMembers =
4356
skillFilters.length > 0
4457
? structuredMembers.filter((member) => {
45-
const allSkills = {
46-
...member.skills.frontend,
47-
...member.skills.backend,
48-
};
49-
const lowercased = Object.keys(allSkills).map((s) => s.toLowerCase());
50-
return skillFilters.every((filterSkill) => lowercased.includes(filterSkill));
58+
const allSkillKeys = [
59+
...Object.keys(member.skills.frontend),
60+
...Object.keys(member.skills.backend),
61+
].map((s) => s.toLowerCase());
62+
return skillFilters.every((filterSkill) => allSkillKeys.includes(filterSkill));
5163
})
5264
: structuredMembers;
5365

0 commit comments

Comments
 (0)