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
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
26.4.0
26.5.1
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Dev: [![CircleCI](https://circleci.com/gh/topcoder-platform/challenge-api/tree/d

## Prerequisites

- [Node.js](https://nodejs.org/en/) 26.4.0 (use the version in `.nvmrc`)
- [Node.js](https://nodejs.org/en/) 26.5.1 (use the version in `.nvmrc`)
- [pnpm](https://pnpm.io/) 11.15.1
- [AWS S3](https://aws.amazon.com/s3/)
- [Docker](https://www.docker.com/)
Expand Down Expand Up @@ -74,6 +74,8 @@ The following parameters can be set in config files or in env variables:
- READONLY: sets the API in read-only mode. POST/PUT/PATCH/DELETE operations will return 403 Forbidden
- LOG_LEVEL: the log level, default is 'debug'
- PORT: the server port, default is 3000
- CORS_ALLOWED_ORIGINS: comma-separated exact HTTPS browser origins allowed to
call the API; HTTP is accepted only for localhost development origins
- AUTH_SECRET: The authorization secret used during token verification.
- VALID_ISSUERS: The valid issuer of tokens.
- AUTH0_URL: AUTH0 URL, used to get M2M token
Expand Down Expand Up @@ -131,7 +133,7 @@ database operation or application startup.

## Local Deployment

0. Select the repository's Node 26.4.0 version with
0. Select the repository's Node 26.5.1 version with

```bash
nvm use
Expand Down
16 changes: 15 additions & 1 deletion app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ const { ForbiddenError } = require("./src/common/errors");

// setup express app
const app = express();
const corsAllowedOrigins = new Set(config.CORS_ALLOWED_ORIGINS);

/**
* Applies the exact-origin CORS allowlist while permitting non-browser requests
* that do not send an Origin header.
*
* @param {string | undefined} requestOrigin browser Origin header supplied by cors middleware
* @param {(error: Error | null, allowed?: boolean) => void} callback cors decision callback
* @returns {void} The allow/deny decision is delivered through callback.
* @throws {Error} This callback does not throw; unlisted origins are denied.
*/
const validateCorsOrigin = (requestOrigin, callback) => {
callback(null, requestOrigin === undefined || corsAllowedOrigins.has(requestOrigin));
};

// Use extended query parsing so bracket syntax like types[]=F2F is handled as arrays
app.set("query parser", "extended");
Expand All @@ -47,7 +61,7 @@ app.use(

app.use(
cors({
origin: "*",
origin: validateCorsOrigin,
exposedHeaders: [
"X-Prev-Page",
"X-Next-Page",
Expand Down
46 changes: 46 additions & 0 deletions config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,56 @@
*/
const _ = require("lodash");
require("dotenv").config();

const DEFAULT_CORS_ALLOWED_ORIGINS = [
"https://www.topcoder.com",
"https://www.topcoder-dev.com",
"https://platform.topcoder.com",
"https://platform.topcoder-dev.com",
];

/**
* Parses the comma-separated browser origins accepted by the API CORS policy.
*
* @param {string | undefined} configuredOrigins value supplied through CORS_ALLOWED_ORIGINS
* @returns {string[]} canonical, exact origins used by the Express CORS callback
* @throws {Error} when an entry is not an HTTPS origin, or an HTTP localhost origin for local development
* @example parseCorsAllowedOrigins("https://platform.topcoder.com,http://localhost:3000")
*/
const parseCorsAllowedOrigins = (configuredOrigins) => {
const values = configuredOrigins
? configuredOrigins.split(",")
: DEFAULT_CORS_ALLOWED_ORIGINS;

return values
.map((value) => value.trim())
.filter(Boolean)
.map((value) => {
let parsed;
try {
parsed = new URL(value);
} catch {
throw new Error(`Invalid CORS_ALLOWED_ORIGINS entry: ${value}`);
}

const isLocalHttpOrigin =
parsed.protocol === "http:" &&
["localhost", "127.0.0.1", "::1"].includes(parsed.hostname);
if (parsed.protocol !== "https:" && !isLocalHttpOrigin) {
throw new Error(`CORS origin must use HTTPS or local HTTP: ${value}`);
}
if (parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash) {
throw new Error(`CORS entry must contain only an origin: ${value}`);
}
return parsed.origin;
});
};

module.exports = {
READONLY: process.env.READONLY === "true" || false,
LOG_LEVEL: process.env.LOG_LEVEL || "debug",
PORT: process.env.PORT || 3000,
CORS_ALLOWED_ORIGINS: parseCorsAllowedOrigins(process.env.CORS_ALLOWED_ORIGINS),
// used to properly set the header response to api calls for services behind a load balancer
API_BASE_URL: process.env.API_BASE_URL || `http://localhost:3000`,
API_VERSION: process.env.API_VERSION || "v6",
Expand Down
Loading
Loading