diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e7a2406..135d7db5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: @@ -59,31 +63,19 @@ jobs: run: npm ci working-directory: ./react-ystemandchess - - name: Create environment file - run: | - mkdir -p src/core/environments - cat < 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 diff --git a/chessServer/.env.example b/chessServer/.env.example new file mode 100644 index 00000000..4e25e7cc --- /dev/null +++ b/chessServer/.env.example @@ -0,0 +1,5 @@ +NODE_ENV=development +PORT=3001 +CORS_ORIGIN=http://localhost:3000,http://localhost:3002 +ALLOWED_ORIGINS= +MIDDLEWARE_URL=http://localhost:8000 diff --git a/chessServer/src/index.js b/chessServer/src/index.js index ea50058c..76f0c777 100644 --- a/chessServer/src/index.js +++ b/chessServer/src/index.js @@ -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"); @@ -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 diff --git a/chessServer/src/tests/cors.test.js b/chessServer/src/tests/cors.test.js new file mode 100644 index 00000000..1a9393e8 --- /dev/null +++ b/chessServer/src/tests/cors.test.js @@ -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(); + }); +}); diff --git a/chessServer/src/validateEnvironment.js b/chessServer/src/validateEnvironment.js new file mode 100644 index 00000000..14a9bdc7 --- /dev/null +++ b/chessServer/src/validateEnvironment.js @@ -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; diff --git a/deploy/dev/docker-compose.yml b/deploy/dev/docker-compose.yml index f21cf090..c72417aa 100644 --- a/deploy/dev/docker-compose.yml +++ b/deploy/dev/docker-compose.yml @@ -12,14 +12,16 @@ 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 @@ -27,10 +29,13 @@ services: 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 @@ -38,8 +43,10 @@ services: 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 @@ -47,7 +54,9 @@ services: 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 diff --git a/deploy/prod/docker-compose.yml b/deploy/prod/docker-compose.yml index 0d28bd21..112a1cfb 100644 --- a/deploy/prod/docker-compose.yml +++ b/deploy/prod/docker-compose.yml @@ -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 @@ -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: @@ -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: @@ -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} diff --git a/middlewareNode/.env.example b/middlewareNode/.env.example new file mode 100644 index 00000000..6bd6f59a --- /dev/null +++ b/middlewareNode/.env.example @@ -0,0 +1,64 @@ +# Runtime environment +NODE_ENV=development +PORT=8000 + +# Required core backend configuration +MONGO_URI= +INDEX_KEY= +SESSION_SECRET= +CORS_ORIGIN=http://localhost:3000 + +# Feature-specific email / Google OAuth configuration +EMAIL_USER= +SENDER_EMAIL= +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +GOOGLE_REDIRECT_URI= +GOOGLE_REFRESH_TOKEN= + +# Application base URL +BASEPATH=http://localhost:3000 + +# Azure storage +AZURE_STORAGE_ACCOUNT= +AZURE_STORAGE_KEY= +AZURE_STORAGE_CONTAINER= +AZURE_STORAGE_REGION= + +# Agora recording +AGORA_APP_ID= +AGORA_UID= +AGORA_CUSTOMER_ID= +AGORA_CUSTOMER_CERT= + +# API / route limits +ANALYTICS_RATE_LIMIT_MAX=100 +LEADERBOARD_RATE_LIMIT_MAX=60 +CHAT_RATE_LIMIT=30 + +# AI provider configuration +GEMINI_API_KEY= +GEMINI_BASE_URL= +GEMINI_MODEL= + +OPENAI_API_KEY= +OPENAI_BASE_URL= +OPENAI_MODEL= + +# Leaderboard scoring +LEADERBOARD_WEIGHT_TIME=1 +LEADERBOARD_WEIGHT_STREAK=5 +LEADERBOARD_WEIGHT_BADGE=10 +LEADERBOARD_WEIGHT_ACTIVITY=3 + +# PVP scoring +PVP_WEIGHT_WIN=3 +PVP_WEIGHT_DRAW=1 +PVP_WEIGHT_LOSS=0 + +# Script-only variable used by src/scripts/provisionAdmin.js +ADMIN_USERNAME= + +# Present in config mapping but not currently consumed by middleware source +JWT_SECRET= +EMAIL_PASS= diff --git a/middlewareNode/config/default.js b/middlewareNode/config/default.js new file mode 100644 index 00000000..5864fdf8 --- /dev/null +++ b/middlewareNode/config/default.js @@ -0,0 +1,38 @@ +module.exports = { + mongoURI: "", + jwtSecret: "", + indexKey: "", + + corsOptions: { + origin: "http://localhost:3000", + }, + + email: { + user: "", + pass: "", + }, + + user: "", + senderEmail: "", + + clientId: "", + clientSecret: "", + redirectUri: "", + refreshToken: "", + + basepath: "http://localhost:3000", + + azureStorageAccount: "", + azureStorageKey: "", + azureContainer: "", + azureStorageRegion: "", + + appID: "", + uid: "", + customerId: "", + customerCertificate: "", + + server: { + port: 8000, + }, +}; diff --git a/middlewareNode/src/config/custom-environment-variables.json b/middlewareNode/src/config/custom-environment-variables.json deleted file mode 100644 index 73430b29..00000000 --- a/middlewareNode/src/config/custom-environment-variables.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "mongoURI": "MONGO_URI", - "jwtSecret": "JWT_SECRET", - "indexKey": "INDEX_KEY", - - "corsOptions": { "origin": "CORS_ORIGIN" }, - - "email": { - "user": "EMAIL_USER", - "pass": "EMAIL_PASS" - }, - - "user": "EMAIL_USER", - "senderEmail": "SENDER_EMAIL", - - "clientId": "GOOGLE_CLIENT_ID", - "clientSecret": "GOOGLE_CLIENT_SECRET", - "redirectUri": "GOOGLE_REDIRECT_URI", - "refreshToken": "GOOGLE_REFRESH_TOKEN", - - "basepath": "BASEPATH", - - "azureStorageAccount": "AZURE_STORAGE_ACCOUNT", - "azureStorageKey": "AZURE_STORAGE_KEY", - "azureContainer": "AZURE_STORAGE_CONTAINER", - "azureStorageRegion": "AZURE_STORAGE_REGION", - - "appID": "AGORA_APP_ID", - "uid": "AGORA_UID", - "customerId": "AGORA_CUSTOMER_ID", - "customerCertificate": "AGORA_CUSTOMER_CERT", - - "server": { "port": "PORT" } -} diff --git a/middlewareNode/src/config/db.js b/middlewareNode/src/config/db.js index b06145d0..80cff60b 100644 --- a/middlewareNode/src/config/db.js +++ b/middlewareNode/src/config/db.js @@ -249,19 +249,45 @@ async function ensureIndexes() { } } +const IS_PRODUCTION = process.env.NODE_ENV === "production"; + const connectDB = async () => { + try { + const target = new URL( + db.replace("mongodb+srv://", "https://").replace("mongodb://", "http://") + ); + console.log( + `[boot] env=${process.env.NODE_ENV || "undefined"} db_host=${target.hostname}${target.pathname}` + ); + } catch (_) { + console.log( + `[boot] env=${process.env.NODE_ENV || "undefined"} db_host=` + ); + } + try { console.log(`Connecting to MongoDB...`); await mongoose.connect(db, { useNewUrlParser: true, useUnifiedTopology: true, - serverSelectionTimeoutMS: 1000, // Timeout after 1 second instead of hanging + serverSelectionTimeoutMS: 1000, }); console.log("MongoDB Connected..."); await ensureIndexes(); - await seedTestUsers(); + + if (!IS_PRODUCTION) { + await seedTestUsers(); + } } catch (err) { console.warn(`Connection to configured MongoDB failed: ${err.message}`); + + if (IS_PRODUCTION) { + console.error( + "Refusing to fall back to in-memory MongoDB in production. Exiting." + ); + process.exit(1); + } + console.warn("Starting local in-memory MongoDB server as fallback..."); try { const { MongoMemoryServer } = require("mongodb-memory-server"); @@ -281,7 +307,7 @@ const connectDB = async () => { await seedTestUsers(); } catch (fallbackErr) { console.error("In-memory MongoDB startup failed:", fallbackErr.message); - process.exit(1); // Exit process if connection fails + process.exit(1); } } }; diff --git a/middlewareNode/src/config/validateEnvironment.js b/middlewareNode/src/config/validateEnvironment.js new file mode 100644 index 00000000..c6eb8267 --- /dev/null +++ b/middlewareNode/src/config/validateEnvironment.js @@ -0,0 +1,28 @@ +const REQUIRED_PRODUCTION_VARS = [ + "MONGO_URI", + "INDEX_KEY", + "CORS_ORIGIN", + "SESSION_SECRET", +]; + +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 (missing.length > 0) { + console.error( + `[boot] Missing required production environment variables: ${missing.join(", ")}` + ); + process.exit(1); + } + + console.log("[boot] Required production environment variables validated"); +} + +module.exports = validateEnvironment; diff --git a/middlewareNode/src/server.js b/middlewareNode/src/server.js index 0f130a30..7c1a7dfa 100644 --- a/middlewareNode/src/server.js +++ b/middlewareNode/src/server.js @@ -1,4 +1,13 @@ // Main server configuration for the Node.js middleware API + +// Load local environment variables before importing modules that read config. +require("dotenv").config(); + +// Validate production configuration before db.js, passport.js, or other +// modules can call config.get(). +const validateEnvironment = require("./config/validateEnvironment"); +validateEnvironment(); + const express = require("express"); const session = require("express-session"); const connectDB = require("./config/db"); @@ -33,7 +42,24 @@ require("./scheduler/activitiesScheduler.js"); require("./scheduler/analyticsSummaryScheduler.js"); // Enable CORS for cross-origin requests -app.use(cors(config.get("corsOptions"))); +const configuredCorsOrigin = config.get("corsOptions.origin"); + +const allowedOrigins = String(configuredCorsOrigin || "") + .split(",") + .map((origin) => origin.trim()) + .filter(Boolean); + +app.use( + cors({ + origin(origin, callback) { + if (!origin || allowedOrigins.includes("*") || allowedOrigins.includes(origin)) { + return callback(null, true); + } + + return callback(null, false); + }, + }) +); // Connect to MongoDB database connectDB(); @@ -44,7 +70,11 @@ app.use(express.json({ extended: false })); // Configure session middleware app.use( session({ - secret: process.env.SESSION_SECRET || "dev-secret-change-in-prod", + secret: + process.env.SESSION_SECRET || + (process.env.NODE_ENV !== "production" + ? "dev-secret-change-in-prod" + : undefined), resave: false, saveUninitialized: false, }) diff --git a/middlewareNode/tests/cors.test.js b/middlewareNode/tests/cors.test.js new file mode 100644 index 00000000..f17afcd7 --- /dev/null +++ b/middlewareNode/tests/cors.test.js @@ -0,0 +1,55 @@ +/** + * Verifies the CORS origin-rejection behavior in src/server.js. + * server.js can't be imported directly (it calls connectDB()/app.listen() and + * requires SESSION_SECRET/Mongo at module load), so this mirrors its origin + * callback logic in an isolated app, the same approach already used by the + * chessServer/stockfishServer test suites. Keep this in sync with server.js + * if that logic changes. + */ + +const express = require("express"); +const cors = require("cors"); +const request = require("supertest"); + +function buildApp(allowedOriginsCsv) { + const allowedOrigins = String(allowedOriginsCsv || "") + .split(",") + .map((o) => o.trim()) + .filter(Boolean); + + const app = express(); + app.use( + cors({ + origin(origin, callback) { + if (!origin || allowedOrigins.includes("*") || allowedOrigins.includes(origin)) { + return callback(null, true); + } + return callback(null, false); + }, + }) + ); + app.get("/ping", (req, res) => res.json({ ok: true })); + return app; +} + +describe("CORS origin callback", () => { + const app = buildApp("https://ystemandchess.com,http://localhost:3000"); + + test("allowed origin gets the Access-Control-Allow-Origin header", async () => { + 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"); + }); + + test("disallowed origin is rejected without throwing a 500", async () => { + 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("requests with no Origin header (curl, server-to-server) are unaffected", async () => { + const res = await request(app).get("/ping"); + expect(res.status).toBe(200); + }); +}); diff --git a/react-ystemandchess/Dockerfile b/react-ystemandchess/Dockerfile index bd8e975c..cc063db3 100644 --- a/react-ystemandchess/Dockerfile +++ b/react-ystemandchess/Dockerfile @@ -8,6 +8,16 @@ RUN npm install COPY . . +ARG REACT_APP_MIDDLEWARE_URL +ARG REACT_APP_STOCKFISH_SERVER_URL +ARG REACT_APP_CHESS_SERVER_URL +ARG REACT_APP_AGORA_APP_ID + +ENV REACT_APP_MIDDLEWARE_URL=$REACT_APP_MIDDLEWARE_URL +ENV REACT_APP_STOCKFISH_SERVER_URL=$REACT_APP_STOCKFISH_SERVER_URL +ENV REACT_APP_CHESS_SERVER_URL=$REACT_APP_CHESS_SERVER_URL +ENV REACT_APP_AGORA_APP_ID=$REACT_APP_AGORA_APP_ID + RUN npm run build FROM node:18.20.8-alpine @@ -23,4 +33,4 @@ EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=3s \ CMD node -e "require('http').get('http://localhost:3000', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" -CMD ["serve", "-s", "build", "-l", "3000"] \ No newline at end of file +CMD ["serve", "-s", "build", "-l", "3000"] diff --git a/react-ystemandchess/src/App.tsx b/react-ystemandchess/src/App.tsx index 53281882..8275273e 100644 --- a/react-ystemandchess/src/App.tsx +++ b/react-ystemandchess/src/App.tsx @@ -1,6 +1,6 @@ import { useEffect } from "react"; import { BrowserRouter as Router } from "react-router-dom"; -import { environment } from "./environments/environment"; +import { environment } from "./environments"; import { useCookies } from "react-cookie"; import { SetPermissionLevel } from "./globals"; import NavBar from "./components/navbar/NavBar"; diff --git a/react-ystemandchess/src/Pages/Admin/Admin.tsx b/react-ystemandchess/src/Pages/Admin/Admin.tsx index 7ba570a5..80482e09 100644 --- a/react-ystemandchess/src/Pages/Admin/Admin.tsx +++ b/react-ystemandchess/src/Pages/Admin/Admin.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import { useNavigate } from "react-router"; import { useCookies } from "react-cookie"; -import { environment } from "../../environments/environment"; +import { environment } from "../../environments"; const Admin = () => { const navigate = useNavigate(); // verify admin status diff --git a/react-ystemandchess/src/Pages/Admin/admin.test.tsx b/react-ystemandchess/src/Pages/Admin/admin.test.tsx index 41facd9e..ce7500a7 100644 --- a/react-ystemandchess/src/Pages/Admin/admin.test.tsx +++ b/react-ystemandchess/src/Pages/Admin/admin.test.tsx @@ -5,7 +5,7 @@ import { CookiesProvider } from "react-cookie"; import Admin from "./Admin"; // Mock environment -jest.mock("../../environments/environment", () => ({ +jest.mock("../../environments", () => ({ environment: { urls: { middlewareURL: "http://mock-api.com", diff --git a/react-ystemandchess/src/Pages/Analytics/ActivityFeed.tsx b/react-ystemandchess/src/Pages/Analytics/ActivityFeed.tsx index 4e57fd1f..1a28065d 100644 --- a/react-ystemandchess/src/Pages/Analytics/ActivityFeed.tsx +++ b/react-ystemandchess/src/Pages/Analytics/ActivityFeed.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, useCallback } from 'react'; import { useCookies } from 'react-cookie'; -import { environment } from '../../environments/environment'; +import { environment } from '../../environments'; import LoadingSpinner from '../../components/Analytics/LoadingSpinner'; import ErrorBanner from '../../components/Analytics/ErrorBanner'; import { DateRange } from '../../components/Analytics/DateRangeFilter'; diff --git a/react-ystemandchess/src/Pages/Analytics/AnalyticsLayout.test.tsx b/react-ystemandchess/src/Pages/Analytics/AnalyticsLayout.test.tsx index c8769f39..c581d1aa 100644 --- a/react-ystemandchess/src/Pages/Analytics/AnalyticsLayout.test.tsx +++ b/react-ystemandchess/src/Pages/Analytics/AnalyticsLayout.test.tsx @@ -1,7 +1,7 @@ import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import AnalyticsLayout from "./AnalyticsLayout"; -jest.mock("../../environments/environment", () => ({ +jest.mock("../../environments", () => ({ environment: { urls: { middlewareURL: "http://mockurl.com" } }, })); diff --git a/react-ystemandchess/src/Pages/Analytics/GlobalView.tsx b/react-ystemandchess/src/Pages/Analytics/GlobalView.tsx index cbf4bd23..3755031b 100644 --- a/react-ystemandchess/src/Pages/Analytics/GlobalView.tsx +++ b/react-ystemandchess/src/Pages/Analytics/GlobalView.tsx @@ -10,7 +10,7 @@ import { Legend, } from 'chart.js'; import { Pie, Bar } from 'react-chartjs-2'; -import { environment } from '../../environments/environment'; +import { environment } from '../../environments'; import { DateRange } from '../../components/Analytics/DateRangeFilter'; import LoadingSpinner from '../../components/Analytics/LoadingSpinner'; import ErrorBanner from '../../components/Analytics/ErrorBanner'; diff --git a/react-ystemandchess/src/Pages/Analytics/IndividualView.tsx b/react-ystemandchess/src/Pages/Analytics/IndividualView.tsx index 4313eb2a..20ef0250 100644 --- a/react-ystemandchess/src/Pages/Analytics/IndividualView.tsx +++ b/react-ystemandchess/src/Pages/Analytics/IndividualView.tsx @@ -1,6 +1,6 @@ import React, { useState, useCallback } from 'react'; import { useCookies } from 'react-cookie'; -import { environment } from '../../environments/environment'; +import { environment } from '../../environments'; import { DateRange } from '../../components/Analytics/DateRangeFilter'; import LoadingSpinner from '../../components/Analytics/LoadingSpinner'; import ErrorBanner from '../../components/Analytics/ErrorBanner'; diff --git a/react-ystemandchess/src/Pages/Analytics/StudentTimeChart.tsx b/react-ystemandchess/src/Pages/Analytics/StudentTimeChart.tsx index eb2e0221..18eea828 100644 --- a/react-ystemandchess/src/Pages/Analytics/StudentTimeChart.tsx +++ b/react-ystemandchess/src/Pages/Analytics/StudentTimeChart.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from 'react'; import { useCookies } from 'react-cookie'; -import { environment } from '../../environments/environment'; +import { environment } from '../../environments'; import StatsChart from '../../features/student/student-profile/StatsChart'; import LoadingSpinner from '../../components/Analytics/LoadingSpinner'; import ErrorBanner from '../../components/Analytics/ErrorBanner'; diff --git a/react-ystemandchess/src/Pages/Analytics/TrendChart.tsx b/react-ystemandchess/src/Pages/Analytics/TrendChart.tsx index 5379613c..1511760e 100644 --- a/react-ystemandchess/src/Pages/Analytics/TrendChart.tsx +++ b/react-ystemandchess/src/Pages/Analytics/TrendChart.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { useCookies } from 'react-cookie'; -import { environment } from '../../environments/environment'; +import { environment } from '../../environments'; import StatsChart from '../../features/student/student-profile/StatsChart'; import LoadingSpinner from '../../components/Analytics/LoadingSpinner'; import ErrorBanner from '../../components/Analytics/ErrorBanner'; diff --git a/react-ystemandchess/src/Pages/Analytics/ZipcodeView.tsx b/react-ystemandchess/src/Pages/Analytics/ZipcodeView.tsx index 156da169..96f3356d 100644 --- a/react-ystemandchess/src/Pages/Analytics/ZipcodeView.tsx +++ b/react-ystemandchess/src/Pages/Analytics/ZipcodeView.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, useCallback } from 'react'; import { useCookies } from 'react-cookie'; -import { environment } from '../../environments/environment'; +import { environment } from '../../environments'; import { DateRange } from '../../components/Analytics/DateRangeFilter'; import LoadingSpinner from '../../components/Analytics/LoadingSpinner'; import ErrorBanner from '../../components/Analytics/ErrorBanner'; diff --git a/react-ystemandchess/src/components/ChatWidget/ChatWidget.tsx b/react-ystemandchess/src/components/ChatWidget/ChatWidget.tsx index ccaaa192..a26dcbab 100644 --- a/react-ystemandchess/src/components/ChatWidget/ChatWidget.tsx +++ b/react-ystemandchess/src/components/ChatWidget/ChatWidget.tsx @@ -1,6 +1,6 @@ import React, { useState, useRef, useEffect } from 'react'; import './ChatWidget.scss'; -import { environment } from '../../environments/environment'; +import { environment } from '../../environments'; import { useCookies } from 'react-cookie'; import { CoachMascot, CoachExpression } from '../animations/CoachMascot/CoachMascot'; diff --git a/react-ystemandchess/src/core/hooks/useAnalyticsApi.test.ts b/react-ystemandchess/src/core/hooks/useAnalyticsApi.test.ts index 582e7666..495a8fab 100644 --- a/react-ystemandchess/src/core/hooks/useAnalyticsApi.test.ts +++ b/react-ystemandchess/src/core/hooks/useAnalyticsApi.test.ts @@ -1,7 +1,7 @@ import { renderHook, waitFor } from "@testing-library/react"; import { useAnalyticsApi } from "./useAnalyticsApi"; -jest.mock("../../environments/environment", () => ({ +jest.mock("../../environments", () => ({ environment: { urls: { middlewareURL: "http://mockurl.com" } }, })); diff --git a/react-ystemandchess/src/core/hooks/useAnalyticsApi.ts b/react-ystemandchess/src/core/hooks/useAnalyticsApi.ts index b7ec88c7..262652e8 100644 --- a/react-ystemandchess/src/core/hooks/useAnalyticsApi.ts +++ b/react-ystemandchess/src/core/hooks/useAnalyticsApi.ts @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback } from 'react'; import { useCookies } from 'react-cookie'; -import { environment } from '../../environments/environment'; +import { environment } from '../../environments'; export interface AnalyticsParams { from?: string; diff --git a/react-ystemandchess/src/core/services/badgesApi.ts b/react-ystemandchess/src/core/services/badgesApi.ts index 99a96f8c..57c044ad 100644 --- a/react-ystemandchess/src/core/services/badgesApi.ts +++ b/react-ystemandchess/src/core/services/badgesApi.ts @@ -9,7 +9,7 @@ * - getUserBadges: Fetches badges earned by a specific user */ -import { environment } from "../../environments/environment"; +import { environment } from "../../environments"; /** * Fetches the complete catalog of all available badges * diff --git a/react-ystemandchess/src/environments/environment.prod.js b/react-ystemandchess/src/environments/environment.prod.js index 0f1a1460..842809df 100644 --- a/react-ystemandchess/src/environments/environment.prod.js +++ b/react-ystemandchess/src/environments/environment.prod.js @@ -1,12 +1,28 @@ +// Production environment config. +// +// Create React App injects REACT_APP_* variables at build time. +// Production builds must provide all service URLs explicitly. + +const requiredProductionEnv = (name) => { + const value = process.env[name]; + + if (process.env.NODE_ENV === 'production' && !value) { + throw new Error(`Missing required production environment variable: ${name}`); + } + + return value || ''; +}; + export const environment = { - production: false, - agora: { - appId: '6c368b93b82a4b3e9fb8e57da830f2a4', - }, - urls: { - middlewareURL: 'http://localhost/middleware/', - stockfishServerURL: 'http://localhost/stockfishserver/', - chessClientURL: 'http://localhost/chessclient/', - chessServer: 'http://localhost/chessserver/', - }, -}; \ No newline at end of file + production: true, + agora: { + appId: process.env.REACT_APP_AGORA_APP_ID || '', + }, + urls: { + // No trailing slash. Consumers append their own route paths. + middlewareURL: requiredProductionEnv('REACT_APP_MIDDLEWARE_URL'), + stockfishServerURL: requiredProductionEnv('REACT_APP_STOCKFISH_SERVER_URL'), + chessServerURL: requiredProductionEnv('REACT_APP_CHESS_SERVER_URL'), + }, + productionType: 'production', +}; diff --git a/react-ystemandchess/src/features/admin/AdminProfile.tsx b/react-ystemandchess/src/features/admin/AdminProfile.tsx index a60ace1b..2afd1a76 100644 --- a/react-ystemandchess/src/features/admin/AdminProfile.tsx +++ b/react-ystemandchess/src/features/admin/AdminProfile.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from "react"; import { useCookies } from "react-cookie"; import { useNavigate } from "react-router"; import { SetPermissionLevel } from "../../globals"; -import { environment } from "../../environments/environment"; +import { environment } from "../../environments"; import userPortraitImg from "../../assets/images/user-portrait-placeholder.svg"; interface Template { diff --git a/react-ystemandchess/src/features/auth/login/Login.tsx b/react-ystemandchess/src/features/auth/login/Login.tsx index 507a1cac..66f5970b 100644 --- a/react-ystemandchess/src/features/auth/login/Login.tsx +++ b/react-ystemandchess/src/features/auth/login/Login.tsx @@ -1,6 +1,6 @@ import React from "react"; import { useState } from 'react'; -import { environment } from "../../../environments/environment"; +import { environment } from "../../../environments"; import { useCookies } from 'react-cookie'; import stemmy from "../../../assets/images/StreakProgressAssets/stemmy.svg"; import stemette from "../../../assets/images/StreakProgressAssets/stemette.svg"; diff --git a/react-ystemandchess/src/features/auth/reset-password/Reset-Password/resetPasswordService.ts b/react-ystemandchess/src/features/auth/reset-password/Reset-Password/resetPasswordService.ts index b333dc95..64469204 100644 --- a/react-ystemandchess/src/features/auth/reset-password/Reset-Password/resetPasswordService.ts +++ b/react-ystemandchess/src/features/auth/reset-password/Reset-Password/resetPasswordService.ts @@ -5,7 +5,7 @@ * Uses Nodemailer with Gmail to deliver reset links. */ -import { environment } from "../../../../environments/environment"; +import { environment } from "../../../../environments"; const nodemailer = require('nodemailer'); // Configure email transporter with Gmail credentials diff --git a/react-ystemandchess/src/features/auth/signup/AddChild.tsx b/react-ystemandchess/src/features/auth/signup/AddChild.tsx index 7f555e21..31f55ab1 100644 --- a/react-ystemandchess/src/features/auth/signup/AddChild.tsx +++ b/react-ystemandchess/src/features/auth/signup/AddChild.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { useCookies } from "react-cookie"; -import { environment } from "../../../environments/environment"; +import { environment } from "../../../environments"; import AuthLayout from "./AuthLayout"; import stemette from "../../../assets/images/StreakProgressAssets/stemette.svg"; diff --git a/react-ystemandchess/src/features/auth/signup/MentorSignUp.tsx b/react-ystemandchess/src/features/auth/signup/MentorSignUp.tsx index 5dc1b3ad..31d058cf 100644 --- a/react-ystemandchess/src/features/auth/signup/MentorSignUp.tsx +++ b/react-ystemandchess/src/features/auth/signup/MentorSignUp.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { useNavigate } from "react-router-dom"; import { useCookies } from "react-cookie"; -import { environment } from "../../../environments/environment"; +import { environment } from "../../../environments"; import AuthLayout from "./AuthLayout"; import stemmyVine from "../../../assets/images/ActivitiesAssets/stemmy.svg"; diff --git a/react-ystemandchess/src/features/auth/signup/ParentSection.tsx b/react-ystemandchess/src/features/auth/signup/ParentSection.tsx index 84358347..68a2d342 100644 --- a/react-ystemandchess/src/features/auth/signup/ParentSection.tsx +++ b/react-ystemandchess/src/features/auth/signup/ParentSection.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { useCookies } from "react-cookie"; -import { environment } from "../../../environments/environment"; +import { environment } from "../../../environments"; import treesGroup from "../../../assets/images/Trees-Group.png"; import stemmyVine from "../../../assets/images/ActivitiesAssets/stemmy.svg"; import OnboardingSteps from "./OnboardingSteps"; diff --git a/react-ystemandchess/src/features/auth/signup/ParentSignUp.tsx b/react-ystemandchess/src/features/auth/signup/ParentSignUp.tsx index 3413e9cf..c9284bca 100644 --- a/react-ystemandchess/src/features/auth/signup/ParentSignUp.tsx +++ b/react-ystemandchess/src/features/auth/signup/ParentSignUp.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { useNavigate } from "react-router-dom"; import { useCookies } from "react-cookie"; -import { environment } from "../../../environments/environment"; +import { environment } from "../../../environments"; import AuthLayout from "./AuthLayout"; import stemmyVine from "../../../assets/images/ActivitiesAssets/stemmy.svg"; diff --git a/react-ystemandchess/src/features/engine/PlayComputer.test.tsx b/react-ystemandchess/src/features/engine/PlayComputer.test.tsx index 64ef01da..86d6e5fd 100644 --- a/react-ystemandchess/src/features/engine/PlayComputer.test.tsx +++ b/react-ystemandchess/src/features/engine/PlayComputer.test.tsx @@ -42,7 +42,7 @@ jest.mock('../../components/ChessBoard/ChessBoard', () => { }); // Mock environment -jest.mock('../../environments/environment', () => ({ +jest.mock('../../environments', () => ({ environment: { urls: { stockfishServerURL: 'http://localhost:8080', diff --git a/react-ystemandchess/src/features/engine/PlayComputer.tsx b/react-ystemandchess/src/features/engine/PlayComputer.tsx index 03c63f59..4719ff7b 100644 --- a/react-ystemandchess/src/features/engine/PlayComputer.tsx +++ b/react-ystemandchess/src/features/engine/PlayComputer.tsx @@ -6,7 +6,7 @@ import { io } from 'socket.io-client'; import { useLocation } from 'react-router'; import { Move } from '../../core/types/chess'; import ChessBoard, { ChessBoardRef } from '../../components/ChessBoard/ChessBoard'; -import { environment } from "../../environments/environment"; +import { environment } from "../../environments"; // Module styles (placeholder file should exist at the same folder) import styles from './PlayComputer.module.scss'; import StockfishTutor from './StockfishTutor'; diff --git a/react-ystemandchess/src/features/engine/PlayComputerWithTutor.tsx b/react-ystemandchess/src/features/engine/PlayComputerWithTutor.tsx index 4d946fe5..6247bea6 100644 --- a/react-ystemandchess/src/features/engine/PlayComputerWithTutor.tsx +++ b/react-ystemandchess/src/features/engine/PlayComputerWithTutor.tsx @@ -4,7 +4,7 @@ import { Chess as ChessClass } from 'chess.js'; import { io } from 'socket.io-client'; import { Move } from '../../core/types/chess'; import ChessBoard, { ChessBoardRef } from '../../components/ChessBoard/ChessBoard'; -import { environment } from '../../environments/environment'; +import { environment } from '../../environments'; import StockfishTutor from './StockfishTutor'; import styles from './PlayComputer.module.scss'; diff --git a/react-ystemandchess/src/features/engine/StockfishTutor.tsx b/react-ystemandchess/src/features/engine/StockfishTutor.tsx index 24b186fd..5394bf3b 100644 --- a/react-ystemandchess/src/features/engine/StockfishTutor.tsx +++ b/react-ystemandchess/src/features/engine/StockfishTutor.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { environment } from '../../environments/environment'; +import { environment } from '../../environments'; import styles from './StockfishTutor.module.scss'; import { CoachMascot, CoachExpression } from '../../components/animations/CoachMascot/CoachMascot'; import { Chess as ChessClass } from 'chess.js'; diff --git a/react-ystemandchess/src/features/lessons/lessons-selection/LessonsSelection.jsx b/react-ystemandchess/src/features/lessons/lessons-selection/LessonsSelection.jsx index 648c1c23..727ba591 100644 --- a/react-ystemandchess/src/features/lessons/lessons-selection/LessonsSelection.jsx +++ b/react-ystemandchess/src/features/lessons/lessons-selection/LessonsSelection.jsx @@ -1,6 +1,6 @@ import { useNavigate } from "react-router"; import { useState, useEffect, useRef, useCallback, useMemo, memo } from 'react'; -import { environment } from "../../../environments/environment"; +import { environment } from "../../../environments"; import { getAllScenarios } from "../lessons-main/Scenarios"; import { useCookies } from "react-cookie"; diff --git a/react-ystemandchess/src/features/lessons/piece-lessons/lesson-overlay/Lesson-overlay.test.tsx b/react-ystemandchess/src/features/lessons/piece-lessons/lesson-overlay/Lesson-overlay.test.tsx index 55b2fb2f..de3e4ab7 100644 --- a/react-ystemandchess/src/features/lessons/piece-lessons/lesson-overlay/Lesson-overlay.test.tsx +++ b/react-ystemandchess/src/features/lessons/piece-lessons/lesson-overlay/Lesson-overlay.test.tsx @@ -4,7 +4,7 @@ jest.mock("socket.io-client"); // Mock environment -jest.mock("../../../../environments/environment"); +jest.mock("../../../../environments"); // Mock utility modules jest.mock("../../../../core/utils/goalEvaluator"); @@ -103,7 +103,7 @@ const mockIo = io as jest.MockedFunction; // Import mocked modules const goalEvaluator = require("../../../../core/utils/goalEvaluator"); const eventLogger = require("../../../../core/utils/eventLogger"); -const { environment } = require("../../../../environments/environment"); +const { environment } = require("../../../../environments"); // Create socket factory const createMockSocket = () => { diff --git a/react-ystemandchess/src/features/lessons/piece-lessons/lesson-overlay/Lesson-overlay.tsx b/react-ystemandchess/src/features/lessons/piece-lessons/lesson-overlay/Lesson-overlay.tsx index ca31ced4..122f1f65 100644 --- a/react-ystemandchess/src/features/lessons/piece-lessons/lesson-overlay/Lesson-overlay.tsx +++ b/react-ystemandchess/src/features/lessons/piece-lessons/lesson-overlay/Lesson-overlay.tsx @@ -6,7 +6,7 @@ import { io } from 'socket.io-client'; import ChessBoard, { ChessBoardRef } from '../../../../components/ChessBoard/ChessBoard'; import PromotionPopup from '../../lessons-main/PromotionPopup'; import MoveTracker from '../move-tracker/MoveTracker'; -import { environment } from "../../../../environments/environment"; +import { environment } from "../../../../environments"; import { Move } from "../../../../core/types/chess"; import { EvaluationContext, Goal } from '../../../../core/types/goals'; diff --git a/react-ystemandchess/src/features/lessons/piece-lessons/lesson-overlay/hooks/useLessonManager.ts b/react-ystemandchess/src/features/lessons/piece-lessons/lesson-overlay/hooks/useLessonManager.ts index 71dd2194..3a45d1c7 100644 --- a/react-ystemandchess/src/features/lessons/piece-lessons/lesson-overlay/hooks/useLessonManager.ts +++ b/react-ystemandchess/src/features/lessons/piece-lessons/lesson-overlay/hooks/useLessonManager.ts @@ -1,5 +1,5 @@ import { useState, useCallback } from "react"; -import { environment } from "../../../../../environments/environment"; +import { environment } from "../../../../../environments"; export function useLessonManager(piece: string, cookies: any, initialLessonNum?: number) { const [lessonNum, setLessonNum] = useState(initialLessonNum ?? 0); diff --git a/react-ystemandchess/src/features/lessons/piece-lessons/lesson-overlay/hooks/useSocketChessEngine.ts b/react-ystemandchess/src/features/lessons/piece-lessons/lesson-overlay/hooks/useSocketChessEngine.ts index 2b17234e..1bc8ea76 100644 --- a/react-ystemandchess/src/features/lessons/piece-lessons/lesson-overlay/hooks/useSocketChessEngine.ts +++ b/react-ystemandchess/src/features/lessons/piece-lessons/lesson-overlay/hooks/useSocketChessEngine.ts @@ -1,4 +1,4 @@ -import { environment } from "../../../../../environments/environment"; +import { environment } from "../../../../../environments"; import { useEffect, useRef } from "react"; import io from "socket.io-client"; diff --git a/react-ystemandchess/src/features/lessons/piece-lessons/lesson-overlay/hooks/useTimeTracking.test.ts b/react-ystemandchess/src/features/lessons/piece-lessons/lesson-overlay/hooks/useTimeTracking.test.ts index 37cd0a90..cfba444d 100644 --- a/react-ystemandchess/src/features/lessons/piece-lessons/lesson-overlay/hooks/useTimeTracking.test.ts +++ b/react-ystemandchess/src/features/lessons/piece-lessons/lesson-overlay/hooks/useTimeTracking.test.ts @@ -8,7 +8,7 @@ jest.mock("../../../../../globals", () => ({ })); // mock environment URL -jest.mock("../../../../../environments/environment", () => ({ +jest.mock("../../../../../environments", () => ({ environment: { urls: { middlewareURL: "http://mockurl.com" } }, })); diff --git a/react-ystemandchess/src/features/lessons/piece-lessons/lesson-overlay/hooks/useTimeTracking.ts b/react-ystemandchess/src/features/lessons/piece-lessons/lesson-overlay/hooks/useTimeTracking.ts index 15f728bb..c1f70db3 100644 --- a/react-ystemandchess/src/features/lessons/piece-lessons/lesson-overlay/hooks/useTimeTracking.ts +++ b/react-ystemandchess/src/features/lessons/piece-lessons/lesson-overlay/hooks/useTimeTracking.ts @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from "react"; -import { environment } from "../../../../../environments/environment"; +import { environment } from "../../../../../environments"; import { SetPermissionLevel } from "../../../../../globals"; export function useTimeTracking(piece: string, cookies: any) { diff --git a/react-ystemandchess/src/features/mentor/mentor-profile/NewMentorProfile.tsx b/react-ystemandchess/src/features/mentor/mentor-profile/NewMentorProfile.tsx index 4e6429f1..f0210a5c 100644 --- a/react-ystemandchess/src/features/mentor/mentor-profile/NewMentorProfile.tsx +++ b/react-ystemandchess/src/features/mentor/mentor-profile/NewMentorProfile.tsx @@ -3,7 +3,7 @@ import "./NewMentorProfile.scss"; import Images from "../../../assets/images/imageImporter"; import { SetPermissionLevel } from '../../../globals'; import { useCookies } from 'react-cookie'; -import { environment } from "../../../environments/environment"; +import { environment } from "../../../environments"; import { useNavigate } from "react-router"; import StatsChart from "../../student/student-profile/StatsChart"; import Lessons from "../../lessons/lessons-main/Lessons"; diff --git a/react-ystemandchess/src/features/mentor/mentor-profile/NewMentorProfile/NewMentorProfile.tsx b/react-ystemandchess/src/features/mentor/mentor-profile/NewMentorProfile/NewMentorProfile.tsx index b00c3cef..69c96761 100644 --- a/react-ystemandchess/src/features/mentor/mentor-profile/NewMentorProfile/NewMentorProfile.tsx +++ b/react-ystemandchess/src/features/mentor/mentor-profile/NewMentorProfile/NewMentorProfile.tsx @@ -3,7 +3,7 @@ import "./NewMentorProfile.scss"; import Images from "../../../../assets/images/imageImporter"; import { SetPermissionLevel } from '../../../../globals'; import { useCookies } from 'react-cookie'; -import { environment } from "../../../../environments/environment"; +import { environment } from "../../../../environments"; import { useNavigate } from "react-router"; import StatsChart from "../../../student/student-profile/StatsChart"; import Lessons from "../../../lessons/lessons-main/Lessons"; diff --git a/react-ystemandchess/src/features/puzzles/Puzzles.tsx b/react-ystemandchess/src/features/puzzles/Puzzles.tsx index bce687f7..3a22153a 100644 --- a/react-ystemandchess/src/features/puzzles/Puzzles.tsx +++ b/react-ystemandchess/src/features/puzzles/Puzzles.tsx @@ -4,7 +4,7 @@ import { themesDescription, } from "../../core/services/themesService"; import Modal, { ModalProps } from "../../components/modal/Modal"; -import { environment } from "../../environments/environment"; +import { environment } from "../../environments"; import { v4 as uuidv4 } from "uuid"; import { SetPermissionLevel } from "../../globals"; import { useCookies } from "react-cookie"; diff --git a/react-ystemandchess/src/features/student/student-page/Student.tsx b/react-ystemandchess/src/features/student/student-page/Student.tsx index 4d6889ed..a8e28196 100644 --- a/react-ystemandchess/src/features/student/student-page/Student.tsx +++ b/react-ystemandchess/src/features/student/student-page/Student.tsx @@ -2,7 +2,7 @@ import React, { useState, useRef } from "react"; import "./Student.scss"; import ChessBoard, { ChessBoardRef } from "../../../components/ChessBoard/ChessBoard"; import { useChessSocket } from "../../lessons/piece-lessons/lesson-overlay/hooks/useChessSocket"; -import { environment } from "../../../environments/environment"; +import { environment } from "../../../environments"; import { Move } from "../../../core/types/chess"; import { v4 as uuidv4 } from "uuid"; import ChatWidget from '../../../components/ChatWidget/ChatWidget'; diff --git a/react-ystemandchess/src/features/student/student-profile/Modals/ActivitiesModal.tsx b/react-ystemandchess/src/features/student/student-profile/Modals/ActivitiesModal.tsx index 0cf8910f..999175fc 100644 --- a/react-ystemandchess/src/features/student/student-profile/Modals/ActivitiesModal.tsx +++ b/react-ystemandchess/src/features/student/student-profile/Modals/ActivitiesModal.tsx @@ -26,7 +26,7 @@ import { ReactComponent as TopicBag } from "../../../../assets/images/Activities import { ReactComponent as ShortBottomVine} from "../../../../assets/images/ActivitiesAssets/short_bottom_vine.svg"; import { ReactComponent as BottomVine} from "../../../../assets/images/ActivitiesAssets/bottom_vine.svg"; import { ReactComponent as Stemmy} from "../../../../assets/images/ActivitiesAssets/stemmy.svg"; -import { environment } from "../../../../environments/environment"; +import { environment } from "../../../../environments"; import { useCookies } from "react-cookie"; import { parseActivities } from "../../../../core/utils/activityNames"; diff --git a/react-ystemandchess/src/features/student/student-profile/Modals/LeaderboardModal.tsx b/react-ystemandchess/src/features/student/student-profile/Modals/LeaderboardModal.tsx index 367a17a4..beeaa0c0 100644 --- a/react-ystemandchess/src/features/student/student-profile/Modals/LeaderboardModal.tsx +++ b/react-ystemandchess/src/features/student/student-profile/Modals/LeaderboardModal.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import { useCookies } from "react-cookie"; import "./LeaderboardModal.scss"; import { ReactComponent as LeaderboardIcon } from "../../../../assets/images/student/leaderboard_sidebar_icon.svg"; -import { environment } from "../../../../environments/environment"; +import { environment } from "../../../../environments"; import rank1Img from "../../../../assets/images/student/Leaderboard_rank_1.svg"; import rank2Img from "../../../../assets/images/student/Leaderboard_rank_2.svg"; diff --git a/react-ystemandchess/src/features/student/student-profile/NewStudentProfile.tsx b/react-ystemandchess/src/features/student/student-profile/NewStudentProfile.tsx index 9ebfc156..de971eba 100644 --- a/react-ystemandchess/src/features/student/student-profile/NewStudentProfile.tsx +++ b/react-ystemandchess/src/features/student/student-profile/NewStudentProfile.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useRef, useMemo, lazy, Suspense } from "react"; import { SetPermissionLevel } from '../../../globals'; import { useCookies } from 'react-cookie'; -import { environment } from "../../../environments/environment"; +import { environment } from "../../../environments"; import { useNavigate } from "react-router"; import StatsChart from "./StatsChart"; import Puzzles from "../../puzzles/Puzzles"; diff --git a/react-ystemandchess/src/globals.ts b/react-ystemandchess/src/globals.ts index ae5fd9ad..d17f9df9 100644 --- a/react-ystemandchess/src/globals.ts +++ b/react-ystemandchess/src/globals.ts @@ -5,7 +5,7 @@ * components, particularly for authentication and permission management. */ -import { environment } from "./environments/environment"; +import { environment } from "./environments"; /** * Global variable to store user information after authentication diff --git a/stockfishServer/.env.example b/stockfishServer/.env.example new file mode 100644 index 00000000..7d7bca16 --- /dev/null +++ b/stockfishServer/.env.example @@ -0,0 +1,4 @@ +NODE_ENV=development +PORT=8080 +CORS_ORIGIN=http://localhost:3000 +ALLOWED_ORIGINS= diff --git a/stockfishServer/src/index.js b/stockfishServer/src/index.js index 387ee56a..66cd0ba3 100644 --- a/stockfishServer/src/index.js +++ b/stockfishServer/src/index.js @@ -1,20 +1,29 @@ require("dotenv").config(); +const validateEnvironment = require("./validateEnvironment"); +validateEnvironment(); + const express = require("express"); const http = require("http"); const { Server } = require("socket.io"); const cors = require("cors"); const initializeSocket = require("./managers/socket"); +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("*"); @@ -28,7 +37,7 @@ const corsOptions = { if (allowedOrigins.indexOf(origin) !== -1) { callback(null, true); } else { - callback(new Error(`Origin ${origin} not allowed by CORS`)); + callback(null, false); } }, methods: ["GET", "POST"], diff --git a/stockfishServer/src/validateEnvironment.js b/stockfishServer/src/validateEnvironment.js new file mode 100644 index 00000000..4c94408f --- /dev/null +++ b/stockfishServer/src/validateEnvironment.js @@ -0,0 +1,29 @@ +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 = []; + + if (!hasCorsConfiguration()) { + missing.push("CORS_ORIGIN or ALLOWED_ORIGINS"); + } + + if (missing.length > 0) { + console.error( + `[stockfishServer] Missing required production environment variables: ${missing.join(", ")}` + ); + process.exit(1); + } + + console.log("[stockfishServer] Required production environment variables validated"); +} + +module.exports = validateEnvironment;