Skip to content
Closed
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
36 changes: 14 additions & 22 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ on:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
# Lets this workflow be run manually against any branch (Actions tab ->
# "Run workflow", or `gh workflow run`), without needing to push/PR to
# main first. Doesn't change push/pull_request behavior at all.
workflow_dispatch: {}

jobs:
build:
Expand Down Expand Up @@ -59,31 +63,19 @@ jobs:
run: npm ci
working-directory: ./react-ystemandchess

- name: Create environment file
run: |
mkdir -p src/core/environments
cat <<EOF > src/core/environments/environment.ts
export const environment = {
production: false,
agora: {
appId: '${{ secrets.APP_ID }}',
},
email: {
user: '${{ secrets.EMAIL_USER || '' }}',
pass: '${{ secrets.EMAIL_PASS || '' }}'
},
urls: {
middlewareURL: '${{ secrets.MIDDLEWARE_URL }}',
stockFishURL: '${{ secrets.STOCKFISH_URL }}',
chessServer: '${{ secrets.CHESS_SERVER }}',
},
};
EOF
working-directory: ./react-ystemandchess

- name: Build react-ystemandchess
run: CI=false npm run build --if-present
working-directory: ./react-ystemandchess
env:
# These are read by src/environments/environment.prod.js, which is
# what the app actually imports (via src/environments/index.js).
# The previous "Create environment file" step wrote to
# src/core/environments/environment.ts, a path nothing in the app
# imports, so these secrets never reached the built app.
REACT_APP_MIDDLEWARE_URL: ${{ secrets.MIDDLEWARE_URL || 'https://ystemandchess.com/middleware' }}
REACT_APP_STOCKFISH_SERVER_URL: ${{ secrets.STOCKFISH_URL || 'https://ystemandchess.com/stockfishserver' }}
REACT_APP_CHESS_SERVER_URL: ${{ secrets.CHESS_SERVER || 'https://ystemandchess.com/chessserver' }}
REACT_APP_AGORA_APP_ID: ${{ secrets.APP_ID || '' }}

- name: Test react-ystemandchess
run: npm test
Expand Down
5 changes: 5 additions & 0 deletions chessServer/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
NODE_ENV=development
PORT=3001
CORS_ORIGIN=http://localhost:3000,http://localhost:3002
ALLOWED_ORIGINS=
MIDDLEWARE_URL=http://localhost:8000
31 changes: 24 additions & 7 deletions chessServer/src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
require("dotenv").config();

const validateEnvironment = require("./validateEnvironment");
validateEnvironment();

const express = require("express");
const http = require("http");
const socketIo = require("socket.io");
Expand All @@ -13,28 +16,42 @@ const server = http.createServer(app);
// Add logging functionaility to the server
app.use(morgan("dev")); // dev -> preset format

const allowedOriginsSetting = process.env.CORS_ORIGIN || process.env.ALLOWED_ORIGINS;
const isProduction = process.env.NODE_ENV === "production";

const allowedOriginsSetting =
process.env.CORS_ORIGIN || process.env.ALLOWED_ORIGINS;

const allowedOrigins = allowedOriginsSetting
? allowedOriginsSetting.split(",").map((o) => o.trim())
: [
"https://ystemandchess.com",
"https://www.ystemandchess.com",
"http://localhost:3000",
"http://localhost:3002",
"http://localhost:4200",
...(isProduction
? []
: [
"http://localhost:3000",
"http://localhost:3002",
"http://localhost:4200",
]),
];

const hasWildcard = allowedOrigins.includes("*");

const corsOptions = {
origin: function (origin, callback) {
if (!origin) return callback(null, true);
if (allowedOrigins.indexOf(origin) !== -1 || allowedOrigins.includes("*")) {
if (hasWildcard) {
return callback(null, true);
}
if (allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error(`Origin ${origin} not allowed by CORS`));
callback(null, false);
}
},
methods: ["GET", "POST"],
credentials: true,
// When wildcard is configured, disallow credentials to prevent unsafe CORS configuration
credentials: !hasWildcard,
};

// Apply CORS middleware to handle cross-origin requests
Expand Down
67 changes: 67 additions & 0 deletions chessServer/src/tests/cors.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Verifies the CORS origin/credentials behavior in src/index.js.
* index.js can't be imported directly (it calls server.listen() as a
* module-load side effect), so this mirrors its corsOptions logic in an
* isolated app, the same approach the other tests in this file already use
* for their own standalone servers. Keep this in sync with src/index.js if
* that logic changes.
*/

const express = require("express");
const cors = require("cors");
const request = require("supertest");

function buildApp(allowedOriginsCsv) {
const allowedOrigins = allowedOriginsCsv.split(",").map((o) => o.trim());
const hasWildcard = allowedOrigins.includes("*");

const app = express();
app.use(
cors({
origin: function (origin, callback) {
if (!origin) return callback(null, true);
if (hasWildcard) {
return callback(null, true);
}
if (allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(null, false);
}
},
methods: ["GET", "POST"],
credentials: !hasWildcard,
})
);
app.get("/ping", (req, res) => res.json({ ok: true }));
return app;
}

describe("chessServer CORS origin/credentials behavior", () => {
test("allowed origin (no wildcard): credentials allowed, header set to the origin", async () => {
const app = buildApp("https://ystemandchess.com,http://localhost:3000");
const res = await request(app).get("/ping").set("Origin", "http://localhost:3000");

expect(res.status).toBe(200);
expect(res.headers["access-control-allow-origin"]).toBe("http://localhost:3000");
expect(res.headers["access-control-allow-credentials"]).toBe("true");
});

test("disallowed origin (no wildcard): rejected without a 500, no CORS header", async () => {
const app = buildApp("https://ystemandchess.com,http://localhost:3000");
const res = await request(app).get("/ping").set("Origin", "https://evil.example.com");

expect(res.status).not.toBe(500);
expect(res.status).toBe(200);
expect(res.headers["access-control-allow-origin"]).toBeUndefined();
});

test("wildcard configured: any origin allowed, but credentials are disabled", async () => {
const app = buildApp("*");
const res = await request(app).get("/ping").set("Origin", "https://anything.example.com");

expect(res.status).toBe(200);
expect(res.headers["access-control-allow-origin"]).toBe("https://anything.example.com");
expect(res.headers["access-control-allow-credentials"]).toBeUndefined();
});
});
36 changes: 36 additions & 0 deletions chessServer/src/validateEnvironment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const REQUIRED_PRODUCTION_VARS = [
"MIDDLEWARE_URL",
];

function hasCorsConfiguration() {
return Boolean(
process.env.CORS_ORIGIN?.trim() ||
process.env.ALLOWED_ORIGINS?.trim()
);
}

function validateEnvironment() {
if (process.env.NODE_ENV !== "production") {
return;
}

const missing = REQUIRED_PRODUCTION_VARS.filter((name) => {
const value = process.env[name];
return !value || !value.trim();
});

if (!hasCorsConfiguration()) {
missing.push("CORS_ORIGIN or ALLOWED_ORIGINS");
}

if (missing.length > 0) {
console.error(
`[chessServer] Missing required production environment variables: ${missing.join(", ")}`
);
process.exit(1);
}

console.log("[chessServer] Required production environment variables validated");
}

module.exports = validateEnvironment;
21 changes: 15 additions & 6 deletions deploy/dev/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,42 +12,51 @@ services:
- stockfishserver

react-app:
build: ../../react-ystemandchess
build:
context: ../../react-ystemandchess
args:
REACT_APP_CHESS_SERVER_URL: http://localhost:3001
REACT_APP_MIDDLEWARE_URL: http://localhost:8000
REACT_APP_STOCKFISH_SERVER_URL: http://localhost:9324
REACT_APP_AGORA_APP_ID: ""
container_name: react-app
ports:
- "3000:3000"
environment:
- REACT_APP_CHESS_SERVER_URL=http://localhost:3001
- REACT_APP_MIDDLEWARE_URL=http://localhost:8000
- REACT_APP_STOCKFISH_URL=http://localhost:8080

middlewarenode:
build: ../../middlewareNode
container_name: middlewarenode
ports:
- "8000:8000"
environment:
- NODE_ENV=development
- PORT=8000
- INDEX_KEY=dev-index-key-replace-in-prod
- CORS_ORIGIN=http://localhost,http://localhost:3000
- SESSION_SECRET=dev-secret-replace-in-prod
# Analytics rate limit — requests per 15-minute window per IP (default: 100)
- ANALYTICS_RATE_LIMIT_MAX=100
- LEADERBOARD_RATE_LIMIT_MAX=60

chessserver:
build: ../../chessServer
container_name: chessserver
ports:
- "3001:3001"
environment:
- NODE_ENV=development
- PORT=3001
- MIDDLEWARE_URL=http://middlewarenode:8000
- CORS_ORIGIN=http://localhost,http://localhost:3000,http://localhost:3002

stockfishserver:
build: ../../stockfishServer
container_name: stockfishserver
ports:
- "9324:9324"
environment:
- NODE_ENV=development
- PORT=9324
- CORS_ORIGIN=http://localhost,http://localhost:3000

# Usage: docker-compose up --build
# Access at: http://localhost
48 changes: 25 additions & 23 deletions deploy/prod/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
version: '3.4'

networks:
ysc-net:
external:
name: ysc-net
name: ysc-net
external: true

services:
nginx:
image: nginx:1.19.2-alpine
container_name: reverse-proxy-server
volumes:
- ./deploy/prod/nginx.conf:/etc/nginx/nginx.conf
- ./nginx.conf:/etc/nginx/nginx.conf
- /home/azureuser/ysc-2/app.ystemandchess.com/YStemAndChess/dist_new/YStemAndChess:/var/www/html
- /etc/letsencrypt/live/ystemandchess.com/fullchain.pem:/etc/ysc-certs/ysc-cert.pem
- /etc/letsencrypt/live/ystemandchess.com/privkey.pem:/etc/ysc-certs/ysc-key.pem
Expand All @@ -32,7 +30,10 @@ services:
image: chessserver:${TAG}
container_name: chessserver
environment:
- PORT=${PORT}
- NODE_ENV=production
- PORT=3001
- MIDDLEWARE_URL=${MIDDLEWARE_URL}
- CORS_ORIGIN=${CORS_ORIGIN}
networks:
- ysc-net
expose:
Expand All @@ -42,7 +43,9 @@ services:
image: stockfishserver:${TAG}
container_name: stockfishserver
environment:
- NODE_ENV=production
- PORT=8080
- CORS_ORIGIN=${CORS_ORIGIN}
networks:
- ysc-net
expose:
Expand All @@ -52,34 +55,33 @@ services:
image: middlewarenode
container_name: middleware
environment:
- NODE_ENV=production
- PORT=8000
- indexKey=${indexKey}
- MONGO_URI=${MONGO_URI}
- INDEX_KEY=${INDEX_KEY}
- CORS_ORIGIN=${CORS_ORIGIN}
- AZURE_STORAGE_ACCOUNT=${AZURE_STORAGE_ACCOUNT}
- AZURE_STORAGE_KEY=${AZURE_STORAGE_KEY}
- AZURE_STORAGE_CONTAINER=${AZURE_STORAGE_CONTAINER}
- AZURE_STORAGE_REGION=${AZURE_STORAGE_REGION}
- mongoURI=${mongoURI}
- appID=${appID}
- auth=${auth}
- channel=${channel}
- uid=${uid}
- jwtSecret=${jwtSecret}
- NODE_ENV=${NODE_ENV}
- basepath=${basepath}
- clientId=${clientId}
- clientSecret=${clientSecret}
- redirectUri=${redirectUri}
- refreshToken=${refreshToken}
- user=${user}
- senderEmail=${senderEmail}
- AGORA_APP_ID=${AGORA_APP_ID}
- AGORA_UID=${AGORA_UID}
- AGORA_CUSTOMER_ID=${AGORA_CUSTOMER_ID}
- AGORA_CUSTOMER_CERT=${AGORA_CUSTOMER_CERT}
- BASEPATH=${BASEPATH}
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
- GOOGLE_REDIRECT_URI=${GOOGLE_REDIRECT_URI}
- GOOGLE_REFRESH_TOKEN=${GOOGLE_REFRESH_TOKEN}
- EMAIL_USER=${EMAIL_USER}
- SENDER_EMAIL=${SENDER_EMAIL}
- SESSION_SECRET=${SESSION_SECRET}
- ANALYTICS_RATE_LIMIT_MAX=${ANALYTICS_RATE_LIMIT_MAX:-100}
- LEADERBOARD_RATE_LIMIT_MAX=${LEADERBOARD_RATE_LIMIT_MAX:-60}
networks:
- ysc-net
expose:
- '8000'
volumes:
- ../middleware:/var/www/html

ystemandchess:
image: ystemandchess:${TAG}
Expand Down
Loading
Loading