From 97649fe9432d3463dc32ad470115fc9b88828228 Mon Sep 17 00:00:00 2001 From: Gunasekar-K Date: Tue, 29 Jul 2025 15:57:38 +0530 Subject: [PATCH 01/13] circleci integration --- .circleci/config.yml | 79 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..373f521 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,79 @@ +version: 2.1 + +parameters: + reset-db: + type: boolean + default: false +defaults: &defaults + docker: + - image: cimg/python:3.13.2-browsers +install_dependency: &install_dependency + name: Installation of build and deployment dependencies. + command: | + sudo apt update + sudo apt install -y jq python3-pip + sudo pip3 install awscli --upgrade +install_deploysuite: &install_deploysuite + name: Installation of install_deploysuite. + command: | + git clone --branch v1.4.19 https://github.com/topcoder-platform/tc-deploy-scripts ../buildscript + cp ./../buildscript/master_deploy.sh . + cp ./../buildscript/buildenv.sh . + cp ./../buildscript/awsconfiguration.sh . + cp ./../buildscript/psvar-processor.sh . + +builddeploy_steps: &builddeploy_steps + - checkout + - setup_remote_docker + - run: *install_dependency + - run: *install_deploysuite + - run: docker buildx build --no-cache=true --build-arg RESET_DB_ARG=<> --build-arg SEED_DATA_ARG=${DEPLOYMENT_ENVIRONMENT} -t ${APPNAME}:latest . + - run: + name: Running MasterScript. + command: | + ./awsconfiguration.sh $DEPLOY_ENV + source awsenvconf + ./psvar-processor.sh -t appenv -p /config/${APPNAME}/deployvar + source deployvar_env + ./master_deploy.sh -d ECS -e $DEPLOY_ENV -t latest -j /config/${APPNAME}/appvar,/config/common/global-appvar -i ${APPNAME} -p FARGATE + +jobs: + # Build & Deploy against development backend + "build-dev": + !!merge <<: *defaults + environment: + DEPLOY_ENV: "DEV" + LOGICAL_ENV: "dev" + APPNAME: "groups-api-v6" + DEPLOYMENT_ENVIRONMENT: 'dev' + steps: *builddeploy_steps + + "build-prod": + !!merge <<: *defaults + environment: + DEPLOY_ENV: "PROD" + LOGICAL_ENV: "prod" + APPNAME: "groups-api-v6" + DEPLOYMENT_ENVIRONMENT: 'prod' + steps: *builddeploy_steps + +workflows: + version: 2 + build: + jobs: + # Development builds are executed on "develop" branch only. + - "build-dev": + context: org-global + filters: + branches: + only: + - develop + + # Production builds are exectuted only on tagged commits to the + # master branch. + - "build-prod": + context: org-global + filters: + branches: + only: + - master \ No newline at end of file From d73289aa9b273439181b1af46cf1fb66942bb25f Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 7 Aug 2025 15:01:24 +1000 Subject: [PATCH 02/13] Better CORS --- src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index 7618377..2018668 100644 --- a/src/main.ts +++ b/src/main.ts @@ -28,7 +28,7 @@ async function bootstrap() { credentials: true, origin: process.env.CORS_ALLOWED_ORIGIN ? new RegExp(process.env.CORS_ALLOWED_ORIGIN) - : ['http://localhost:3000', /\.localhost:3000$/], + : ['http://localhost:3000', /\.localhost:3000$/, 'https://*.topcoder-dev.com', 'https://*.topcoder.com'], methods: 'GET, POST, OPTIONS, PUT, DELETE, PATCH', }; app.use(cors(corsConfig)); From 55bce4baad6faa39f4a834c5f35c188ef0f137a3 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 7 Aug 2025 16:44:03 +1000 Subject: [PATCH 03/13] CORS update --- src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index 2018668..b58667f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -28,7 +28,7 @@ async function bootstrap() { credentials: true, origin: process.env.CORS_ALLOWED_ORIGIN ? new RegExp(process.env.CORS_ALLOWED_ORIGIN) - : ['http://localhost:3000', /\.localhost:3000$/, 'https://*.topcoder-dev.com', 'https://*.topcoder.com'], + : ['http://localhost:3000', /\.localhost:3000$/, 'https://topcoder.com', 'https://topcoder-dev.com', /\.topcoder-dev\.com$/, /\.topcoder\.com$/], methods: 'GET, POST, OPTIONS, PUT, DELETE, PATCH', }; app.use(cors(corsConfig)); From 8ef6a2cacc47088fc9f55bdb017f9a720d3f940c Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 15 Aug 2025 10:51:24 +1000 Subject: [PATCH 04/13] Fix administrator role name --- src/shared/enums/userRole.enum.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/enums/userRole.enum.ts b/src/shared/enums/userRole.enum.ts index 347c9a8..d0ed7b1 100644 --- a/src/shared/enums/userRole.enum.ts +++ b/src/shared/enums/userRole.enum.ts @@ -2,7 +2,7 @@ * Enum defining user roles for role-based access control */ export enum UserRole { - Admin = 'Administrator', + Admin = 'administrator', Copilot = 'Copilot', Reviewer = 'Reviewer', Submitter = 'Submitter', From bcc497ce605d61128e7f204170d089498250d52c Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 25 Sep 2025 06:42:37 +1000 Subject: [PATCH 05/13] Expose API docs --- src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index b58667f..2cebd9c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -147,7 +147,7 @@ async function bootstrap() { const document = SwaggerModule.createDocument(app, config, { include: [ApiModule], }); - SwaggerModule.setup(`/api-docs`, app, document); + SwaggerModule.setup(`/v6/groups/api-docs`, app, document); logger.log('Swagger documentation configured'); // Add an event handler to log uncaught promise rejections and prevent the server from crashing From 3a57300e33c6dbece28096cc25bb6b2e873206a6 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Tue, 30 Sep 2025 10:42:07 +1000 Subject: [PATCH 06/13] CORS updates --- src/main.ts | 46 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/src/main.ts b/src/main.ts index 2cebd9c..9a70370 100644 --- a/src/main.ts +++ b/src/main.ts @@ -22,14 +22,54 @@ async function bootstrap() { app.setGlobalPrefix(`${apiVer}`); // CORS related settings + const topcoderOriginPatterns = [ + /^https?:\/\/([\w-]+\.)*topcoder\.com(?::\d+)?$/i, + /^https?:\/\/([\w-]+\.)*topcoder-dev\.com(?::\d+)?$/i, + ]; + + const allowList: (string | RegExp)[] = ['http://localhost:3000', /\.localhost:3000$/]; + + if (process.env.CORS_ALLOWED_ORIGIN) { + try { + allowList.push(new RegExp(process.env.CORS_ALLOWED_ORIGIN)); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + logger.warn( + `Invalid CORS_ALLOWED_ORIGIN pattern (${process.env.CORS_ALLOWED_ORIGIN}): ${errorMessage}`, + ); + } + } + + const isAllowedOrigin = (origin: string): boolean => { + if (allowList.some((allowedOrigin) => { + if (allowedOrigin instanceof RegExp) { + return allowedOrigin.test(origin); + } + return allowedOrigin === origin; + })) { + return true; + } + + return topcoderOriginPatterns.some((pattern) => pattern.test(origin)); + }; + const corsConfig: cors.CorsOptions = { allowedHeaders: 'Origin, X-Requested-With, Content-Type, Accept, Authorization, Access-Control-Allow-Origin, Access-Control-Allow-Headers,currentOrg,overrideOrg,x-atlassian-cloud-id,x-api-key,x-orgid', credentials: true, - origin: process.env.CORS_ALLOWED_ORIGIN - ? new RegExp(process.env.CORS_ALLOWED_ORIGIN) - : ['http://localhost:3000', /\.localhost:3000$/, 'https://topcoder.com', 'https://topcoder-dev.com', /\.topcoder-dev\.com$/, /\.topcoder\.com$/], methods: 'GET, POST, OPTIONS, PUT, DELETE, PATCH', + origin: (requestOrigin, callback) => { + if (!requestOrigin) { + return callback(null, false); + } + + if (isAllowedOrigin(requestOrigin)) { + return callback(null, requestOrigin); + } + + return callback(null, false); + }, }; app.use(cors(corsConfig)); logger.log('CORS configuration applied'); From a7a6694fb1b719b2ddedc0da358027f263040dc9 Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Fri, 3 Oct 2025 08:33:28 +0300 Subject: [PATCH 07/13] add review buddy --- .github/workflows/code_reviewer.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/code_reviewer.yml diff --git a/.github/workflows/code_reviewer.yml b/.github/workflows/code_reviewer.yml new file mode 100644 index 0000000..82c7862 --- /dev/null +++ b/.github/workflows/code_reviewer.yml @@ -0,0 +1,22 @@ +name: AI PR Reviewer + +on: + pull_request: + types: + - opened + - synchronize +permissions: + pull-requests: write +jobs: + tc-ai-pr-review: + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v3 + + - name: TC AI PR Reviewer + uses: topcoder-platform/tc-ai-pr-reviewer@master + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # The GITHUB_TOKEN is there by default so you just need to keep it like it is and not necessarily need to add it as secret as it will throw an error. [More Details](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret) + LAB45_API_KEY: ${{ secrets.LAB45_API_KEY }} + exclude: '**/*.json, **/*.md, **/*.jpg, **/*.png, **/*.jpeg, **/*.bmp, **/*.webp' # Optional: exclude patterns separated by commas \ No newline at end of file From 9993ae43b9aa97cdaf6c342f5dfc1207c08187f1 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Sun, 5 Oct 2025 16:30:58 +1100 Subject: [PATCH 08/13] Fix for handling new groups (PM-2277) --- src/api/group/group.service.ts | 13 +++++++------ src/api/subgroup/subGroup.service.ts | 9 +++++++-- src/main.ts | 19 ++++++++++++------- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/api/group/group.service.ts b/src/api/group/group.service.ts index 061845c..41f3bee 100644 --- a/src/api/group/group.service.ts +++ b/src/api/group/group.service.ts @@ -128,10 +128,6 @@ export class GroupService { if (criteria.oldId) { prismaFilter.where.oldId = criteria.oldId; - } else { - prismaFilter.where.oldId = { - not: null, - }; } if (criteria.name) { prismaFilter.where.name = { @@ -352,13 +348,18 @@ export class GroupService { await checkGroupName(dto.name, '', tx); // create group + const createdBy = authUser.userId ? authUser.userId : '00000000'; + const createdAt = new Date().toISOString(); const groupData = { ...dto, domain: dto.domain || '', ssoId: dto.ssoId || '', organizationId: dto.organizationId || '', - createdBy: authUser.userId ? authUser.userId : '00000000', - createdAt: new Date().toISOString(), + createdBy, + createdAt, + // Initialize updated fields to match created fields on creation + updatedBy: createdBy, + updatedAt: createdAt, }; const result = await tx.group.create({ data: groupData }); diff --git a/src/api/subgroup/subGroup.service.ts b/src/api/subgroup/subGroup.service.ts index 6770c60..b1b96c9 100644 --- a/src/api/subgroup/subGroup.service.ts +++ b/src/api/subgroup/subGroup.service.ts @@ -65,13 +65,18 @@ export class SubGroupService { await checkGroupName(dto.name, '', tx); // create group + const createdBy = authUser.userId ? authUser.userId : '00000000'; + const createdAt = new Date().toISOString(); const groupData = { ...dto, domain: dto.domain || '', ssoId: dto.ssoId || '', organizationId: dto.organizationId || '', - createdBy: authUser.userId ? authUser.userId : '00000000', - createdAt: new Date().toISOString(), + createdBy, + createdAt, + // Initialize updated fields to match created fields on creation + updatedBy: createdBy, + updatedAt: createdAt, }; const subGroup = await tx.group.create({ data: groupData }); diff --git a/src/main.ts b/src/main.ts index 9a70370..88967f1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -27,7 +27,10 @@ async function bootstrap() { /^https?:\/\/([\w-]+\.)*topcoder-dev\.com(?::\d+)?$/i, ]; - const allowList: (string | RegExp)[] = ['http://localhost:3000', /\.localhost:3000$/]; + const allowList: (string | RegExp)[] = [ + 'http://localhost:3000', + /\.localhost:3000$/, + ]; if (process.env.CORS_ALLOWED_ORIGIN) { try { @@ -42,12 +45,14 @@ async function bootstrap() { } const isAllowedOrigin = (origin: string): boolean => { - if (allowList.some((allowedOrigin) => { - if (allowedOrigin instanceof RegExp) { - return allowedOrigin.test(origin); - } - return allowedOrigin === origin; - })) { + if ( + allowList.some((allowedOrigin) => { + if (allowedOrigin instanceof RegExp) { + return allowedOrigin.test(origin); + } + return allowedOrigin === origin; + }) + ) { return true; } From de87c0a627d7ee09f777ce1744f6885b7483649a Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Tue, 7 Oct 2025 11:03:49 +1100 Subject: [PATCH 09/13] Fix adding groups as members of groups --- src/api/group-membership/groupMembership.service.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/api/group-membership/groupMembership.service.ts b/src/api/group-membership/groupMembership.service.ts index de549f8..fdd6835 100644 --- a/src/api/group-membership/groupMembership.service.ts +++ b/src/api/group-membership/groupMembership.service.ts @@ -214,12 +214,6 @@ export class GroupMembershipService { let memberOldId; - if (!group.oldId || group.oldId.length <= 0) { - throw new ForbiddenException( - 'Parent group is not ready yet, try after sometime', - ); - } - const memberId = dto.memberId ? dto.memberId : (dto.universalUID as string); From 048d4079214cbfa9a0b1065a0358c7538392bdea Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 8 Oct 2025 19:14:53 +1100 Subject: [PATCH 10/13] README update --- README.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/README.md b/README.md index aedf077..1380d88 100644 --- a/README.md +++ b/README.md @@ -83,3 +83,34 @@ For a read-only scope M2M token, use: - then you can use Postman to test all apis - Swagger docs are accessible at `http://localhost:3000/api-docs` + +**Downstream Usage** + +- This service is consumed by multiple Topcoder apps. Below is a quick map of where and how it’s called to help with debugging. + +**platform-ui** + +- Admin pages manage groups and memberships using v6 endpoints: + - List/search groups: `GET /v6/groups?page=1&perPage=10000` (optionally filter by `name`, or by `memberId` + `membershipType=user`). See platform-ui/src/apps/admin/src/lib/services/groups.service.ts. + - Fetch group by id: `GET /v6/groups/{id}` (optional `fields` query). See platform-ui/src/apps/admin/src/lib/services/groups.service.ts. + - List group members: `GET /v6/groups/{id}/members?page&perPage`. See platform-ui/src/apps/admin/src/lib/services/groups.service.ts. + - Create group: `POST /v6/groups`. See platform-ui/src/apps/admin/src/lib/services/groups.service.ts. + - Add member: `POST /v6/groups/{id}/members` with `{ membershipType: 'user'|'group', memberId }`. See platform-ui/src/apps/admin/src/lib/services/groups.service.ts. + - Remove member: `DELETE /v6/groups/{id}/members/{memberId}`. See platform-ui/src/apps/admin/src/lib/services/groups.service.ts. +- Local dev proxy maps both `/v5/groups` and `/v6/groups` to this service on port 3001. See platform-ui/src/config/environments/local.env.ts. + +**community-app** + +- Used server-side to expand community metadata group IDs to include descendants (group trees). The code acquires an M2M token and calls the groups service helper, which in turn queries the Groups API for a group’s tree of IDs. See community-app/src/server/services/communities.js and community-app/src/server/services/communities.js. +- Community App requires M2M credentials with access to Groups API for this logic. See community-app/README.md. +- Equivalent v6 endpoint for tree expansion is: `GET /v6/groups/{id}?flattenGroupIdTree=true` (also supports `includeSubGroups`, `includeParentGroup`, `oneLevel`). + +**work-manager** + +- Populates group selectors and filters challenge visibility: + - Search groups by name for autocomplete: `GET /v6/groups?name={query}&perPage={large}`. See work-manager/src/components/ChallengeEditor/Groups-Field/index.js. + - Load current user’s groups when creating/editing a challenge: `GET /v6/groups?membershipType=user&memberId={tcUserId}&perPage={large}`. See work-manager/src/actions/challenges.js. + - Fetch group detail by id: `GET /v6/groups/{id}`. See work-manager/src/services/challenges.js. +- API base configuration points to v6 in dev/local and v5 in prod (for backward compatibility): + - Dev: work-manager/config/constants/development.js. + - Local: work-manager/config/constants/local.js and work-manager/config/constants/local.js. From 17f01ff1aa3b09c20ba70e8c4110ad0ce9a5413d Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Sat, 11 Oct 2025 10:59:44 +1100 Subject: [PATCH 11/13] Remove destructive target --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index e5381bd..c48bfd4 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,7 @@ "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./test/jest-e2e.json", - "postinstall": "npx prisma generate", - "seed-data": "npx prisma db seed" + "postinstall": "npx prisma generate" }, "dependencies": { "@nestjs/axios": "^4.0.0", From 5919454c16bbf580d9a783611f9548abff883519 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Mon, 13 Oct 2025 14:15:12 +1100 Subject: [PATCH 12/13] Fixes for scope checks --- src/shared/modules/global/jwt.service.ts | 39 +++++++++++++++++++----- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/src/shared/modules/global/jwt.service.ts b/src/shared/modules/global/jwt.service.ts index 57df419..60d04b7 100644 --- a/src/shared/modules/global/jwt.service.ts +++ b/src/shared/modules/global/jwt.service.ts @@ -35,6 +35,15 @@ const TEST_M2M_TOKENS: Record = { 'm2m-token-groups': [Scope.AllGroups], }; +const SCOPE_SYNONYMS: Record = { + 'read:group': [Scope.ReadGroups], + [Scope.ReadGroups]: ['read:group'], + 'write:group': [Scope.WriteGroups], + [Scope.WriteGroups]: ['write:group'], + 'all:group': [Scope.AllGroups], + [Scope.AllGroups]: ['all:group'], +}; + @Injectable() export class JwtService implements OnModuleInit { private jwksClientInstance: jwksClient.JwksClient; @@ -177,16 +186,30 @@ export class JwtService implements OnModuleInit { */ private expandScopes(scopes: string[]): string[] { const expandedScopes = new Set(); + const queue = [...scopes]; - // Add all original scopes - scopes.forEach((scope) => expandedScopes.add(scope)); - - // Expand all "all:*" scopes - scopes.forEach((scope) => { - if (ALL_SCOPE_MAPPINGS[scope]) { - ALL_SCOPE_MAPPINGS[scope].forEach((s) => expandedScopes.add(s)); + while (queue.length > 0) { + const scope = queue.shift(); + if (!scope || expandedScopes.has(scope)) { + continue; } - }); + + expandedScopes.add(scope); + + const synonyms = SCOPE_SYNONYMS[scope] ?? []; + synonyms.forEach((alias) => { + if (!expandedScopes.has(alias)) { + queue.push(alias); + } + }); + + const mappedScopes = ALL_SCOPE_MAPPINGS[scope] ?? []; + mappedScopes.forEach((alias) => { + if (!expandedScopes.has(alias)) { + queue.push(alias); + } + }); + } return Array.from(expandedScopes); } From 1208653034f5af9861c2d139a97a473768b15742 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 22 Oct 2025 07:35:47 +1100 Subject: [PATCH 13/13] Return the status on GET requests --- src/api/group/group.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/api/group/group.service.ts b/src/api/group/group.service.ts index 41f3bee..6effb5d 100644 --- a/src/api/group/group.service.ts +++ b/src/api/group/group.service.ts @@ -37,7 +37,7 @@ import { import { M2MService } from 'src/shared/modules/global/m2m.service'; -const ADMIN_GROUP_FIELDS = ['status']; +const ADMIN_GROUP_FIELDS: string[] = []; export const ALLOWED_FIELD_NAMES = [ 'id', @@ -52,6 +52,7 @@ export const ALLOWED_FIELD_NAMES = [ 'domain', 'organizationId', 'oldId', + 'status', ]; @Injectable()