diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..589a402 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,95 @@ +version: 2.1 +defaults: &defaults + docker: + - image: cimg/python:3.13.5-browsers +install_dependency: &install_dependency + name: Installation of build and deployment dependencies. + command: | + pip3 install awscli --upgrade +install_deploysuite: &install_deploysuite + name: Installation of install_deploysuite. + command: | + git clone --branch v1.4.17 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 . + +restore_cache_settings_for_build: &restore_cache_settings_for_build + key: docker-node-modules-{{ checksum "pnpm-lock.yaml" }} + +save_cache_settings: &save_cache_settings + key: docker-node-modules-{{ checksum "pnpm-lock.yaml" }} + paths: + - node_modules + + +builddeploy_steps: &builddeploy_steps +- checkout +- setup_remote_docker +- run: *install_dependency +- run: *install_deploysuite +- restore_cache: *restore_cache_settings_for_build +- run: + name: "Build docker image" + command: | + ./build.sh +- save_cache: *save_cache_settings +- deploy: + 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 -i ${APPNAME} -p FARGATE +jobs: + # Build & Deploy against development backend + "build-dev": + !!merge <<: *defaults + environment: + DEPLOY_ENV: "DEV" + LOGICAL_ENV: "dev" + APPNAME: "tc-mcp" + steps: *builddeploy_steps + + "build-qa": + !!merge <<: *defaults + environment: + DEPLOY_ENV: "QA" + LOGICAL_ENV: "qa" + APPNAME: "tc-mcp" + steps: *builddeploy_steps + + "build-prod": + !!merge <<: *defaults + environment: + DEPLOY_ENV: "PROD" + LOGICAL_ENV: "prod" + APPNAME: "tc-mcp" + steps: *builddeploy_steps + +workflows: + version: 2 + build: + jobs: + # Development builds are executed on "develop" branch only. + - "build-dev": + context: org-global + filters: + branches: + only: + - dev + + - "build-qa": + context: org-global + filters: + branches: + only: + - qa + + - "build-prod": + context: org-global + filters: + branches: + only: master diff --git a/build.sh b/build.sh index 048a4bf..c3c0f18 100755 --- a/build.sh +++ b/build.sh @@ -1,3 +1,3 @@ #!/bin/bash set -eo pipefail -docker buildx build --no-cache=true -t ${APPNAME}}:latest . \ No newline at end of file +docker buildx build --no-cache=true -t ${APPNAME}:latest . \ No newline at end of file diff --git a/package.json b/package.json index 7f09332..a899e47 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "@nestjs/common": "^11.0.1", "@nestjs/core": "^11.0.1", "@nestjs/platform-express": "^11.0.1", - "@rekog/mcp-nest": "^1.6.2", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", "dotenv": "^16.5.0", @@ -34,6 +33,7 @@ "nanoid": "^5.1.5", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", + "@tc/mcp-nest": "topcoder-platform/MCP-Nest.git", "zod": "^3.25.67" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0f0d218..6699b3f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,9 +20,9 @@ importers: '@nestjs/platform-express': specifier: ^11.0.1 version: 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3) - '@rekog/mcp-nest': - specifier: ^1.6.2 - version: 1.6.2(@modelcontextprotocol/sdk@1.13.0)(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)(express@5.1.0)(reflect-metadata@0.2.2)(zod-to-json-schema@3.24.5(zod@3.25.67))(zod@3.25.67) + '@tc/mcp-nest': + specifier: topcoder-platform/MCP-Nest.git + version: '@rekog/mcp-nest@https://codeload.github.com/topcoder-platform/MCP-Nest/tar.gz/8950083c999f7636fed9f62b26330748aed9ed30(@modelcontextprotocol/sdk@1.13.0)(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)(express@5.1.0)(reflect-metadata@0.2.2)(zod-to-json-schema@3.24.5(zod@3.25.67))(zod@3.25.67)' class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -842,8 +842,9 @@ packages: resolution: {integrity: sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@rekog/mcp-nest@1.6.2': - resolution: {integrity: sha512-swcu99a/woQ5T1f4E/YJct1w/xzszH83bhzEuZwXh+cYRnQUJKhaJd5Aey/XyOtt/D9lQTKSdnoKdpija8kuWA==} + '@rekog/mcp-nest@https://codeload.github.com/topcoder-platform/MCP-Nest/tar.gz/8950083c999f7636fed9f62b26330748aed9ed30': + resolution: {tarball: https://codeload.github.com/topcoder-platform/MCP-Nest/tar.gz/8950083c999f7636fed9f62b26330748aed9ed30} + version: 1.6.2 peerDependencies: '@modelcontextprotocol/sdk': '>=1.10.0' '@nestjs/common': '>=9.0.0' @@ -4386,7 +4387,7 @@ snapshots: '@pkgr/core@0.2.7': {} - '@rekog/mcp-nest@1.6.2(@modelcontextprotocol/sdk@1.13.0)(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)(express@5.1.0)(reflect-metadata@0.2.2)(zod-to-json-schema@3.24.5(zod@3.25.67))(zod@3.25.67)': + '@rekog/mcp-nest@https://codeload.github.com/topcoder-platform/MCP-Nest/tar.gz/8950083c999f7636fed9f62b26330748aed9ed30(@modelcontextprotocol/sdk@1.13.0)(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)(express@5.1.0)(reflect-metadata@0.2.2)(zod-to-json-schema@3.24.5(zod@3.25.67))(zod@3.25.67)': dependencies: '@modelcontextprotocol/sdk': 1.13.0 '@nestjs/common': 11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) diff --git a/src/api/health-check/healthCheck.controller.ts b/src/api/health-check/healthCheck.controller.ts index b47a761..4dc6266 100644 --- a/src/api/health-check/healthCheck.controller.ts +++ b/src/api/health-check/healthCheck.controller.ts @@ -19,7 +19,7 @@ export class HealthCheckController { @Public() @Version([VERSION_NEUTRAL, '1']) - @Get('/healthcheck') + @Get('/health') healthCheck(): Promise { const response = new GetHealthCheckResponseDto(); diff --git a/src/app.module.ts b/src/app.module.ts index d53eeec..f5c9db1 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,14 +1,11 @@ import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; -import { McpModule } from '@rekog/mcp-nest'; +import { McpModule } from '@tc/mcp-nest'; import { QueryChallengesTool } from './mcp/tools/challenges/queryChallenges.tool'; -import { randomUUID } from 'crypto'; import { GlobalProvidersModule } from './shared/global/globalProviders.module'; import { TopcoderModule } from './shared/topcoder/topcoder.module'; import { HealthCheckController } from './api/health-check/healthCheck.controller'; import { TokenValidatorMiddleware } from './core/auth/middleware/tokenValidator.middleware'; -import { CreateRequestStoreMiddleware } from './core/request/createRequestStore.middleware'; -import { AuthGuard, RolesGuard } from './core/auth/guards'; -import { APP_GUARD } from '@nestjs/core'; +import { nanoid } from 'nanoid'; @Module({ imports: [ @@ -17,30 +14,18 @@ import { APP_GUARD } from '@nestjs/core'; version: '1.0.0', streamableHttp: { enableJsonResponse: false, - sessionIdGenerator: () => randomUUID(), + sessionIdGenerator: () => nanoid(), statelessMode: false, }, - // guards: [AuthGuard, RolesGuard], }), GlobalProvidersModule, TopcoderModule, ], controllers: [HealthCheckController], - providers: [ - // { - // provide: APP_GUARD, - // useClass: AuthGuard, - // }, - // { - // provide: APP_GUARD, - // useClass: RolesGuard, - // }, - QueryChallengesTool, - ], + providers: [QueryChallengesTool], }) export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { - // consumer.apply(TokenValidatorMiddleware).forRoutes('*'); - // consumer.apply(CreateRequestStoreMiddleware).forRoutes('*'); + consumer.apply(TokenValidatorMiddleware).forRoutes('*'); } } diff --git a/src/config/config.env.ts b/src/config/config.env.ts index 0130772..81a48f6 100644 --- a/src/config/config.env.ts +++ b/src/config/config.env.ts @@ -16,4 +16,8 @@ export class ConfigEnv { @IsString() AUTH0_CLIENT_ID!: string; + + @IsString() + @IsOptional() + API_BASE = '/v6/mcp'; } diff --git a/src/core/auth/auth.constants.ts b/src/core/auth/auth.constants.ts index 71de54c..e0d1620 100644 --- a/src/core/auth/auth.constants.ts +++ b/src/core/auth/auth.constants.ts @@ -1,5 +1,8 @@ export enum Role { + Admin = 'administrator', User = 'Topcoder User', } -export enum M2mScope {} +export enum M2mScope { + QueryPublicChallenges = 'query:public:challenges', +} diff --git a/src/core/auth/decorators/index.ts b/src/core/auth/decorators/index.ts index 05c68d6..07b77ea 100644 --- a/src/core/auth/decorators/index.ts +++ b/src/core/auth/decorators/index.ts @@ -1,5 +1,4 @@ export * from './m2m.decorator'; export * from './m2mScope.decorator'; export * from './public.decorator'; -export * from './roles.decorator'; export * from './user.decorator'; diff --git a/src/core/auth/decorators/roles.decorator.ts b/src/core/auth/decorators/roles.decorator.ts deleted file mode 100644 index 9ffe82c..0000000 --- a/src/core/auth/decorators/roles.decorator.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { SetMetadata } from '@nestjs/common'; -import { Role } from '../auth.constants'; - -export const ROLES_KEY = 'roles'; -export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles); diff --git a/src/core/auth/guards/auth.guard.ts b/src/core/auth/guards/auth.guard.ts index 921ae5b..a63de76 100644 --- a/src/core/auth/guards/auth.guard.ts +++ b/src/core/auth/guards/auth.guard.ts @@ -1,59 +1,16 @@ -import { - CanActivate, - ExecutionContext, - Injectable, - UnauthorizedException, -} from '@nestjs/common'; -import { Reflector } from '@nestjs/core'; -import { IS_PUBLIC_KEY } from '../decorators/public.decorator'; -import { IS_M2M_KEY } from '../decorators/m2m.decorator'; -import { M2mScope } from '../auth.constants'; -import { SCOPES_KEY } from '../decorators/m2mScope.decorator'; - -@Injectable() -export class AuthGuard implements CanActivate { - constructor(private reflector: Reflector) {} - - canActivate(context: ExecutionContext): boolean { - const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ - context.getHandler(), - context.getClass(), - ]); - - if (isPublic) return true; - - const req = context.switchToHttp().getRequest(); - const isM2M = this.reflector.getAllAndOverride(IS_M2M_KEY, [ - context.getHandler(), - context.getClass(), - ]); - - const { m2mUserId } = req; - if (m2mUserId) { - req.user = { - id: m2mUserId, - handle: '', - }; - } - - // Regular authentication - check that we have user's email and have verified the id token - if (!isM2M) { - return Boolean(req.email && req.idTokenVerified); - } - - // M2M authentication - check scopes - if (!req.idTokenVerified || !req.m2mTokenScope) - throw new UnauthorizedException(); - - const allowedM2mScopes = this.reflector.getAllAndOverride( - SCOPES_KEY, - [context.getHandler(), context.getClass()], - ); - - const reqScopes = req.m2mTokenScope.split(' '); - if (reqScopes.some((reqScope) => allowedM2mScopes.includes(reqScope))) { - return true; - } +import { Request } from 'express'; +import { decodeAuthToken } from './guards.utils'; + +/** + * Auth guard function to validate the authorization token from the request headers. + * + * @param req - The incoming HTTP request object. + * @returns A promise that resolves to `true` if the authorization token is valid, otherwise `false`. + */ +export const authGuard = async (req: Request) => { + if (!(await decodeAuthToken(req.headers.authorization ?? ''))) { return false; } -} + + return true; +}; diff --git a/src/core/auth/guards/guards.utils.ts b/src/core/auth/guards/guards.utils.ts new file mode 100644 index 0000000..cd83d82 --- /dev/null +++ b/src/core/auth/guards/guards.utils.ts @@ -0,0 +1,35 @@ +import * as jwt from 'jsonwebtoken'; +import { Logger } from 'src/shared/global'; +import { getSigningKey } from '../jwt'; + +const logger = new Logger('guards.utils()'); + +/** + * Decodes and verifies a JWT token from the provided authorization header. + * + * @param authHeader - The authorization header containing the token, expected in the format "Bearer ". + * @returns A promise that resolves to the decoded JWT payload if the token is valid, + * a string if the payload is a string, or `false` if the token is invalid or the header is improperly formatted. + * + * @throws This function does not throw directly but will return `false` if an error occurs during verification. + */ +export const decodeAuthToken = async ( + authHeader: string, +): Promise => { + const [type, idToken] = authHeader?.split(' ') ?? []; + + if (type !== 'Bearer' || !idToken) { + return false; + } + + let decoded: jwt.JwtPayload | string; + try { + const signingKey = await getSigningKey(idToken); + decoded = jwt.verify(idToken, signingKey); + } catch (error) { + logger.error('Error verifying JWT', error); + return false; + } + + return decoded; +}; diff --git a/src/core/auth/guards/index.ts b/src/core/auth/guards/index.ts index 4ae679c..49afb20 100644 --- a/src/core/auth/guards/index.ts +++ b/src/core/auth/guards/index.ts @@ -1,2 +1,3 @@ export * from './auth.guard'; -export * from './roles.guard'; +export * from './m2m-scope.guard'; +export * from './role.guard'; diff --git a/src/core/auth/guards/m2m-scope.guard.ts b/src/core/auth/guards/m2m-scope.guard.ts new file mode 100644 index 0000000..b1d85a5 --- /dev/null +++ b/src/core/auth/guards/m2m-scope.guard.ts @@ -0,0 +1,30 @@ +import { Request } from 'express'; +import { decodeAuthToken } from './guards.utils'; +import { JwtPayload } from 'jsonwebtoken'; +import { M2mScope } from '../auth.constants'; + +/** + * A utility function to check if the required M2M (Machine-to-Machine) scopes are present + * in the authorization token provided in the request headers. + * + * @param {...M2mScope[]} requiredM2mScopes - The list of required M2M scopes to validate against. + * @returns {Promise<(req: Request) => boolean>} A function that takes an Express `Request` object + * and returns a boolean indicating whether the required scopes are present. + * + * The function decodes the authorization token from the request headers and checks if + * the required scopes are included in the token's scope claim. + */ +export const checkM2MScope = + (...requiredM2mScopes: M2mScope[]) => + async (req: Request) => { + const decodedAuth = await decodeAuthToken(req.headers.authorization ?? ''); + + const authorizedScopes = ((decodedAuth as JwtPayload).scope ?? '').split( + ' ', + ); + if (!requiredM2mScopes.some((scope) => authorizedScopes.includes(scope))) { + return false; + } + + return true; + }; diff --git a/src/core/auth/guards/role.guard.ts b/src/core/auth/guards/role.guard.ts new file mode 100644 index 0000000..15fbc50 --- /dev/null +++ b/src/core/auth/guards/role.guard.ts @@ -0,0 +1,34 @@ +import { Request } from 'express'; +import { decodeAuthToken } from './guards.utils'; +import { Role } from '../auth.constants'; + +/** + * A utility function to check if the required user role are present + * in the authorization token provided in the request headers. + * + * @param {...Role[]} requiredUserRoles - The list of required user roles to validate against. + * @returns {Promise<(req: Request) => boolean>} A function that takes an Express `Request` object + * and returns a boolean indicating whether the required scopes are present. + * + * The function decodes the authorization token from the request headers and checks if + * the required user roles are included in the token's scope claim. + */ +export const checkHasUserRole = + (...requiredUserRoles: Role[]) => + async (req: Request) => { + const decodedAuth = await decodeAuthToken(req.headers.authorization ?? ''); + + const decodedUserRoles = Object.keys(decodedAuth).reduce((roles, key) => { + if (key.match(/claims\/roles$/gi)) { + return decodedAuth[key] as string[]; + } + + return roles; + }, []); + + if (!requiredUserRoles.some((role) => decodedUserRoles.includes(role))) { + return false; + } + + return true; + }; diff --git a/src/core/auth/guards/roles.guard.ts b/src/core/auth/guards/roles.guard.ts deleted file mode 100644 index 8145cce..0000000 --- a/src/core/auth/guards/roles.guard.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; -import { Reflector } from '@nestjs/core'; -import { ROLES_KEY } from '../decorators/roles.decorator'; - -@Injectable() -export class RolesGuard implements CanActivate { - constructor(private reflector: Reflector) {} - - canActivate(context: ExecutionContext): boolean { - const requiredRoles = this.reflector.getAllAndOverride( - ROLES_KEY, - [context.getHandler(), context.getClass()], - ); - - if (!requiredRoles) { - return true; - } - - const request = context.switchToHttp().getRequest(); - const { auth0User } = request; - const userRoles = Object.keys(auth0User).reduce((roles, key) => { - if (key.match(/claims\/roles$/gi)) { - return auth0User[key] as string[]; - } - - return roles; - }, []); - - if (!requiredRoles.some((role) => userRoles.includes(role))) { - return false; - } - - const userHandle = Object.keys(auth0User).reduce((handles, key) => { - if (key.match(/claims\/handle$/gi)) { - return auth0User[key] as string; - } - - return handles; - }, []); - - const userId = Object.keys(auth0User).reduce((ids, key) => { - if (key.match(/claims\/userId$/gi)) { - return auth0User[key] as string; - } - - return ids; - }, []); - - request.user = { - id: userId, - handle: userHandle, - email: request.email, - }; - - return true; - } -} diff --git a/src/core/request/createRequestStore.middleware.ts b/src/core/request/createRequestStore.middleware.ts deleted file mode 100644 index 1b45180..0000000 --- a/src/core/request/createRequestStore.middleware.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Injectable, NestMiddleware } from '@nestjs/common'; -import { Response, NextFunction } from 'express'; -import { RequestMetadata, saveStore } from './requestStore'; - -@Injectable() -export class CreateRequestStoreMiddleware implements NestMiddleware { - constructor() {} - - use(req: any, res: Response, next: NextFunction) { - const requestMetaData = new RequestMetadata({}); - - saveStore(requestMetaData, next); - } -} diff --git a/src/core/request/requestStore.ts b/src/core/request/requestStore.ts deleted file mode 100644 index dc6bdd9..0000000 --- a/src/core/request/requestStore.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { AsyncLocalStorage } from 'async_hooks'; -import { NextFunction } from 'express'; -import { nanoid } from 'nanoid'; - -// Class for storing request specific metadata -export class RequestMetadata { - requestId: string; - - constructor(params: { requestId?: string }) { - this.requestId = params.requestId ?? nanoid(11); - } -} - -// Create a AsyncLocalStorage of type RequestMetaData for storing request specific data -const asyncStorage = new AsyncLocalStorage(); - -// Gets the RequestMetadada object associated with the current request -export function getStore(): RequestMetadata { - let store = asyncStorage.getStore(); - if (store === undefined) { - store = new RequestMetadata({ - requestId: '', - }); - } - - return store; -} - -// For use in middleware -// Saves RequestMetadata for the current request -export function saveStore( - requestMetaData: RequestMetadata, - next: NextFunction, -) { - asyncStorage.run(requestMetaData, () => { - next(); - }); -} diff --git a/src/main.ts b/src/main.ts index c89e56d..f50fe2a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,10 +4,15 @@ import { Logger } from 'src/shared/global'; import { ENV_CONFIG } from './config'; async function bootstrap() { - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(AppModule, { + logger: ['error', 'warn', 'log'], + }); const logger = new Logger('bootstrap()'); + // Global prefix for all routes + app.setGlobalPrefix(ENV_CONFIG.API_BASE); + // Add an event handler to log uncaught promise rejections and prevent the server from crashing process.on('unhandledRejection', (reason, promise) => { logger.error('Unhandled Rejection at:', promise, 'reason:', reason); diff --git a/src/mcp/tools/challenges/queryChallenges.tool.ts b/src/mcp/tools/challenges/queryChallenges.tool.ts index 345c9e6..ed55795 100644 --- a/src/mcp/tools/challenges/queryChallenges.tool.ts +++ b/src/mcp/tools/challenges/queryChallenges.tool.ts @@ -1,11 +1,16 @@ -// greeting.tool.ts import { Injectable, Inject } from '@nestjs/common'; -import { Tool } from '@rekog/mcp-nest'; +import { Tool } from '@tc/mcp-nest'; import { REQUEST } from '@nestjs/core'; import { QUERY_CHALLENGES_TOOL_PARAMETERS } from './queryChallenges.parameters'; import { TopcoderChallengesService } from 'src/shared/topcoder/challenges.service'; import { Logger } from 'src/shared/global'; import { QUERY_CHALLENGES_TOOL_OUTPUT_SCHEMA } from './queryChallenges.output'; +import { + authGuard, + checkHasUserRole, + checkM2MScope, +} from 'src/core/auth/guards'; +import { M2mScope, Role } from 'src/core/auth/auth.constants'; @Injectable() export class QueryChallengesTool { @@ -16,18 +21,7 @@ export class QueryChallengesTool { @Inject(REQUEST) private readonly request: any, ) {} - @Tool({ - name: 'query-tc-challenges', - description: - 'Returns a list of public Topcoder challenges based on the query parameters.', - parameters: QUERY_CHALLENGES_TOOL_PARAMETERS, - outputSchema: QUERY_CHALLENGES_TOOL_OUTPUT_SCHEMA, - annotations: { - title: 'Query Public Topcoder Challenges', - readOnlyHint: true, - }, - }) - async queryChallenges(params) { + private async _queryChallenges(params) { // Validate the input parameters const validatedParams = QUERY_CHALLENGES_TOOL_PARAMETERS.safeParse(params); if (!validatedParams.success) { @@ -126,4 +120,67 @@ export class QueryChallengesTool { }; } } + + @Tool({ + name: 'query-tc-challenges-private', + description: + 'Returns a list of public Topcoder challenges based on the query parameters.', + parameters: QUERY_CHALLENGES_TOOL_PARAMETERS, + outputSchema: QUERY_CHALLENGES_TOOL_OUTPUT_SCHEMA, + annotations: { + title: 'Query Public Topcoder Challenges', + readOnlyHint: true, + }, + canActivate: authGuard, + }) + async queryChallengesPrivate(params) { + return this._queryChallenges(params); + } + + @Tool({ + name: 'query-tc-challenges-protected', + description: + 'Returns a list of public Topcoder challenges based on the query parameters.', + parameters: QUERY_CHALLENGES_TOOL_PARAMETERS, + outputSchema: QUERY_CHALLENGES_TOOL_OUTPUT_SCHEMA, + annotations: { + title: 'Query Public Topcoder Challenges', + readOnlyHint: true, + }, + canActivate: checkHasUserRole(Role.Admin), + }) + async queryChallengesProtected(params) { + return this._queryChallenges(params); + } + + @Tool({ + name: 'query-tc-challenges-m2m', + description: + 'Returns a list of public Topcoder challenges based on the query parameters.', + parameters: QUERY_CHALLENGES_TOOL_PARAMETERS, + outputSchema: QUERY_CHALLENGES_TOOL_OUTPUT_SCHEMA, + annotations: { + title: 'Query Public Topcoder Challenges', + readOnlyHint: true, + }, + canActivate: checkM2MScope(M2mScope.QueryPublicChallenges), + }) + async queryChallengesM2m(params) { + return this._queryChallenges(params); + } + + @Tool({ + name: 'query-tc-challenges-public', + description: + 'Returns a list of public Topcoder challenges based on the query parameters.', + parameters: QUERY_CHALLENGES_TOOL_PARAMETERS, + outputSchema: QUERY_CHALLENGES_TOOL_OUTPUT_SCHEMA, + annotations: { + title: 'Query Public Topcoder Challenges', + readOnlyHint: true, + }, + }) + async queryChallengesPublic(params) { + return this._queryChallenges(params); + } } diff --git a/src/shared/global/logger.ts b/src/shared/global/logger.ts index a6c6bb8..93142b7 100644 --- a/src/shared/global/logger.ts +++ b/src/shared/global/logger.ts @@ -1,12 +1,7 @@ import { Logger as NestLogger } from '@nestjs/common'; import * as stringify from 'json-stringify-safe'; -import { getStore } from 'src/core/request/requestStore'; export class Logger extends NestLogger { - private get store() { - return getStore(); - } - log(...messages: any[]): void { super.log(this.formatMessages(messages)); } @@ -24,10 +19,7 @@ export class Logger extends NestLogger { } private formatMessages(messages: any[]): string { - const requestIdPrefix = this.store.requestId - ? [`{${this.store.requestId}}`] - : []; - return [...requestIdPrefix, ...messages] + return [...messages] .map((msg) => // eslint-disable-next-line @typescript-eslint/no-unsafe-return typeof msg === 'object' ? stringify(msg, null, 2) : String(msg),