Skip to content

Commit a23be38

Browse files
authored
Merge pull request #1 from topcoder-platform/develop
sync dev to master
2 parents 4c62f85 + 1208653 commit a23be38

File tree

10 files changed

+230
-30
lines changed

10 files changed

+230
-30
lines changed

.circleci/config.yml

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
version: 2.1
2+
3+
parameters:
4+
reset-db:
5+
type: boolean
6+
default: false
7+
defaults: &defaults
8+
docker:
9+
- image: cimg/python:3.13.2-browsers
10+
install_dependency: &install_dependency
11+
name: Installation of build and deployment dependencies.
12+
command: |
13+
sudo apt update
14+
sudo apt install -y jq python3-pip
15+
sudo pip3 install awscli --upgrade
16+
install_deploysuite: &install_deploysuite
17+
name: Installation of install_deploysuite.
18+
command: |
19+
git clone --branch v1.4.19 https://github.com/topcoder-platform/tc-deploy-scripts ../buildscript
20+
cp ./../buildscript/master_deploy.sh .
21+
cp ./../buildscript/buildenv.sh .
22+
cp ./../buildscript/awsconfiguration.sh .
23+
cp ./../buildscript/psvar-processor.sh .
24+
25+
builddeploy_steps: &builddeploy_steps
26+
- checkout
27+
- setup_remote_docker
28+
- run: *install_dependency
29+
- run: *install_deploysuite
30+
- run: docker buildx build --no-cache=true --build-arg RESET_DB_ARG=<<pipeline.parameters.reset-db>> --build-arg SEED_DATA_ARG=${DEPLOYMENT_ENVIRONMENT} -t ${APPNAME}:latest .
31+
- run:
32+
name: Running MasterScript.
33+
command: |
34+
./awsconfiguration.sh $DEPLOY_ENV
35+
source awsenvconf
36+
./psvar-processor.sh -t appenv -p /config/${APPNAME}/deployvar
37+
source deployvar_env
38+
./master_deploy.sh -d ECS -e $DEPLOY_ENV -t latest -j /config/${APPNAME}/appvar,/config/common/global-appvar -i ${APPNAME} -p FARGATE
39+
40+
jobs:
41+
# Build & Deploy against development backend
42+
"build-dev":
43+
!!merge <<: *defaults
44+
environment:
45+
DEPLOY_ENV: "DEV"
46+
LOGICAL_ENV: "dev"
47+
APPNAME: "groups-api-v6"
48+
DEPLOYMENT_ENVIRONMENT: 'dev'
49+
steps: *builddeploy_steps
50+
51+
"build-prod":
52+
!!merge <<: *defaults
53+
environment:
54+
DEPLOY_ENV: "PROD"
55+
LOGICAL_ENV: "prod"
56+
APPNAME: "groups-api-v6"
57+
DEPLOYMENT_ENVIRONMENT: 'prod'
58+
steps: *builddeploy_steps
59+
60+
workflows:
61+
version: 2
62+
build:
63+
jobs:
64+
# Development builds are executed on "develop" branch only.
65+
- "build-dev":
66+
context: org-global
67+
filters:
68+
branches:
69+
only:
70+
- develop
71+
72+
# Production builds are exectuted only on tagged commits to the
73+
# master branch.
74+
- "build-prod":
75+
context: org-global
76+
filters:
77+
branches:
78+
only:
79+
- master
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: AI PR Reviewer
2+
3+
on:
4+
pull_request:
5+
types:
6+
- opened
7+
- synchronize
8+
permissions:
9+
pull-requests: write
10+
jobs:
11+
tc-ai-pr-review:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: Checkout Repo
15+
uses: actions/checkout@v3
16+
17+
- name: TC AI PR Reviewer
18+
uses: topcoder-platform/tc-ai-pr-reviewer@master
19+
with:
20+
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)
21+
LAB45_API_KEY: ${{ secrets.LAB45_API_KEY }}
22+
exclude: '**/*.json, **/*.md, **/*.jpg, **/*.png, **/*.jpeg, **/*.bmp, **/*.webp' # Optional: exclude patterns separated by commas

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,34 @@ For a read-only scope M2M token, use:
8383

8484
- then you can use Postman to test all apis
8585
- Swagger docs are accessible at `http://localhost:3000/api-docs`
86+
87+
**Downstream Usage**
88+
89+
- This service is consumed by multiple Topcoder apps. Below is a quick map of where and how it’s called to help with debugging.
90+
91+
**platform-ui**
92+
93+
- Admin pages manage groups and memberships using v6 endpoints:
94+
- 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.
95+
- Fetch group by id: `GET /v6/groups/{id}` (optional `fields` query). See platform-ui/src/apps/admin/src/lib/services/groups.service.ts.
96+
- List group members: `GET /v6/groups/{id}/members?page&perPage`. See platform-ui/src/apps/admin/src/lib/services/groups.service.ts.
97+
- Create group: `POST /v6/groups`. See platform-ui/src/apps/admin/src/lib/services/groups.service.ts.
98+
- Add member: `POST /v6/groups/{id}/members` with `{ membershipType: 'user'|'group', memberId }`. See platform-ui/src/apps/admin/src/lib/services/groups.service.ts.
99+
- Remove member: `DELETE /v6/groups/{id}/members/{memberId}`. See platform-ui/src/apps/admin/src/lib/services/groups.service.ts.
100+
- 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.
101+
102+
**community-app**
103+
104+
- 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.
105+
- Community App requires M2M credentials with access to Groups API for this logic. See community-app/README.md.
106+
- Equivalent v6 endpoint for tree expansion is: `GET /v6/groups/{id}?flattenGroupIdTree=true` (also supports `includeSubGroups`, `includeParentGroup`, `oneLevel`).
107+
108+
**work-manager**
109+
110+
- Populates group selectors and filters challenge visibility:
111+
- Search groups by name for autocomplete: `GET /v6/groups?name={query}&perPage={large}`. See work-manager/src/components/ChallengeEditor/Groups-Field/index.js.
112+
- 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.
113+
- Fetch group detail by id: `GET /v6/groups/{id}`. See work-manager/src/services/challenges.js.
114+
- API base configuration points to v6 in dev/local and v5 in prod (for backward compatibility):
115+
- Dev: work-manager/config/constants/development.js.
116+
- Local: work-manager/config/constants/local.js and work-manager/config/constants/local.js.

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@
1818
"test:cov": "jest --coverage",
1919
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
2020
"test:e2e": "jest --config ./test/jest-e2e.json",
21-
"postinstall": "npx prisma generate",
22-
"seed-data": "npx prisma db seed"
21+
"postinstall": "npx prisma generate"
2322
},
2423
"dependencies": {
2524
"@nestjs/axios": "^4.0.0",

src/api/group-membership/groupMembership.service.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -214,12 +214,6 @@ export class GroupMembershipService {
214214

215215
let memberOldId;
216216

217-
if (!group.oldId || group.oldId.length <= 0) {
218-
throw new ForbiddenException(
219-
'Parent group is not ready yet, try after sometime',
220-
);
221-
}
222-
223217
const memberId = dto.memberId
224218
? dto.memberId
225219
: (dto.universalUID as string);

src/api/group/group.service.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import {
3737

3838
import { M2MService } from 'src/shared/modules/global/m2m.service';
3939

40-
const ADMIN_GROUP_FIELDS = ['status'];
40+
const ADMIN_GROUP_FIELDS: string[] = [];
4141

4242
export const ALLOWED_FIELD_NAMES = [
4343
'id',
@@ -52,6 +52,7 @@ export const ALLOWED_FIELD_NAMES = [
5252
'domain',
5353
'organizationId',
5454
'oldId',
55+
'status',
5556
];
5657

5758
@Injectable()
@@ -128,10 +129,6 @@ export class GroupService {
128129

129130
if (criteria.oldId) {
130131
prismaFilter.where.oldId = criteria.oldId;
131-
} else {
132-
prismaFilter.where.oldId = {
133-
not: null,
134-
};
135132
}
136133
if (criteria.name) {
137134
prismaFilter.where.name = {
@@ -352,13 +349,18 @@ export class GroupService {
352349
await checkGroupName(dto.name, '', tx);
353350

354351
// create group
352+
const createdBy = authUser.userId ? authUser.userId : '00000000';
353+
const createdAt = new Date().toISOString();
355354
const groupData = {
356355
...dto,
357356
domain: dto.domain || '',
358357
ssoId: dto.ssoId || '',
359358
organizationId: dto.organizationId || '',
360-
createdBy: authUser.userId ? authUser.userId : '00000000',
361-
createdAt: new Date().toISOString(),
359+
createdBy,
360+
createdAt,
361+
// Initialize updated fields to match created fields on creation
362+
updatedBy: createdBy,
363+
updatedAt: createdAt,
362364
};
363365

364366
const result = await tx.group.create({ data: groupData });

src/api/subgroup/subGroup.service.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,18 @@ export class SubGroupService {
6565
await checkGroupName(dto.name, '', tx);
6666

6767
// create group
68+
const createdBy = authUser.userId ? authUser.userId : '00000000';
69+
const createdAt = new Date().toISOString();
6870
const groupData = {
6971
...dto,
7072
domain: dto.domain || '',
7173
ssoId: dto.ssoId || '',
7274
organizationId: dto.organizationId || '',
73-
createdBy: authUser.userId ? authUser.userId : '00000000',
74-
createdAt: new Date().toISOString(),
75+
createdBy,
76+
createdAt,
77+
// Initialize updated fields to match created fields on creation
78+
updatedBy: createdBy,
79+
updatedAt: createdAt,
7580
};
7681

7782
const subGroup = await tx.group.create({ data: groupData });

src/main.ts

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,59 @@ async function bootstrap() {
2222
app.setGlobalPrefix(`${apiVer}`);
2323

2424
// CORS related settings
25+
const topcoderOriginPatterns = [
26+
/^https?:\/\/([\w-]+\.)*topcoder\.com(?::\d+)?$/i,
27+
/^https?:\/\/([\w-]+\.)*topcoder-dev\.com(?::\d+)?$/i,
28+
];
29+
30+
const allowList: (string | RegExp)[] = [
31+
'http://localhost:3000',
32+
/\.localhost:3000$/,
33+
];
34+
35+
if (process.env.CORS_ALLOWED_ORIGIN) {
36+
try {
37+
allowList.push(new RegExp(process.env.CORS_ALLOWED_ORIGIN));
38+
} catch (error) {
39+
const errorMessage =
40+
error instanceof Error ? error.message : String(error);
41+
logger.warn(
42+
`Invalid CORS_ALLOWED_ORIGIN pattern (${process.env.CORS_ALLOWED_ORIGIN}): ${errorMessage}`,
43+
);
44+
}
45+
}
46+
47+
const isAllowedOrigin = (origin: string): boolean => {
48+
if (
49+
allowList.some((allowedOrigin) => {
50+
if (allowedOrigin instanceof RegExp) {
51+
return allowedOrigin.test(origin);
52+
}
53+
return allowedOrigin === origin;
54+
})
55+
) {
56+
return true;
57+
}
58+
59+
return topcoderOriginPatterns.some((pattern) => pattern.test(origin));
60+
};
61+
2562
const corsConfig: cors.CorsOptions = {
2663
allowedHeaders:
2764
'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',
2865
credentials: true,
29-
origin: process.env.CORS_ALLOWED_ORIGIN
30-
? new RegExp(process.env.CORS_ALLOWED_ORIGIN)
31-
: ['http://localhost:3000', /\.localhost:3000$/],
3266
methods: 'GET, POST, OPTIONS, PUT, DELETE, PATCH',
67+
origin: (requestOrigin, callback) => {
68+
if (!requestOrigin) {
69+
return callback(null, false);
70+
}
71+
72+
if (isAllowedOrigin(requestOrigin)) {
73+
return callback(null, requestOrigin);
74+
}
75+
76+
return callback(null, false);
77+
},
3378
};
3479
app.use(cors(corsConfig));
3580
logger.log('CORS configuration applied');
@@ -147,7 +192,7 @@ async function bootstrap() {
147192
const document = SwaggerModule.createDocument(app, config, {
148193
include: [ApiModule],
149194
});
150-
SwaggerModule.setup(`/api-docs`, app, document);
195+
SwaggerModule.setup(`/v6/groups/api-docs`, app, document);
151196
logger.log('Swagger documentation configured');
152197

153198
// Add an event handler to log uncaught promise rejections and prevent the server from crashing

src/shared/enums/userRole.enum.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Enum defining user roles for role-based access control
33
*/
44
export enum UserRole {
5-
Admin = 'Administrator',
5+
Admin = 'administrator',
66
Copilot = 'Copilot',
77
Reviewer = 'Reviewer',
88
Submitter = 'Submitter',

src/shared/modules/global/jwt.service.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ const TEST_M2M_TOKENS: Record<string, string[]> = {
3535
'm2m-token-groups': [Scope.AllGroups],
3636
};
3737

38+
const SCOPE_SYNONYMS: Record<string, string[]> = {
39+
'read:group': [Scope.ReadGroups],
40+
[Scope.ReadGroups]: ['read:group'],
41+
'write:group': [Scope.WriteGroups],
42+
[Scope.WriteGroups]: ['write:group'],
43+
'all:group': [Scope.AllGroups],
44+
[Scope.AllGroups]: ['all:group'],
45+
};
46+
3847
@Injectable()
3948
export class JwtService implements OnModuleInit {
4049
private jwksClientInstance: jwksClient.JwksClient;
@@ -177,16 +186,30 @@ export class JwtService implements OnModuleInit {
177186
*/
178187
private expandScopes(scopes: string[]): string[] {
179188
const expandedScopes = new Set<string>();
189+
const queue = [...scopes];
180190

181-
// Add all original scopes
182-
scopes.forEach((scope) => expandedScopes.add(scope));
183-
184-
// Expand all "all:*" scopes
185-
scopes.forEach((scope) => {
186-
if (ALL_SCOPE_MAPPINGS[scope]) {
187-
ALL_SCOPE_MAPPINGS[scope].forEach((s) => expandedScopes.add(s));
191+
while (queue.length > 0) {
192+
const scope = queue.shift();
193+
if (!scope || expandedScopes.has(scope)) {
194+
continue;
188195
}
189-
});
196+
197+
expandedScopes.add(scope);
198+
199+
const synonyms = SCOPE_SYNONYMS[scope] ?? [];
200+
synonyms.forEach((alias) => {
201+
if (!expandedScopes.has(alias)) {
202+
queue.push(alias);
203+
}
204+
});
205+
206+
const mappedScopes = ALL_SCOPE_MAPPINGS[scope] ?? [];
207+
mappedScopes.forEach((alias) => {
208+
if (!expandedScopes.has(alias)) {
209+
queue.push(alias);
210+
}
211+
});
212+
}
190213

191214
return Array.from(expandedScopes);
192215
}

0 commit comments

Comments
 (0)