Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,7 @@ workflows:
branches:
only:
- develop
- PM-4482
- security_july_2026
- PM-5460

# Production builds are exectuted only on tagged commits to the
# master branch.
Expand Down
22 changes: 16 additions & 6 deletions .github/workflows/trivy.yaml
Original file line number Diff line number Diff line change
@@ -1,34 +1,44 @@
name: Trivy Scanner

permissions:
contents: read
security-events: write
on:
push:
branches:
- main
- master
- dev
- develop
pull_request:
workflow_dispatch:

permissions:
actions: read
contents: read
security-events: write

jobs:
trivy-scan:
name: Use Trivy
name: Trivy SAST and SCA
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Run Trivy scanner in repo mode
uses: aquasecurity/trivy-action@0.35.0
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
version: "v0.73.0"
scan-type: "fs"
scan-ref: "."
ignore-unfixed: true
format: "sarif"
output: "trivy-results.sarif"
severity: "CRITICAL,HIGH,UNKNOWN"
limit-severities-for-sarif: true
scanners: vuln,secret,misconfig,license
github-pat: ${{ secrets.GITHUB_TOKEN }}

- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
if: always()
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: "trivy-results.sarif"
5 changes: 3 additions & 2 deletions ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
- PostgreSQL
- Docker and Docker Compose

The service does not consume Kafka messages. It continues to publish events through
the Bus API wrapper, so the existing Bus API and Auth0 configuration remains unchanged.
The service does not consume Kafka messages. It uses the Bus API wrapper only for
email-change verification events; profile and trait mutations do not publish events.
The existing Bus API and Auth0 configuration remains required for email verification.

## Install, Build, and Run

Expand Down
9 changes: 1 addition & 8 deletions app-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,7 @@ const EVENT_ORIGINATOR = 'topcoder-member-api'
const EVENT_MIME_TYPE = 'application/json'

const TOPICS = {
MemberCreated: 'member.action.profile.create',
MemberUpdated: 'member.action.profile.update',
EmailChanged: 'member.action.email.profile.emailchange.verification',
MemberTraitCreated: 'member.action.profile.trait.create',
MemberTraitUpdated: 'member.action.profile.trait.update',
MemberTraitDeleted: 'member.action.profile.trait.delete',
MemberSkillsCreated: 'member.action.profile.skills.create',
MemberSkillsUpdated: 'member.action.profile.skills.update'
EmailChanged: 'member.action.email.profile.emailchange.verification'
}

const MAMBO_GET_REWARDS_ALLOWED_FIELDS = [
Expand Down
12 changes: 8 additions & 4 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2489,11 +2489,12 @@ definitions:
properties:
skills:
type: boolean
gigAvailability:
engagementAvailability:
type: boolean
bio:
preferredRoles:
type: boolean
profilePicture:
description: True when the member has selected at least one preferred role.
bio:
type: boolean
workHistory:
type: boolean
Expand All @@ -2504,7 +2505,10 @@ definitions:
skillsLastUpdateDate:
type: date-time
description: 'ISO-8601 formatted date times (YYYY-MM-DDTHH:mm:ss.sssZ)'
gigAvailabilityLastUpdateDate:
engagementAvailabilityLastUpdateDate:
type: date-time
description: 'ISO-8601 formatted date times (YYYY-MM-DDTHH:mm:ss.sssZ)'
preferredRolesLastUpdateDate:
type: date-time
description: 'ISO-8601 formatted date times (YYYY-MM-DDTHH:mm:ss.sssZ)'
workHistoryLastUpdateDate:
Expand Down
60 changes: 47 additions & 13 deletions src/services/MemberService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,34 @@ function isEngagementAvailabilityComplete (member, openToWorkData) {
return hasAvailability || hasLegacyPreferredRoles
}

/**
* Determine whether the member has selected at least one preferred role.
* Prefers the top-level personalization.preferredRoles field (current model)
* and falls back to legacy openToWork.preferredRoles when the top-level key
* is absent.
* @param {Array} personalizationData personalization trait data entries
* @returns {Boolean} true when at least one preferred role is selected
*/
function hasPreferredRolesSelected (personalizationData) {
if (!Array.isArray(personalizationData) || personalizationData.length === 0) {
return false
}

const personalizationEntry = personalizationData.find(r =>
Object.prototype.hasOwnProperty.call(r, 'preferredRoles') ||
Object.prototype.hasOwnProperty.call(r, 'openToWork')
) || personalizationData[0] || {}

let preferredRoles
if (Object.prototype.hasOwnProperty.call(personalizationEntry, 'preferredRoles')) {
preferredRoles = personalizationEntry.preferredRoles
} else {
preferredRoles = personalizationEntry.openToWork && personalizationEntry.openToWork.preferredRoles
}

return Array.isArray(preferredRoles) && preferredRoles.length > 0
}

/**
* Resolve compact memberStats track/type UUIDs before deriving current
* maxRating labels for member profile responses.
Expand Down Expand Up @@ -617,6 +645,7 @@ async function getProfileCompleteness (currentUser, handle, query) {
// data.verified = false
data.skills = false
data.engagementAvailability = false
data.preferredRoles = false
data.bio = false
data.workHistory = false
data.education = false
Expand All @@ -626,6 +655,7 @@ async function getProfileCompleteness (currentUser, handle, query) {

data.skillsLastUpdateDate = undefined
data.engagementAvailabilityLastUpdateDate = undefined
data.preferredRolesLastUpdateDate = undefined
data.workHistoryLastUpdateDate = undefined
data.educationLastUpdateDate = undefined
data.locationLastUpdateDate = undefined
Expand All @@ -645,14 +675,22 @@ async function getProfileCompleteness (currentUser, handle, query) {
data.workHistoryLastUpdateDate = new Date(item.updatedAt).toISOString()
}

if (item.traitId === 'personalization' && item.traits.data.length > 0 && !data.engagementAvailability) {
const openToWorkTrait = item.traits.data.find(r => Object.keys(r).includes('openToWork')) || {}
const openToWorkData = openToWorkTrait.openToWork
if (item.traitId === 'personalization' && item.traits.data.length > 0) {
if (!data.engagementAvailability) {
const openToWorkTrait = item.traits.data.find(r => Object.keys(r).includes('openToWork')) || {}
const openToWorkData = openToWorkTrait.openToWork

if (isEngagementAvailabilityComplete(member, openToWorkData)) {
if (isEngagementAvailabilityComplete(member, openToWorkData)) {
completeItems += 1
data.engagementAvailability = true
data.engagementAvailabilityLastUpdateDate = new Date(item.updatedAt).toISOString()
}
}

if (!data.preferredRoles && hasPreferredRolesSelected(item.traits.data)) {
completeItems += 1
data.engagementAvailability = true
data.engagementAvailabilityLastUpdateDate = new Date(item.updatedAt).toISOString()
data.preferredRoles = true
data.preferredRolesLastUpdateDate = new Date(item.updatedAt).toISOString()
}
}
})
Expand All @@ -666,6 +704,9 @@ async function getProfileCompleteness (currentUser, handle, query) {
if (!data.engagementAvailability) {
showToast.push('engagementAvailability')
}
if (!data.preferredRoles) {
showToast.push('preferredRoles')
}

// TODO: Do we use the short bio or the "description" field of the member object?
if (member.description && !data.bio) {
Expand Down Expand Up @@ -905,8 +946,6 @@ async function updateMember (currentUser, handle, query, data) {

// convert prisma data to response format
prismaHelper.convertMember(result)
// send data to event bus
await helper.postBusEvent(constants.TOPICS.MemberUpdated, result)
if (emailChanged) {
// send email verification to old email
await helper.postBusEvent(constants.TOPICS.EmailChanged, {
Expand Down Expand Up @@ -1078,7 +1117,6 @@ async function updateHandle (currentUser, handle, query, data) {
}

prismaHelper.convertMember(updatedMember)
await helper.postBusEvent(constants.TOPICS.MemberUpdated, updatedMember)
return cleanMember(currentUser, updatedMember, selectFields)
}

Expand Down Expand Up @@ -1142,7 +1180,6 @@ async function verifyEmail (currentUser, handle, query) {
data: _.omit(member, ['maxRating', 'phones'])
})
prismaHelper.convertMember(result)
await helper.postBusEvent(constants.TOPICS.MemberUpdated, result)
return { emailChangeCompleted, verifiedEmail }
}

Expand Down Expand Up @@ -1221,8 +1258,6 @@ async function uploadPhoto (currentUser, handle, files) {
}
})
prismaHelper.convertMember(result)
// post bus event
await helper.postBusEvent(constants.TOPICS.MemberUpdated, result)
return { photoURL }
}

Expand Down Expand Up @@ -1343,7 +1378,6 @@ async function deleteMember (currentUser, handle, data) {
}

prismaHelper.convertMember(updatedMember)
await helper.postBusEvent(constants.TOPICS.MemberUpdated, updatedMember)

return {
handle: deletedHandle,
Expand Down
53 changes: 1 addition & 52 deletions src/services/MemberTraitService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ const moment = require('moment')
const helper = require('../common/helper')
const logger = require('../common/logger')
const errors = require('../common/errors')
const constants = require('../../app-constants')
const prisma = require('../common/prisma').getClient()
const prismaManager = require('../common/prisma')
const skillsPrisma = prismaManager.getSkillsClient()
Expand Down Expand Up @@ -479,22 +478,6 @@ async function createTraits (currentUser, handle, data) {
data: prismaData
})
}
// send data to event bus
for (let item of data) {
const trait = { ...item }
trait.userId = helper.bigIntToNumber(member.userId)
trait.createdBy = Number(currentUser.userId || config.TC_WEBSERVICE_USERID)
if (trait.traits) {
trait.traits = { 'traitId': trait.traitId, 'data': trait.traits.data }
} else {
trait.traits = { 'traitId': trait.traitId, 'data': [] }
}
// convert date time
trait.createdAt = new Date().getTime()
// post bus event
await helper.postBusEvent(constants.TOPICS.MemberTraitCreated, trait)
}

// merge result
existingTraits = _.concat(existingTraits, data)

Expand Down Expand Up @@ -575,24 +558,6 @@ async function updateTraits (currentUser, handle, data) {
await prisma.memberTraits.create({ data: createData })
}

// post bus events: created for new traits, updated for existing ones
const existingIds = new Set((existingTraits || []).map(t => t.traitId))
for (let r of result) {
if (!existingIds.has(r.traitId)) {
const trait = { ...r }
trait.userId = helper.bigIntToNumber(member.userId)
trait.createdBy = Number(currentUser.userId || config.TC_WEBSERVICE_USERID)
if (trait.traits) {
trait.traits = { traitId: trait.traitId, data: trait.traits.data }
} else {
trait.traits = { traitId: trait.traitId, data: [] }
}
trait.createdAt = new Date().getTime()
await helper.postBusEvent(constants.TOPICS.MemberTraitCreated, trait)
} else {
await helper.postBusEvent(constants.TOPICS.MemberTraitUpdated, r)
}
}
return result
}

Expand Down Expand Up @@ -632,26 +597,10 @@ async function removeTraits (currentUser, handle, query) {
})))
})
}
// remove existingTraits data
const memberProfileTraitIds = []
_.forEach(existingTraits, t => {
if (!traitIds || _.includes(traitIds, t.traitId)) {
memberProfileTraitIds.push(t.traitId)
}
})

// remove deleted traits from the data used to recalculate skill-score deductions
existingTraits = _.filter(existingTraits, t => !traitIds.includes(t.traitId))

await updateSkillScoreDeduction(currentUser, member, existingTraits)
// post bus event
if (memberProfileTraitIds.length > 0) {
await helper.postBusEvent(constants.TOPICS.MemberTraitDeleted, {
userId: helper.bigIntToNumber(member.userId),
memberProfileTraitIds,
updatedAt: new Date(),
updatedBy: currentUser.userId || currentUser.sub
})
}
}

removeTraits.schema = {
Expand Down
Loading
Loading