Skip to content
Open
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
4 changes: 2 additions & 2 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ npm run dev
## Environment

- `PORT`: Server port (default `3001`)
- `CORS_ALLOWED_ORIGINS`: Comma-separated allowed origins for CORS (example: `https://app.example.com,https://admin.example.com`)
- `CORS_ORIGIN`: Legacy single-origin CORS setting (fallback when `CORS_ALLOWED_ORIGINS` is not set)
- `CORS_ALLOWED_ORIGINS`: Comma-separated allowed origins for CORS (example: `https://app.example.com,https://admin.example.com`). If this is not set in production, CORS defaults to a safe deny-all for cross-origin requests.
- `CORS_ORIGIN`: Legacy single-origin CORS setting (fallback when `CORS_ALLOWED_ORIGINS` is not set). In development, if neither is set, defaults to `http://localhost:5173`.
- `STELLAR_NETWORK`: `testnet` or `mainnet`
- `SOROBAN_RPC_URL`: Soroban RPC URL exposed in API metadata
- `REWARDS_CONTRACT_ID`: Optional rewards contract ID exposed by `/api/v1/config`
Expand Down
8 changes: 7 additions & 1 deletion backend/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,14 @@ function nextCampaignId(campaigns) {

export function createApp(options = {}) {
const apiKey = options.apiKey ?? process.env.TRIVELA_API_KEY ?? '';
const corsAllowedOrigins =
const corsOriginConfig =
options.corsAllowedOrigins ?? process.env.CORS_ALLOWED_ORIGINS ?? process.env.CORS_ORIGIN;
const corsAllowedOrigins =
typeof corsOriginConfig === 'string' && corsOriginConfig.trim().length > 0
? corsOriginConfig
: process.env.NODE_ENV === 'production'
? ''
: 'http://localhost:5173';
const stellarNetwork = options.stellarNetwork ?? process.env.STELLAR_NETWORK ?? 'testnet';
const sorobanRpcUrl = options.sorobanRpcUrl ?? process.env.SOROBAN_RPC_URL ?? DEFAULT_RPC_URL;
const rewardsContractId = readOptionalConfigValue(options, 'REWARDS_CONTRACT_ID');
Expand Down
39 changes: 39 additions & 0 deletions backend/src/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,45 @@ test('/health/rpc returns 503 when the Soroban RPC health check fails', async ()
}
});

test('CORS allows configured origins and rejects others', async () => {
const { server, baseUrl } = await startTestServer({
corsAllowedOrigins: 'https://app.example.com, https://admin.example.com',
});

try {
const allowedRes = await fetch(`${baseUrl}/api/v1`, {
headers: { origin: 'https://app.example.com' },
});
assert.equal(allowedRes.status, 200);
assert.equal(allowedRes.headers.get('access-control-allow-origin'), 'https://app.example.com');

const deniedRes = await fetch(`${baseUrl}/api/v1`, {
headers: { origin: 'https://evil.example' },
});
assert.equal(deniedRes.status, 200);
assert.equal(deniedRes.headers.get('access-control-allow-origin'), null);
} finally {
await stopTestServer(server);
}
});

test('CORS production default is safe (no open origins)', async () => {
const originalNodeEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';
const { server, baseUrl } = await startTestServer({});

try {
const response = await fetch(`${baseUrl}/api/v1`, {
headers: { origin: 'http://localhost:5173' },
});
assert.equal(response.status, 200);
assert.equal(response.headers.get('access-control-allow-origin'), null);
} finally {
await stopTestServer(server);
process.env.NODE_ENV = originalNodeEnv;
}
});

test('POST /api/campaigns creates a new campaign and returns it', async () => {
const { server, baseUrl } = await startTestServer();

Expand Down