@@ -3,29 +3,42 @@ const FormResponse = require('../models/hgnFormResponse');
33const 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