Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b21c0ec
Fix environment separation
ToldYO Aug 29, 2026
37cdbec
Fix production frontend Docker deployment
ToldYO Aug 29, 2026
2d06324
fix(frontend): align environment URLs with authoritative port map and…
ToldYO Aug 29, 2026
9daf3bb
build(docker): declare REACT_APP_CHESS_CLIENT_URL build arg and env i…
ToldYO Aug 29, 2026
db57604
feat(tutor,auth): pass cookies.login JWT auth token to AI chat and ch…
ToldYO Aug 29, 2026
bdcc4fe
fix(middleware): enhance environment validation diagnostics for devel…
ToldYO Aug 29, 2026
9779e5a
deploy(compose): configure canonical frontend build args and remove d…
ToldYO Aug 29, 2026
4a91198
ci(actions): add middlewareNode test step and PR single-authorship ve…
ToldYO Aug 29, 2026
1e1c942
test(frontend): add environment configuration and port map invariant …
ToldYO Aug 29, 2026
6bc2786
chore(scripts): add PR authorship verification script
ToldYO Aug 29, 2026
793a501
ci(actions): configure checkout fetch-depth, test timeouts, and robus…
ToldYO Aug 29, 2026
38c5378
fix(middleware): supply fallback defaults in config and add test conf…
ToldYO Aug 29, 2026
a4103b6
ci(actions): inspect PR SHAs directly and normalize ToldYO/Ahmad Nakh…
ToldYO Aug 29, 2026
2c9fee5
feat(tutor,auth): send login jwt on all ChatWidget session and messag…
ToldYO Aug 31, 2026
3d5ed12
test(tutor): add unit tests for ChatWidget authenticated api requests
ToldYO Aug 31, 2026
b5311ed
fix(engine): target stockfish server url exclusively for stockfish an…
ToldYO Aug 31, 2026
52c7cca
fix(middleware): generate ephemeral random jwt secret at boot and rem…
ToldYO Aug 31, 2026
dc25110
fix(middleware): update startup diagnostics for ephemeral jwt signing…
ToldYO Aug 31, 2026
a08eb45
deploy(prod): pass REACT_APP_CHESS_CLIENT_URL build arg in production…
ToldYO Aug 31, 2026
986a189
chore(scripts): clarify remedy guidance in single-authorship verifica…
ToldYO Aug 31, 2026
78ce30a
ci(actions): clarify remedy guidance in PR authorship verification wo…
ToldYO Aug 31, 2026
38bd2bd
fix(tutor): restore environment urls reference in StockfishTutor
ToldYO Aug 31, 2026
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
59 changes: 36 additions & 23 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@ 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:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Use Node.js 20
uses: actions/setup-node@v4
Expand All @@ -28,6 +34,10 @@ jobs:
run: npm run build --if-present
working-directory: ./middlewareNode

- name: Test middlewareNode
run: npm test -- --testTimeout=30000
working-directory: ./middlewareNode

# chessServer
- name: Install dependencies (chessServer)
run: npm ci
Expand Down Expand Up @@ -59,32 +69,35 @@ 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:
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_CHESS_CLIENT_URL: ${{ secrets.CHESS_CLIENT || 'https://ystemandchess.com/chessclient' }}
REACT_APP_AGORA_APP_ID: ${{ secrets.APP_ID || '' }}

- name: Test react-ystemandchess
run: npm test
run: CI=true npm test -- --watchAll=false
working-directory: ./react-ystemandchess

- name: Verify PR commit authorship
if: github.event_name == 'pull_request'
run: |
BASE_SHA="${{ github.event.pull_request.base.sha }}"
HEAD_SHA="${{ github.event.pull_request.head.sha }}"
if [ -n "$BASE_SHA" ] && [ -n "$HEAD_SHA" ]; then
AUTHORS=$(git log --format='%an' "${BASE_SHA}..${HEAD_SHA}" | sort -u | grep -v '^$' || true)
else
AUTHORS=$(git log --format='%an' "origin/${{ github.base_ref }}..HEAD" | sort -u | grep -v '^$' || true)
fi
NORMALIZED_AUTHORS=$(echo "$AUTHORS" | sed 's/ToldYO/Ahmad Nakhala/' | sort -u | grep -v '^$' || true)
AUTHOR_COUNT=$(echo "$NORMALIZED_AUTHORS" | grep -v '^$' | wc -l)
echo "Commit authors on branch: $AUTHORS"
if [ "$AUTHOR_COUNT" -gt 1 ]; then
echo "ERROR: Branch contains commits by multiple distinct authors ($AUTHOR_COUNT). Base your branch on the upstream PR branch or wait for it to land on main to keep PR authorship clean."
exit 1
fi
echo "PR authorship verified: clean single author ($NORMALIZED_AUTHORS)."
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;
53 changes: 0 additions & 53 deletions deploy/dev/docker-compose.yml

This file was deleted.

Loading
Loading