Compare commits
24 Commits
f07c1c8577
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| 79b67e9003 | |||
| 7fe6cda825 | |||
| f142cac64a | |||
| 99d24daff2 | |||
| f7f8798f43 | |||
| ed0b6cf589 | |||
| 7a55f6c376 | |||
| 274c0b98e3 | |||
| 64517450f7 | |||
| fb68e6d43a | |||
| 04bc50d6ee | |||
| 3f7cadcd63 | |||
| e9e5eb32ad | |||
| 5b02a13cb4 | |||
| 4cd0fe970f | |||
| e66965dfc1 | |||
| 98cf490ad0 | |||
| 4c0286952c | |||
| c835846c2d | |||
| 4366b6aba4 | |||
| 0b21489989 | |||
| 766614f5d2 | |||
| e5a69fa4a9 | |||
| 832102c9f4 |
+40
-5
@@ -1,12 +1,13 @@
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
aws-cli: circleci/aws-cli@4.1.2
|
||||
aws-ecr: circleci/aws-ecr@9.4.0
|
||||
aws-ecs: circleci/aws-ecs@4.0.0
|
||||
jobs:
|
||||
build_and_test:
|
||||
docker:
|
||||
- image: circleci/node:14
|
||||
|
||||
working_directory: ~/app
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
@@ -26,9 +27,43 @@ jobs:
|
||||
name: Test Frontend
|
||||
command: |
|
||||
npm test
|
||||
run_task:
|
||||
docker:
|
||||
- image: cimg/python:3.10
|
||||
steps:
|
||||
- aws-cli/setup:
|
||||
role_arn: arn:aws:iam::982081055194:role/circleci-ecs-ecr
|
||||
role_session_name: example-session
|
||||
- aws-ecs/run_task:
|
||||
awsvpc: true
|
||||
assign_public_ip: ENABLED
|
||||
cluster: arn:aws:ecs:eu-north-1:982081055194:cluster/mern-ludo-cluster
|
||||
launch_type: FARGATE
|
||||
task_definition: mern-ludo-fargate
|
||||
subnet_ids: subnet-06f63f5063b74a7c9,subnet-07cdbe8b01b46e0b7,subnet-06dbac60591c79f5d
|
||||
security_group_ids: sg-0cfd2369802b8be43
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build:
|
||||
build_and_test_and_deploy:
|
||||
jobs:
|
||||
- build_and_test
|
||||
- aws-ecr/build_and_push_image:
|
||||
auth:
|
||||
- aws-cli/setup:
|
||||
role_arn: arn:aws:iam::982081055194:role/circleci-ecs-ecr
|
||||
role_session_name: ecr-session
|
||||
repo: mern-ludo
|
||||
public_registry: true
|
||||
requires:
|
||||
- build_and_test
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- main
|
||||
- run_task:
|
||||
requires:
|
||||
- aws-ecr/build_and_push_image
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- main
|
||||
@@ -0,0 +1,35 @@
|
||||
# Include any files or directories that you don't want to be copied to your
|
||||
# container here (e.g., local build artifacts, temporary files, etc.).
|
||||
#
|
||||
# For more help, visit the .dockerignore file reference guide at
|
||||
# https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
||||
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/.next
|
||||
**/.cache
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
**/build
|
||||
**/dist
|
||||
LICENSE
|
||||
README.md
|
||||
node_modules
|
||||
@@ -0,0 +1,8 @@
|
||||
# MongoDB connection for backend
|
||||
CONNECTION_URI=mongodb://admin:adminpassword@192.168.0.197:27018/ludo?authSource=admin&replicaSet=rs0
|
||||
|
||||
# Backend port
|
||||
PORT=3000
|
||||
|
||||
# Environment
|
||||
NODE_ENV=development
|
||||
+2
-1
@@ -9,7 +9,7 @@ backend/node_modules
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
@@ -21,3 +21,4 @@ backend/node_modules
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
# .env
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
# FROM node:14 as frontend
|
||||
# WORKDIR /app
|
||||
# COPY . /app
|
||||
# RUN npm install --production
|
||||
# RUN npm run build
|
||||
|
||||
# FROM node:14 as backend
|
||||
# WORKDIR /app
|
||||
# COPY /backend /app
|
||||
# RUN npm install
|
||||
|
||||
# FROM node:14
|
||||
# WORKDIR /app
|
||||
# COPY --from=backend /app /app/
|
||||
# COPY --from=frontend /app/build /app/build
|
||||
|
||||
# EXPOSE 8080
|
||||
|
||||
# CMD ["npm", "run", "start"]
|
||||
|
||||
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const mongoose = require('mongoose');
|
||||
require('dotenv').config();
|
||||
const { sessionMiddleware } = require('./config/session');
|
||||
|
||||
const PORT = process.env.PORT || 5000;
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(cookieParser());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.json());
|
||||
app.set('trust proxy', 1);
|
||||
|
||||
/* ---------- CORS ---------- */
|
||||
app.use(
|
||||
cors({
|
||||
origin:
|
||||
process.env.NODE_ENV === 'production'
|
||||
? true // same origin (Docker / prod)
|
||||
: 'http://localhost:3000',
|
||||
credentials: true,
|
||||
})
|
||||
);
|
||||
|
||||
app.use(sessionMiddleware);
|
||||
|
||||
const server = app.listen(PORT, "0.0.0.0", () => {
|
||||
console.log(`Server listening on port ${PORT}`);
|
||||
});
|
||||
|
||||
/* ---------- Mongo + Socket ---------- */
|
||||
require('./config/database')(mongoose);
|
||||
require('./config/socket')(server);
|
||||
|
||||
/* ---------- Serve React build ---------- */
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
const buildPath = path.resolve(__dirname, 'build');
|
||||
|
||||
app.use(express.static(buildPath));
|
||||
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(buildPath, 'index.html'));
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { server };
|
||||
|
||||
# Node 20 for Pi 5 (ARM64)
|
||||
FROM node:20-bullseye-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy all files
|
||||
COPY . /app
|
||||
|
||||
# Install frontend (root) dependencies
|
||||
RUN npm install --legacy-peer-deps
|
||||
|
||||
# Build frontend
|
||||
RUN npm run build
|
||||
|
||||
# Install backend dependencies
|
||||
RUN cd backend && npm install --production --legacy-peer-deps
|
||||
|
||||
# Move frontend build into backend/public for Express
|
||||
# RUN cp -r build backend/public
|
||||
RUN rm -rf backend/build
|
||||
RUN cp -r build backend/build
|
||||
|
||||
# Copy wait-for-mongo.sh and make it executable
|
||||
COPY wait-for-mongo.sh ./
|
||||
RUN chmod +x wait-for-mongo.sh
|
||||
|
||||
# Default fallback values (can be overridden by Compose)
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=8080
|
||||
ENV CONNECTION_URI=mongodb://192.168.0.197:27017/ludo?replicaSet=rs0
|
||||
|
||||
EXPOSE 18081
|
||||
|
||||
# Start backend with wait-for-mongo
|
||||
CMD ["bash", "./wait-for-mongo.sh", "mongo", "27017", "node", "backend/server.js"]
|
||||
@@ -1,11 +1,10 @@
|
||||
# <center>Online Multiplayer Ludo Game</center>
|
||||
|
||||
## About
|
||||
<h1 align="center">Online Multiplayer Ludo Game </h1>
|
||||
|
||||
Ludo Online is a multiplayer web-based implementation of the classic board game Ludo, built using the MERN stack and integrated with SocketIO for real-time communication.
|
||||
|
||||
\>\> Play Online here <<
|
||||
\>\> [Watch YouTube Video here](https://youtu.be/mGMnH9Nvsyw) <<
|
||||
<p align="center">
|
||||
>> <a href="https://youtu.be/mGMnH9Nvsyw">Watch YouTube Video here</a> <<
|
||||
</p>
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -14,34 +13,49 @@ Ludo Online is a multiplayer web-based implementation of the classic board game
|
||||
## Tech Stack
|
||||
|
||||
Frontend:
|
||||
|
||||
  
|
||||
  
|
||||
|
||||
Backend:
|
||||
|
||||
   
|
||||
|
||||
Tests:
|
||||
|
||||
  
|
||||
Tools:
|
||||
|
||||
Other:
|
||||
|
||||
    
|
||||
|
||||
## Key Features and Challenges
|
||||
|
||||
- Maintained session consistency with **Express Session** and **MongoDB**.
|
||||
|
||||
- Enabled real-time communication via **WebSocket** and **SocketIO**.
|
||||
|
||||
- Maintained code reliability by implementing unit and integration tests using **Mocha**, **Chai**, and **Jest**.
|
||||
- Hosted in a **Docker** container on **AWS EC2**.
|
||||
- Established CI/CD using **CircleCI**.
|
||||
|
||||
- Implemented E2E tests utilizing **Cypress**, addressing challenges related to [testing collaboration](https://docs.cypress.io/guides/references/trade-offs#Multiple-browsers-open-at-the-same-time) and canvas functionality in the application.
|
||||
|
||||
- Established a CI/CD pipeline using **CircleCI**, with pushing **Docker** container to **AWS ECR** and deploying to **AWS ECS**
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
1. Download this repository
|
||||
|
||||
2. Generate your own [mongoDB atlas](https://www.mongodb.com) credential URL. It should looks like this:
|
||||
|
||||
```
|
||||
|
||||
mongodb+srv://madmin:<password>@clustername.mongodb.net/<dbname>?retryWrites=true&w=majority
|
||||
|
||||
```
|
||||
|
||||
3. Add this URL to the /backend/credentials.js file
|
||||
3. Add this URL to the /backend/.env file (refer to .env.example)
|
||||
|
||||
4. Perform these commands in the main directory:
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
PORT=8080
|
||||
# MongoDB connection for backend
|
||||
CONNECTION_URI=mongodb://admin:adminpassword@192.168.0.197:27018/ludo?authSource=admin&replicaSet=rs0
|
||||
|
||||
NODE_ENV="development"
|
||||
+11
-12
@@ -1,15 +1,14 @@
|
||||
const CONNECTION_URI = require('../credentials.js');
|
||||
|
||||
module.exports = function (mongoose) {
|
||||
mongoose.set('useFindAndModify', false);
|
||||
mongoose
|
||||
.connect(CONNECTION_URI, {
|
||||
module.exports = async function (mongoose) {
|
||||
try {
|
||||
console.log('🔌 Attempting to connect with URI:', process.env.CONNECTION_URI);
|
||||
await mongoose.connect(process.env.CONNECTION_URI, {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
dbName: 'test',
|
||||
})
|
||||
.then(() => {
|
||||
console.log('MongoDB Connected…');
|
||||
})
|
||||
.catch(err => console.error(err));
|
||||
directConnection: true,
|
||||
});
|
||||
console.log('✅ MongoDB connected');
|
||||
} catch (err) {
|
||||
console.error('❌ MongoDB connection error:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,10 +1,41 @@
|
||||
const session = require('express-session');
|
||||
const CONNECTION_URI = require('../credentials.js');
|
||||
const MongoDBStore = require('connect-mongodb-session')(session);
|
||||
const store = new MongoDBStore({
|
||||
uri: CONNECTION_URI,
|
||||
|
||||
console.log('📋 Session.js - CONNECTION_URI:', process.env.CONNECTION_URI);
|
||||
|
||||
// Parse the connection URI to extract connection options
|
||||
const uriString = process.env.CONNECTION_URI;
|
||||
const uriUrl = new URL(uriString);
|
||||
|
||||
// Extract individual components
|
||||
const baseUri = `${uriUrl.protocol}//${uriUrl.username}:${uriUrl.password}@${uriUrl.hostname}:${uriUrl.port}${uriUrl.pathname}`;
|
||||
const replicaSet = uriUrl.searchParams.get('replicaSet');
|
||||
const authSource = uriUrl.searchParams.get('authSource');
|
||||
|
||||
console.log('📋 Base URI:', baseUri);
|
||||
console.log('📋 ReplicaSet:', replicaSet);
|
||||
console.log('📋 AuthSource:', authSource);
|
||||
|
||||
// Build connection options with directConnection to bypass replica set discovery
|
||||
const connectionOptions = {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
directConnection: true, // Force direct connection to specified server
|
||||
};
|
||||
|
||||
if (authSource) {
|
||||
connectionOptions.authSource = authSource;
|
||||
}
|
||||
|
||||
const storeOptions = {
|
||||
uri: baseUri,
|
||||
collection: 'sessions',
|
||||
});
|
||||
connectionOptions: connectionOptions,
|
||||
};
|
||||
|
||||
console.log('📋 Store options:', JSON.stringify(storeOptions, null, 2));
|
||||
|
||||
const store = new MongoDBStore(storeOptions);
|
||||
const sessionMiddleware = session({
|
||||
store: store,
|
||||
credentials: true,
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
// Write your own mongoDBatlas credentials here
|
||||
module.exports = '';
|
||||
@@ -12,9 +12,19 @@ module.exports = socket => {
|
||||
if (isMoveValid(req.session, pawn, room)) {
|
||||
const newPositionOfMovedPawn = pawn.getPositionAfterMove(room.rolledNumber);
|
||||
room.changePositionOfPawn(pawn, newPositionOfMovedPawn);
|
||||
room.beatPawns(newPositionOfMovedPawn, req.session.color);
|
||||
room.changeMovingPlayer();
|
||||
const beaten = room.beatPawns(newPositionOfMovedPawn, req.session.color);
|
||||
// If pawn killed any opponent pawns, attacker gets another turn.
|
||||
// Also a roll of 6 grants another turn.
|
||||
// Additionally, grant an extra turn when a pawn reaches its final home position.
|
||||
const FINAL_POSITIONS = { red: 73, blue: 79, green: 85, yellow: 91 };
|
||||
const reachedHome = FINAL_POSITIONS[pawn.color] === newPositionOfMovedPawn;
|
||||
const winner = room.getWinner();
|
||||
if ((beaten > 0 || room.rolledNumber === 6 || reachedHome) && !winner) {
|
||||
room.resetTurnForSamePlayer();
|
||||
} else {
|
||||
room.changeMovingPlayer();
|
||||
}
|
||||
// const winner = room.getWinner();
|
||||
if (winner) {
|
||||
room.endGame(winner);
|
||||
sendWinner(room._id.toString(), winner);
|
||||
|
||||
@@ -19,7 +19,12 @@ const makeRandomMove = async roomId => {
|
||||
const randomPawn = pawnsThatCanMove[Math.floor(Math.random() * pawnsThatCanMove.length)];
|
||||
room.movePawn(randomPawn);
|
||||
}
|
||||
room.changeMovingPlayer();
|
||||
// If player rolled a 6 they should get another turn: keep same player but reset timer
|
||||
if (room.rolledNumber !== 6) {
|
||||
room.changeMovingPlayer();
|
||||
} else {
|
||||
room.resetTurnForSamePlayer();
|
||||
}
|
||||
const winner = room.getWinner();
|
||||
if (winner) {
|
||||
room.endGame(winner);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const { getRoom, updateRoom } = require('../services/roomService');
|
||||
const { colors } = require('../utils/constants');
|
||||
const { COLORS } = require('../utils/constants');
|
||||
|
||||
module.exports = socket => {
|
||||
const req = socket.request;
|
||||
@@ -44,7 +44,7 @@ module.exports = socket => {
|
||||
if (err) return socket.disconnect();
|
||||
req.session.roomId = room._id.toString();
|
||||
req.session.playerId = room.players[room.players.length - 1]._id.toString();
|
||||
req.session.color = colors[room.players.length - 1];
|
||||
req.session.color = COLORS[room.players.length - 1];
|
||||
req.session.save();
|
||||
socket.join(room._id.toString());
|
||||
socket.emit('player:data', JSON.stringify(req.session));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { getRooms, getRoom, updateRoom, createNewRoom } = require('../services/roomService');
|
||||
const { getRooms, getRoom, updateRoom, createNewRoom, deleteRoom } = require('../services/roomService');
|
||||
const { sendToOnePlayerRooms, sendToOnePlayerData, sendWinner } = require('../socket/emits');
|
||||
|
||||
module.exports = socket => {
|
||||
@@ -8,6 +8,7 @@ module.exports = socket => {
|
||||
const room = await getRoom(req.session.roomId);
|
||||
// Handle the situation when the server crashes and any player reconnects after the time has expired
|
||||
// Typically, the responsibility for changing players is managed by gameHandler.js.
|
||||
if(!room) return;
|
||||
if (room.nextMoveTime <= Date.now()) {
|
||||
room.changeMovingPlayer();
|
||||
await updateRoom(room);
|
||||
@@ -26,7 +27,23 @@ module.exports = socket => {
|
||||
sendToOnePlayerRooms(socket.id, await getRooms());
|
||||
};
|
||||
|
||||
const handleDeleteRoom = async roomId => {
|
||||
try {
|
||||
console.log('🗑️ Attempting to delete room:', roomId);
|
||||
const result = await deleteRoom(roomId);
|
||||
console.log('✅ Room deleted successfully:', result);
|
||||
const updatedRooms = await getRooms();
|
||||
console.log('📋 Updated room count:', updatedRooms.length);
|
||||
console.log('📤 Sending updated rooms to socket:', socket.id);
|
||||
sendToOnePlayerRooms(socket.id, updatedRooms);
|
||||
} catch (error) {
|
||||
console.error('❌ Error deleting room:', error);
|
||||
socket.emit('error:deleteRoom', 'Failed to delete room');
|
||||
}
|
||||
};
|
||||
|
||||
socket.on('room:data', handleGetData);
|
||||
socket.on('room:rooms', handleGetAllRooms);
|
||||
socket.on('room:create', handleCreateRoom);
|
||||
socket.on('room:delete', handleDeleteRoom);
|
||||
};
|
||||
|
||||
@@ -9,7 +9,8 @@ const PawnSchema = new Schema({
|
||||
});
|
||||
|
||||
PawnSchema.methods.canMove = function (rolledNumber) {
|
||||
if (this.position === this.basePos && (rolledNumber === 6 || rolledNumber === 1)) {
|
||||
// Pawn can leave base only when a 6 is rolled
|
||||
if (this.position === this.basePos && rolledNumber === 6) {
|
||||
return true;
|
||||
}
|
||||
// (if player's pawn is near finish line) if the move does not go beyond the win line
|
||||
|
||||
+103
-30
@@ -1,9 +1,21 @@
|
||||
const mongoose = require('mongoose');
|
||||
const { colors } = require('../utils/constants');
|
||||
const { COLORS, MOVE_TIME } = require('../utils/constants');
|
||||
const { makeRandomMove } = require('../handlers/handlersFunctions');
|
||||
const timeoutManager = require('./timeoutManager.js');
|
||||
const PawnSchema = require('./pawn');
|
||||
const PlayerSchema = require('./player');
|
||||
|
||||
// Safe/colored box positions in Ludo (where pawns cannot be killed)
|
||||
const SAFE_POSITIONS = [16, 29, 42, 55];
|
||||
// const HOME_ENTRY_POSITIONS = [66, 27, 40, 53];
|
||||
const STAR_POSITIONS = [63, 24, 37, 50];
|
||||
|
||||
const isSafePosition = (position) => {
|
||||
return SAFE_POSITIONS.includes(position)
|
||||
|| STAR_POSITIONS.includes(position)
|
||||
|| position > 66; // Also safe in home stretch
|
||||
};
|
||||
|
||||
const RoomSchema = new mongoose.Schema({
|
||||
name: String,
|
||||
private: { type: Boolean, default: false },
|
||||
@@ -12,7 +24,6 @@ const RoomSchema = new mongoose.Schema({
|
||||
started: { type: Boolean, default: false },
|
||||
full: { type: Boolean, default: false },
|
||||
nextMoveTime: Number,
|
||||
timeoutID: Number,
|
||||
rolledNumber: Number,
|
||||
players: [PlayerSchema],
|
||||
winner: { type: String, default: null },
|
||||
@@ -24,10 +35,10 @@ const RoomSchema = new mongoose.Schema({
|
||||
let pawn = {};
|
||||
pawn.basePos = i;
|
||||
pawn.position = i;
|
||||
if (i < 4) pawn.color = colors[0];
|
||||
else if (i < 8) pawn.color = colors[1];
|
||||
else if (i < 12) pawn.color = colors[2];
|
||||
else if (i < 16) pawn.color = colors[3];
|
||||
if (i < 4) pawn.color = COLORS[0];
|
||||
else if (i < 8) pawn.color = COLORS[1];
|
||||
else if (i < 12) pawn.color = COLORS[2];
|
||||
else if (i < 16) pawn.color = COLORS[3];
|
||||
startPositions.push(pawn);
|
||||
}
|
||||
return startPositions;
|
||||
@@ -36,28 +47,63 @@ const RoomSchema = new mongoose.Schema({
|
||||
});
|
||||
|
||||
RoomSchema.methods.beatPawns = function (position, attackingPawnColor) {
|
||||
// Do not beat pawns on safe/colored positions
|
||||
if (isSafePosition(position)) {
|
||||
return 0;
|
||||
}
|
||||
let beatenCount = 0;
|
||||
const pawnsOnPosition = this.pawns.filter(pawn => pawn.position === position);
|
||||
pawnsOnPosition.forEach(pawn => {
|
||||
if (pawn.color !== attackingPawnColor) {
|
||||
const index = this.getPawnIndex(pawn._id);
|
||||
this.pawns[index].position = this.pawns[index].basePos;
|
||||
if (index !== -1 && this.pawns[index].position !== this.pawns[index].basePos) {
|
||||
this.pawns[index].position = this.pawns[index].basePos;
|
||||
beatenCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
return beatenCount;
|
||||
};
|
||||
|
||||
RoomSchema.methods.changeMovingPlayer = function () {
|
||||
if (this.winner) return;
|
||||
const playerIndex = this.players.findIndex(player => player.nowMoving === true);
|
||||
this.players[playerIndex].nowMoving = false;
|
||||
if (playerIndex + 1 === this.players.length) {
|
||||
if (!Array.isArray(this.players) || this.players.length === 0) {
|
||||
console.warn(`[room:${this._id}] changeMovingPlayer: players array is empty or null`);
|
||||
return;
|
||||
}
|
||||
const playerIndex = this.players.findIndex(player => player && player.nowMoving === true);
|
||||
if (playerIndex === -1) {
|
||||
console.warn(`[room:${this._id}] changeMovingPlayer: no player currently marked as nowMoving`);
|
||||
// Default to first player
|
||||
this.players[0].nowMoving = true;
|
||||
} else {
|
||||
this.players[playerIndex + 1].nowMoving = true;
|
||||
this.players[playerIndex].nowMoving = false;
|
||||
if (playerIndex + 1 === this.players.length) {
|
||||
this.players[0].nowMoving = true;
|
||||
} else {
|
||||
this.players[playerIndex + 1].nowMoving = true;
|
||||
}
|
||||
}
|
||||
this.nextMoveTime = Date.now() + 15000;
|
||||
this.nextMoveTime = Date.now() + MOVE_TIME;
|
||||
this.rolledNumber = null;
|
||||
if (this.timeoutID) clearTimeout(this.timeoutID);
|
||||
this.timeoutID = setTimeout(makeRandomMove, 15000, this._id.toString());
|
||||
timeoutManager.clear(this._id.toString());
|
||||
timeoutManager.set(makeRandomMove, MOVE_TIME, this._id.toString());
|
||||
};
|
||||
|
||||
RoomSchema.methods.resetTurnForSamePlayer = function () {
|
||||
if (this.winner) return;
|
||||
// Keep the same player moving but reset the roll and move timer
|
||||
const now = Date.now();
|
||||
const currentPlayer = this.getCurrentlyMovingPlayer();
|
||||
if (!currentPlayer) {
|
||||
console.warn(`[room:${this._id}] resetTurnForSamePlayer: No current player found!`);
|
||||
return;
|
||||
}
|
||||
this.nextMoveTime = now + MOVE_TIME;
|
||||
this.rolledNumber = null;
|
||||
console.log(`[room:${this._id}] resetTurnForSamePlayer: player=${currentPlayer.color}, now=${now}, nextMoveTime=${this.nextMoveTime}, MOVE_TIME=${MOVE_TIME}`);
|
||||
timeoutManager.clear(this._id.toString());
|
||||
timeoutManager.set(makeRandomMove, MOVE_TIME, this._id.toString());
|
||||
};
|
||||
|
||||
RoomSchema.methods.movePawn = function (pawn) {
|
||||
@@ -68,8 +114,9 @@ RoomSchema.methods.movePawn = function (pawn) {
|
||||
|
||||
RoomSchema.methods.getPawnsThatCanMove = function () {
|
||||
const movingPlayer = this.getCurrentlyMovingPlayer();
|
||||
if (!movingPlayer) return [];
|
||||
const playerPawns = this.getPlayerPawns(movingPlayer.color);
|
||||
return playerPawns.filter(pawn => pawn.canMove(this.rolledNumber));
|
||||
return (playerPawns || []).filter(pawn => pawn.canMove(this.rolledNumber));
|
||||
};
|
||||
|
||||
RoomSchema.methods.changePositionOfPawn = function (pawn, newPosition) {
|
||||
@@ -78,43 +125,50 @@ RoomSchema.methods.changePositionOfPawn = function (pawn, newPosition) {
|
||||
};
|
||||
|
||||
RoomSchema.methods.canStartGame = function () {
|
||||
return this.players.filter(player => player.ready).length >= 2;
|
||||
if (!Array.isArray(this.players)) return false;
|
||||
return this.players.filter(player => player && player.ready).length >= 2;
|
||||
};
|
||||
|
||||
RoomSchema.methods.startGame = function () {
|
||||
if (!Array.isArray(this.players) || this.players.length === 0) {
|
||||
console.warn(`[room:${this._id}] startGame: players array empty`);
|
||||
return;
|
||||
}
|
||||
this.started = true;
|
||||
this.nextMoveTime = Date.now() + 15000;
|
||||
this.nextMoveTime = Date.now() + MOVE_TIME;
|
||||
this.players.forEach(player => (player.ready = true));
|
||||
this.players[0].nowMoving = true;
|
||||
this.timeoutID = setTimeout(makeRandomMove, 15000, this._id.toString());
|
||||
timeoutManager.set(makeRandomMove, MOVE_TIME, this._id.toString());
|
||||
};
|
||||
|
||||
RoomSchema.methods.endGame = function (winner) {
|
||||
this.timeoutID = null;
|
||||
timeoutManager.clear(this._id.toString());
|
||||
this.rolledNumber = null;
|
||||
this.nextMoveTime = null;
|
||||
this.players.map(player => (player.nowMoving = false));
|
||||
if (Array.isArray(this.players)) this.players.forEach(player => (player.nowMoving = false));
|
||||
this.winner = winner;
|
||||
this.save();
|
||||
};
|
||||
|
||||
RoomSchema.methods.getWinner = function () {
|
||||
if (this.pawns.filter(pawn => pawn.color === 'red' && pawn.position === 73).length === 4) {
|
||||
if (!Array.isArray(this.pawns)) return null;
|
||||
if (this.pawns.filter(pawn => pawn && pawn.color === 'red' && pawn.position === 73).length === 4) {
|
||||
return 'red';
|
||||
}
|
||||
if (this.pawns.filter(pawn => pawn.color === 'blue' && pawn.position === 79).length === 4) {
|
||||
if (this.pawns.filter(pawn => pawn && pawn.color === 'blue' && pawn.position === 79).length === 4) {
|
||||
return 'blue';
|
||||
}
|
||||
if (this.pawns.filter(pawn => pawn.color === 'green' && pawn.position === 85).length === 4) {
|
||||
if (this.pawns.filter(pawn => pawn && pawn.color === 'green' && pawn.position === 85).length === 4) {
|
||||
return 'green';
|
||||
}
|
||||
if (this.pawns.filter(pawn => pawn.color === 'yellow' && pawn.position === 91).length === 4) {
|
||||
if (this.pawns.filter(pawn => pawn && pawn.color === 'yellow' && pawn.position === 91).length === 4) {
|
||||
return 'yellow';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
RoomSchema.methods.isFull = function () {
|
||||
if (!Array.isArray(this.players)) return false;
|
||||
if (this.players.length === 4) {
|
||||
this.full = true;
|
||||
}
|
||||
@@ -122,33 +176,52 @@ RoomSchema.methods.isFull = function () {
|
||||
};
|
||||
|
||||
RoomSchema.methods.getPlayer = function (playerId) {
|
||||
return this.players.find(player => player._id.toString() === playerId.toString());
|
||||
if (!Array.isArray(this.players)) return null;
|
||||
return this.players.find(player => player && player._id && player._id.toString() === playerId.toString()) || null;
|
||||
};
|
||||
|
||||
RoomSchema.methods.addPlayer = function (name, id) {
|
||||
if (this.full) return;
|
||||
if (!Array.isArray(this.players)) this.players = [];
|
||||
this.players.push({
|
||||
sessionID: id,
|
||||
name: name,
|
||||
ready: false,
|
||||
color: colors[this.players.length],
|
||||
color: COLORS[this.players.length],
|
||||
});
|
||||
};
|
||||
|
||||
RoomSchema.methods.getPawnIndex = function (pawnId) {
|
||||
return this.pawns.findIndex(pawn => pawn._id.toString() === pawnId.toString());
|
||||
if (!Array.isArray(this.pawns)) return -1;
|
||||
return this.pawns.findIndex(pawn => pawn && pawn._id && pawn._id.toString() === pawnId.toString());
|
||||
};
|
||||
|
||||
RoomSchema.methods.getPawn = function (pawnId) {
|
||||
return this.pawns.find(pawn => pawn._id.toString() === pawnId.toString());
|
||||
if (!Array.isArray(this.pawns)) return null;
|
||||
return this.pawns.find(pawn => pawn && pawn._id && pawn._id.toString() === pawnId.toString()) || null;
|
||||
};
|
||||
|
||||
RoomSchema.methods.getPlayerPawns = function (color) {
|
||||
return this.pawns.filter(pawn => pawn.color === color);
|
||||
if (!Array.isArray(this.pawns)) return [];
|
||||
return this.pawns.filter(pawn => pawn && pawn.color === color);
|
||||
};
|
||||
|
||||
RoomSchema.methods.getPawnsOnPosition = function (position) {
|
||||
if (!Array.isArray(this.pawns)) return [];
|
||||
return this.pawns.filter(pawn => pawn && pawn.position === position);
|
||||
};
|
||||
|
||||
RoomSchema.methods.isStacked = function (position) {
|
||||
return this.getPawnsOnPosition(position).length > 1;
|
||||
};
|
||||
|
||||
RoomSchema.methods.getOpponentPawnsOnPosition = function (position, color) {
|
||||
return this.getPawnsOnPosition(position).filter(pawn => pawn.color !== color);
|
||||
};
|
||||
|
||||
RoomSchema.methods.getCurrentlyMovingPlayer = function () {
|
||||
return this.players.find(player => player.nowMoving === true);
|
||||
if (!Array.isArray(this.players)) return null;
|
||||
return this.players.find(player => player && player.nowMoving === true) || null;
|
||||
};
|
||||
|
||||
const Room = mongoose.model('Room', RoomSchema);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
const timeoutManager = {
|
||||
timeouts: new Map(),
|
||||
add: function (roomId, timeoutId) {
|
||||
this.timeouts.set(roomId, timeoutId);
|
||||
},
|
||||
get: function (roomId) {
|
||||
return this.timeouts.get(roomId);
|
||||
},
|
||||
clear: function (roomId) {
|
||||
clearTimeout(this.timeouts.get(roomId));
|
||||
this.timeouts.delete(roomId);
|
||||
},
|
||||
set: function (timeoutFunction, time, roomId) {
|
||||
const timeoutId = setTimeout(timeoutFunction, time, roomId);
|
||||
this.add(roomId, timeoutId);
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = timeoutManager;
|
||||
Generated
+286
-6
@@ -10,9 +10,11 @@
|
||||
"connect-mongodb-session": "^3.1.1",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.17.1",
|
||||
"mongoose": "^5.12.0",
|
||||
"sinon": "^17.0.1",
|
||||
"socket.io": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -21,6 +23,45 @@
|
||||
"socket.io-client": "^4.7.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@sinonjs/commons": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz",
|
||||
"integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==",
|
||||
"dependencies": {
|
||||
"type-detect": "4.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sinonjs/fake-timers": {
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz",
|
||||
"integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==",
|
||||
"dependencies": {
|
||||
"@sinonjs/commons": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sinonjs/samsam": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz",
|
||||
"integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==",
|
||||
"dependencies": {
|
||||
"@sinonjs/commons": "^2.0.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"type-detect": "^4.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
|
||||
"integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==",
|
||||
"dependencies": {
|
||||
"type-detect": "4.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sinonjs/text-encoding": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz",
|
||||
"integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ=="
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
||||
@@ -668,6 +709,17 @@
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
|
||||
"integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/motdotla/dotenv?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
@@ -1081,7 +1133,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -1257,6 +1308,11 @@
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/just-extend": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz",
|
||||
"integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg=="
|
||||
},
|
||||
"node_modules/kareem": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.2.tgz",
|
||||
@@ -1293,6 +1349,11 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
|
||||
},
|
||||
"node_modules/lodash.get": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="
|
||||
},
|
||||
"node_modules/lodash.set": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
|
||||
@@ -1676,6 +1737,55 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/nise": {
|
||||
"version": "5.1.5",
|
||||
"resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz",
|
||||
"integrity": "sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==",
|
||||
"dependencies": {
|
||||
"@sinonjs/commons": "^2.0.0",
|
||||
"@sinonjs/fake-timers": "^10.0.2",
|
||||
"@sinonjs/text-encoding": "^0.7.1",
|
||||
"just-extend": "^4.0.2",
|
||||
"path-to-regexp": "^1.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nise/node_modules/@sinonjs/commons": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
|
||||
"integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==",
|
||||
"dependencies": {
|
||||
"type-detect": "4.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/nise/node_modules/@sinonjs/fake-timers": {
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz",
|
||||
"integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==",
|
||||
"dependencies": {
|
||||
"@sinonjs/commons": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nise/node_modules/@sinonjs/fake-timers/node_modules/@sinonjs/commons": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz",
|
||||
"integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==",
|
||||
"dependencies": {
|
||||
"type-detect": "4.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/nise/node_modules/isarray": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
|
||||
},
|
||||
"node_modules/nise/node_modules/path-to-regexp": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
|
||||
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
|
||||
"dependencies": {
|
||||
"isarray": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
@@ -2039,6 +2149,42 @@
|
||||
"resolved": "https://registry.npmjs.org/sift/-/sift-13.5.2.tgz",
|
||||
"integrity": "sha512-+gxdEOMA2J+AI+fVsCqeNn7Tgx3M9ZN9jdi95939l1IJ8cZsqS8sqpJyOkic2SJk+1+98Uwryt/gL6XDaV+UZA=="
|
||||
},
|
||||
"node_modules/sinon": {
|
||||
"version": "17.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz",
|
||||
"integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==",
|
||||
"dependencies": {
|
||||
"@sinonjs/commons": "^3.0.0",
|
||||
"@sinonjs/fake-timers": "^11.2.2",
|
||||
"@sinonjs/samsam": "^8.0.0",
|
||||
"diff": "^5.1.0",
|
||||
"nise": "^5.1.5",
|
||||
"supports-color": "^7.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/sinon"
|
||||
}
|
||||
},
|
||||
"node_modules/sinon/node_modules/diff": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
|
||||
"integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/sinon/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/sliced": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz",
|
||||
@@ -2306,7 +2452,6 @@
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
|
||||
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
@@ -2506,6 +2651,47 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@sinonjs/commons": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz",
|
||||
"integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==",
|
||||
"requires": {
|
||||
"type-detect": "4.0.8"
|
||||
}
|
||||
},
|
||||
"@sinonjs/fake-timers": {
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz",
|
||||
"integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==",
|
||||
"requires": {
|
||||
"@sinonjs/commons": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"@sinonjs/samsam": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz",
|
||||
"integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==",
|
||||
"requires": {
|
||||
"@sinonjs/commons": "^2.0.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"type-detect": "^4.0.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sinonjs/commons": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
|
||||
"integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==",
|
||||
"requires": {
|
||||
"type-detect": "4.0.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sinonjs/text-encoding": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz",
|
||||
"integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ=="
|
||||
},
|
||||
"@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
||||
@@ -2983,6 +3169,11 @@
|
||||
"integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==",
|
||||
"dev": true
|
||||
},
|
||||
"dotenv": {
|
||||
"version": "16.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
|
||||
"integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ=="
|
||||
},
|
||||
"ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
@@ -3291,8 +3482,7 @@
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
|
||||
},
|
||||
"he": {
|
||||
"version": "1.2.0",
|
||||
@@ -3412,6 +3602,11 @@
|
||||
"argparse": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"just-extend": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz",
|
||||
"integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg=="
|
||||
},
|
||||
"kareem": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.2.tgz",
|
||||
@@ -3439,6 +3634,11 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
|
||||
},
|
||||
"lodash.get": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="
|
||||
},
|
||||
"lodash.set": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
|
||||
@@ -3711,6 +3911,59 @@
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
|
||||
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
|
||||
},
|
||||
"nise": {
|
||||
"version": "5.1.5",
|
||||
"resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz",
|
||||
"integrity": "sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==",
|
||||
"requires": {
|
||||
"@sinonjs/commons": "^2.0.0",
|
||||
"@sinonjs/fake-timers": "^10.0.2",
|
||||
"@sinonjs/text-encoding": "^0.7.1",
|
||||
"just-extend": "^4.0.2",
|
||||
"path-to-regexp": "^1.7.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sinonjs/commons": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
|
||||
"integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==",
|
||||
"requires": {
|
||||
"type-detect": "4.0.8"
|
||||
}
|
||||
},
|
||||
"@sinonjs/fake-timers": {
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz",
|
||||
"integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==",
|
||||
"requires": {
|
||||
"@sinonjs/commons": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sinonjs/commons": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz",
|
||||
"integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==",
|
||||
"requires": {
|
||||
"type-detect": "4.0.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"isarray": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
|
||||
},
|
||||
"path-to-regexp": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
|
||||
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
|
||||
"requires": {
|
||||
"isarray": "0.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
@@ -3989,6 +4242,34 @@
|
||||
"resolved": "https://registry.npmjs.org/sift/-/sift-13.5.2.tgz",
|
||||
"integrity": "sha512-+gxdEOMA2J+AI+fVsCqeNn7Tgx3M9ZN9jdi95939l1IJ8cZsqS8sqpJyOkic2SJk+1+98Uwryt/gL6XDaV+UZA=="
|
||||
},
|
||||
"sinon": {
|
||||
"version": "17.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz",
|
||||
"integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==",
|
||||
"requires": {
|
||||
"@sinonjs/commons": "^3.0.0",
|
||||
"@sinonjs/fake-timers": "^11.2.2",
|
||||
"@sinonjs/samsam": "^8.0.0",
|
||||
"diff": "^5.1.0",
|
||||
"nise": "^5.1.5",
|
||||
"supports-color": "^7.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"diff": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
|
||||
"integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw=="
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sliced": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz",
|
||||
@@ -4187,8 +4468,7 @@
|
||||
"type-detect": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
|
||||
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
|
||||
"dev": true
|
||||
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="
|
||||
},
|
||||
"type-is": {
|
||||
"version": "1.6.18",
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
"connect-mongodb-session": "^3.1.1",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.17.1",
|
||||
"mongoose": "^5.12.0",
|
||||
"sinon": "^17.0.1",
|
||||
"socket.io": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+25
-11
@@ -1,38 +1,52 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const mongoose = require('mongoose');
|
||||
require('dotenv').config({ path: path.join(__dirname, '.env') });
|
||||
const { sessionMiddleware } = require('./config/session');
|
||||
|
||||
const PORT = 8080;
|
||||
const PORT = process.env.PORT || 5000;
|
||||
|
||||
console.log('🔍 Environment loaded from:', path.join(__dirname, '.env'));
|
||||
console.log('📍 Connection URI:', process.env.CONNECTION_URI);
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(cookieParser());
|
||||
app.use(
|
||||
express.urlencoded({
|
||||
extended: true,
|
||||
})
|
||||
);
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.json());
|
||||
app.set('trust proxy', 1);
|
||||
|
||||
/* ---------- CORS ---------- */
|
||||
app.use(
|
||||
cors({
|
||||
origin: 'http://localhost:3000',
|
||||
origin:
|
||||
process.env.NODE_ENV === 'production'
|
||||
? true // same origin (Docker / prod)
|
||||
: 'http://localhost:3000',
|
||||
credentials: true,
|
||||
})
|
||||
);
|
||||
|
||||
app.use(sessionMiddleware);
|
||||
|
||||
const server = app.listen(PORT);
|
||||
const server = app.listen(PORT, "0.0.0.0", () => {
|
||||
console.log(`Server listening on port ${PORT}`);
|
||||
});
|
||||
|
||||
/* ---------- Mongo + Socket ---------- */
|
||||
require('./config/database')(mongoose);
|
||||
require('./config/socket')(server);
|
||||
|
||||
/* ---------- Serve React build ---------- */
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
app.use(express.static('/app/build'));
|
||||
app.get('/', (req, res) => {
|
||||
res.sendFile('/app/build/index.html');
|
||||
const buildPath = path.resolve(__dirname, 'build');
|
||||
|
||||
app.use(express.static(buildPath));
|
||||
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(buildPath, 'index.html'));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -23,8 +23,21 @@ const createNewRoom = async data => {
|
||||
return room;
|
||||
};
|
||||
|
||||
const deleteRoom = async roomId => {
|
||||
return await Room.findByIdAndDelete(roomId).exec();
|
||||
};
|
||||
|
||||
Room.watch().on('change', async data => {
|
||||
sendToPlayersData(await getRoom(data.documentKey._id));
|
||||
// Ignore delete operations
|
||||
if (data.operationType === 'delete') {
|
||||
console.log('🗑️ Room deleted, skipping sendToPlayersData');
|
||||
return;
|
||||
}
|
||||
|
||||
const room = await getRoom(data.documentKey._id);
|
||||
if (room) {
|
||||
sendToPlayersData(room);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = { getRoom, getRooms, updateRoom, getJoinableRoom, createNewRoom };
|
||||
module.exports = { getRoom, getRooms, updateRoom, getJoinableRoom, createNewRoom, deleteRoom };
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
const mongodb = require('mongodb');
|
||||
require('dotenv').config({ path: require('path').join(__dirname, '.env') });
|
||||
|
||||
// Try without replicaSet
|
||||
const uri = 'mongodb://admin:adminpassword@192.168.0.197:27017/ludo?authSource=admin';
|
||||
console.log('Testing without replicaSet:', uri);
|
||||
|
||||
mongodb.MongoClient.connect(uri, {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
authSource: 'admin',
|
||||
serverSelectionTimeoutMS: 5000,
|
||||
}, (err, client) => {
|
||||
if (err) {
|
||||
console.error('❌ Connection error:', err.message);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('✅ Connected successfully!');
|
||||
client.close();
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
const mongodb = require('mongodb');
|
||||
require('dotenv').config({ path: require('path').join(__dirname, '.env') });
|
||||
|
||||
const uri = process.env.CONNECTION_URI;
|
||||
console.log('Testing with URI:', uri);
|
||||
|
||||
const uriUrl = new URL(uri);
|
||||
console.log('URL components:');
|
||||
console.log(' hostname:', uriUrl.hostname);
|
||||
console.log(' port:', uriUrl.port);
|
||||
console.log(' pathname:', uriUrl.pathname);
|
||||
console.log(' searchParams:', Object.fromEntries(uriUrl.searchParams));
|
||||
|
||||
// Try to connect directly
|
||||
mongodb.MongoClient.connect(uri, {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
replicaSet: 'rs0',
|
||||
authSource: 'admin'
|
||||
}, (err, client) => {
|
||||
if (err) {
|
||||
console.error('Connection error:', err.message);
|
||||
} else {
|
||||
console.log('✅ Connected successfully!');
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
process.env.DEBUG = 'mongodb:*';
|
||||
|
||||
const mongodb = require('mongodb');
|
||||
|
||||
const uri = 'mongodb://admin:adminpassword@192.168.0.197:27017/ludo?authSource=admin';
|
||||
console.log('Connecting to:', uri);
|
||||
|
||||
const client = new mongodb.MongoClient(uri, {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
authSource: 'admin',
|
||||
serverSelectionTimeoutMS: 3000,
|
||||
loggerLevel: 'debug',
|
||||
});
|
||||
|
||||
client.connect((err) => {
|
||||
if (err) {
|
||||
console.error('❌ Connection error:', err);
|
||||
} else {
|
||||
console.log('✅ Connected!');
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
const mongodb = require('mongodb');
|
||||
const net = require('net');
|
||||
const dns = require('dns').promises;
|
||||
|
||||
async function test() {
|
||||
// Test DNS resolution
|
||||
console.log('Testing DNS resolution...');
|
||||
try {
|
||||
const res = await dns.resolve4('192.168.0.197');
|
||||
console.log('DNS resolve4(192.168.0.197):', res);
|
||||
} catch (e) {
|
||||
console.log('DNS error:', e.message);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await dns.resolve4('mongo');
|
||||
console.log('DNS resolve4(mongo):', res);
|
||||
} catch (e) {
|
||||
console.log('DNS mongo error:', e.message);
|
||||
}
|
||||
|
||||
// Test direct socket connection
|
||||
console.log('\nTesting direct TCP connection...');
|
||||
const socket = net.createConnection(27017, '192.168.0.197');
|
||||
socket.on('connect', () => {
|
||||
console.log('✅ TCP connection successful to 192.168.0.197:27017');
|
||||
socket.destroy();
|
||||
});
|
||||
socket.on('error', (err) => {
|
||||
console.log('❌ TCP connection error:', err.message);
|
||||
});
|
||||
}
|
||||
|
||||
test();
|
||||
@@ -0,0 +1,50 @@
|
||||
const chai = require('chai');
|
||||
const sinon = require('sinon');
|
||||
const timeoutManager = require('../../models/timeoutManager');
|
||||
|
||||
const { expect } = chai;
|
||||
|
||||
describe('timeoutManager', () => {
|
||||
beforeEach(() => {
|
||||
timeoutManager.timeouts.clear();
|
||||
sinon.useFakeTimers({ shouldClearNativeTimers: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('should add a timeout to the map', () => {
|
||||
timeoutManager.add('room1', 1);
|
||||
expect(timeoutManager.timeouts.size).to.equal(1);
|
||||
});
|
||||
|
||||
it('should get a timeout from the map', () => {
|
||||
timeoutManager.add('room1', 1);
|
||||
const timeoutId = timeoutManager.get('room1');
|
||||
expect(timeoutId).to.equal(1);
|
||||
});
|
||||
|
||||
it('should clear a timeout from the map', () => {
|
||||
timeoutManager.add('room1', 1);
|
||||
timeoutManager.clear('room1');
|
||||
expect(timeoutManager.timeouts.size).to.equal(0);
|
||||
});
|
||||
|
||||
it('should set a new timeout', () => {
|
||||
const timeoutFunction = sinon.spy();
|
||||
timeoutManager.set(timeoutFunction, 100, 'room1');
|
||||
expect(timeoutManager.timeouts.size).to.equal(1);
|
||||
sinon.clock.tick(101);
|
||||
sinon.assert.calledOnce(timeoutFunction);
|
||||
});
|
||||
|
||||
it('should not call the timeout function if cleared', () => {
|
||||
const timeoutFunction = sinon.spy();
|
||||
timeoutManager.set(timeoutFunction, 100, 'room1');
|
||||
timeoutManager.clear('room1');
|
||||
sinon.clock.tick(101);
|
||||
sinon.assert.notCalled(timeoutFunction);
|
||||
expect(timeoutManager.timeouts.size).to.equal(0);
|
||||
});
|
||||
});
|
||||
@@ -1,2 +1,3 @@
|
||||
const colors = ["red", "blue", "green", "yellow"];
|
||||
module.exports = { colors };
|
||||
const COLORS = ['red', 'blue', 'green', 'yellow'];
|
||||
const MOVE_TIME = 15000;
|
||||
module.exports = { COLORS, MOVE_TIME };
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
mongo:
|
||||
image: mongo:7.0
|
||||
container_name: ludo-mongo
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- ludo-net
|
||||
volumes:
|
||||
- ludo-mongo-data:/data/db
|
||||
- /opt/ludo-secrets/mongo-keyfile:/etc/mongo-keyfile:ro
|
||||
# - ./mongo-keyfile:/etc/mongo-keyfile:ro
|
||||
# - ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
|
||||
command:
|
||||
- bash
|
||||
- -c
|
||||
- |
|
||||
cp /etc/mongo-keyfile /data/db/keyfile &&
|
||||
chown mongodb:mongodb /data/db/keyfile &&
|
||||
chmod 400 /data/db/keyfile &&
|
||||
exec mongod --replSet rs0 --keyFile /data/db/keyfile --bind_ip_all
|
||||
ports:
|
||||
- "27017:27017"
|
||||
|
||||
app:
|
||||
build: .
|
||||
container_name: mern-ludo-app
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- ludo-net
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "18081:${PORT}" # Host:Container mapping
|
||||
depends_on:
|
||||
- mongo
|
||||
|
||||
networks:
|
||||
ludo-net:
|
||||
|
||||
volumes:
|
||||
ludo-mongo-data:
|
||||
Generated
+1
@@ -26,6 +26,7 @@
|
||||
"web-vitals": "^3.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-transform-private-property-in-object": "^7.16.7",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"cypress": "^13.6.1"
|
||||
|
||||
+2
-5
@@ -23,9 +23,7 @@
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"heroku-postbuild": "cd backend && npm install && cd .. && npm install && npm run build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
"test": "react-scripts test"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
@@ -48,11 +46,10 @@
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"proxy": "http://localhost:5000",
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"cypress": "^13.6.1",
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.16.7"
|
||||
"@babel/plugin-transform-private-property-in-object": "^7.16.7"
|
||||
}
|
||||
}
|
||||
|
||||
+7
-1
@@ -13,7 +13,13 @@ function App() {
|
||||
const [playerSocket, setPlayerSocket] = useState();
|
||||
const [redirect, setRedirect] = useState();
|
||||
useEffect(() => {
|
||||
const socket = io('http://localhost:8080', { withCredentials: true });
|
||||
let socket;
|
||||
if(process.env.NODE_ENV !== 'production') {
|
||||
socket = io(`http://${window.location.hostname}:8080`, { withCredentials: true });
|
||||
} else {
|
||||
socket = io(`${window.location.protocol}//${window.location.host}`, { withCredentials: true });
|
||||
}
|
||||
|
||||
socket.on('player:data', data => {
|
||||
data = JSON.parse(data);
|
||||
setPlayerData(data);
|
||||
|
||||
@@ -85,7 +85,11 @@ const Gameboard = () => {
|
||||
<div className={styles.winnerContainer}>
|
||||
<img src={trophyImage} alt='winner' />
|
||||
<h1>
|
||||
1st: <span style={{ color: winner }}>{winner}</span>
|
||||
{context.color === winner ? (
|
||||
<>You Won!</>
|
||||
) : (
|
||||
<>1st: <span style={{ color: winner }}>{winner}</span></>
|
||||
)}
|
||||
</h1>
|
||||
<button onClick={() => socket.emit('player:exit')}>Play again</button>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import positionMapCoords from '../positions';
|
||||
import pawnImages from '../../../constants/pawnImages';
|
||||
import canPawnMove from './canPawnMove';
|
||||
import getPositionAfterMove from './getPositionAfterMove';
|
||||
import { pawnsAt } from './pawnHelpers';
|
||||
|
||||
const Map = ({ pawns, nowMoving, rolledNumber }) => {
|
||||
const player = useContext(PlayerDataContext);
|
||||
@@ -14,15 +15,26 @@ const Map = ({ pawns, nowMoving, rolledNumber }) => {
|
||||
|
||||
const [hintPawn, setHintPawn] = useState();
|
||||
|
||||
const paintPawn = (context, pawn) => {
|
||||
const { x, y } = positionMapCoords[pawn.position];
|
||||
const paintPawn = (context, pawn, x, y, pawnWidth = 28, onDraw) => {
|
||||
const pawnHeight = Math.round((pawnWidth * 30) / 35);
|
||||
const touchableArea = new Path2D();
|
||||
touchableArea.arc(x, y, 12, 0, 2 * Math.PI);
|
||||
const touchRadius = Math.max(6, Math.round(pawnWidth / 2));
|
||||
touchableArea.arc(x, y, touchRadius, 0, 2 * Math.PI);
|
||||
const image = new Image();
|
||||
image.src = pawnImages[pawn.color];
|
||||
image.onload = function () {
|
||||
context.drawImage(image, x - 17, y - 15, 35, 30);
|
||||
const imgSrc = pawnImages[pawn.color] || pawnImages['red'];
|
||||
image.src = imgSrc;
|
||||
|
||||
const drawAndCallback = () => {
|
||||
context.drawImage(image, x - Math.round(pawnWidth / 2), y - Math.round(pawnHeight / 2), pawnWidth, pawnHeight);
|
||||
if (typeof onDraw === 'function') onDraw();
|
||||
};
|
||||
|
||||
if (image.complete) {
|
||||
drawAndCallback();
|
||||
} else {
|
||||
image.onload = drawAndCallback;
|
||||
}
|
||||
|
||||
return touchableArea;
|
||||
};
|
||||
|
||||
@@ -33,7 +45,7 @@ const Map = ({ pawns, nowMoving, rolledNumber }) => {
|
||||
cursorX = event.clientX - rect.left,
|
||||
cursorY = event.clientY - rect.top;
|
||||
for (const pawn of pawns) {
|
||||
if (ctx.isPointInPath(pawn.touchableArea, cursorX, cursorY)) {
|
||||
if (pawn.touchableArea && ctx.isPointInPath(pawn.touchableArea, cursorX, cursorY)) {
|
||||
if (canPawnMove(pawn, rolledNumber)) socket.emit('game:move', pawn._id);
|
||||
}
|
||||
}
|
||||
@@ -50,6 +62,7 @@ const Map = ({ pawns, nowMoving, rolledNumber }) => {
|
||||
canvas.style.cursor = 'default';
|
||||
for (const pawn of pawns) {
|
||||
if (
|
||||
pawn.touchableArea &&
|
||||
ctx.isPointInPath(pawn.touchableArea, x, y) &&
|
||||
player.color === pawn.color &&
|
||||
canPawnMove(pawn, rolledNumber)
|
||||
@@ -74,11 +87,88 @@ const Map = ({ pawns, nowMoving, rolledNumber }) => {
|
||||
image.src = mapImage;
|
||||
image.onload = function () {
|
||||
ctx.drawImage(image, 0, 0);
|
||||
// draw pawns grouped by position with 2-row layout
|
||||
const drawn = new Set();
|
||||
pawns.forEach((pawn, index) => {
|
||||
pawns[index].touchableArea = paintPawn(ctx, pawn);
|
||||
if (drawn.has(pawn._id)) return;
|
||||
const group = pawnsAt(pawns, pawn.position);
|
||||
const total = group.length;
|
||||
const center = positionMapCoords[pawn.position];
|
||||
if (!center) return;
|
||||
|
||||
const slotOffsetX = 14; // horizontal spacing
|
||||
const slotOffsetY = 2; // minimal vertical spacing between rows
|
||||
|
||||
// helper to draw a single representative pawn for a color group
|
||||
// and show a small badge with the count when there are multiple pawns
|
||||
const drawColorGroup = (pawnGroup, x, y, baseWidth) => {
|
||||
if (!pawnGroup || pawnGroup.length === 0) return null;
|
||||
const rep = pawnGroup[0];
|
||||
// draw pawn and then badge after image renders so badge is on top
|
||||
const touch = paintPawn(ctx, rep, x, y, baseWidth, () => {
|
||||
if (pawnGroup.length > 1) {
|
||||
const badgeRadius = 8; // smaller badge
|
||||
const badgeX = x + Math.round(baseWidth / 2) - 4; // slightly closer to pawn edge
|
||||
const badgeY = y - Math.round(baseWidth / 2) + 4;
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.85)';
|
||||
ctx.arc(badgeX, badgeY, badgeRadius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.font = '10px Arial'; // smaller text
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(String(pawnGroup.length), badgeX, badgeY);
|
||||
}
|
||||
});
|
||||
const foundIndex = pawns.findIndex(pp => pp._id === rep._id);
|
||||
if (foundIndex !== -1) pawns[foundIndex].touchableArea = touch;
|
||||
return touch;
|
||||
};
|
||||
|
||||
// group pawns by color so same-color pawns stack vertically (tight)
|
||||
const colorGroups = {};
|
||||
group.forEach(g => {
|
||||
if (!g) return;
|
||||
if (!colorGroups[g.color]) colorGroups[g.color] = [];
|
||||
colorGroups[g.color].push(g);
|
||||
});
|
||||
const colors = Object.keys(colorGroups);
|
||||
const nColors = colors.length;
|
||||
const colorSpacing = slotOffsetX; // horizontal spacing between different colors
|
||||
|
||||
colors.forEach((color, ci) => {
|
||||
const pawnsOfColor = colorGroups[color];
|
||||
const x = center.x + (ci - (nColors - 1) / 2) * colorSpacing;
|
||||
const baseWidth = pawnsOfColor.length > 1 ? 20 : total === 1 ? 28 : 24;
|
||||
drawColorGroup(pawnsOfColor, x, center.y, baseWidth);
|
||||
pawnsOfColor.forEach(p => drawn.add(p._id));
|
||||
});
|
||||
|
||||
// badge for >6 pawns
|
||||
if (total > 6) {
|
||||
const badgeX = center.x + slotOffsetX + 12;
|
||||
const badgeY = center.y - slotOffsetY - 12;
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.75)';
|
||||
ctx.arc(badgeX, badgeY, 12, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.font = '11px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(String(total), badgeX, badgeY);
|
||||
}
|
||||
});
|
||||
if (hintPawn) {
|
||||
paintPawn(ctx, hintPawn);
|
||||
// draw hint as a grey circle at target position
|
||||
const pos = positionMapCoords[hintPawn.position];
|
||||
if (pos) {
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = 'rgba(128,128,128,0.6)';
|
||||
ctx.arc(pos.x, pos.y, 14, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const canPawnMove = (pawn, rolledNumber) => {
|
||||
// If is in base
|
||||
if ((rolledNumber === 1 || rolledNumber === 6) && pawn.position === pawn.basePos) {
|
||||
if ((rolledNumber === 6) && pawn.position === pawn.basePos) {
|
||||
return true;
|
||||
// Other situations: pawn is on map or pawn is in end positions
|
||||
} else if (pawn.position !== pawn.basePos) {
|
||||
|
||||
@@ -42,7 +42,7 @@ const getPositionAfterMove = (pawn, rolledNumber) => {
|
||||
return position;
|
||||
}
|
||||
case 'yellow':
|
||||
if (pawn.position + rolledNumber <= 85) {
|
||||
if (pawn.position + rolledNumber <= 91) {
|
||||
if (position >= 12 && position <= 15) {
|
||||
return 29;
|
||||
} else if (position <= 67 && position + rolledNumber > 67) {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export const pawnsAt = (pawns, position) => {
|
||||
return Array.isArray(pawns) ? pawns.filter(p => p && p.position === position) : [];
|
||||
};
|
||||
|
||||
export const countPawnsAt = (pawns, position) => pawnsAt(pawns, position).length;
|
||||
|
||||
export const opponentsAt = (pawns, position, myColor) =>
|
||||
pawnsAt(pawns, position).filter(p => p.color !== myColor);
|
||||
|
||||
export default { pawnsAt, countPawnsAt, opponentsAt };
|
||||
@@ -20,12 +20,23 @@ const JoinServer = () => {
|
||||
useEffect(() => {
|
||||
socket.emit('room:rooms');
|
||||
socket.on('room:rooms', () => {
|
||||
console.log('✅ Room list updated');
|
||||
setIsLoading(false);
|
||||
});
|
||||
socket.on('error:deleteRoom', (error) => {
|
||||
console.error('❌ Delete error:', error);
|
||||
alert('Failed to delete server: ' + error);
|
||||
getRooms();
|
||||
});
|
||||
return () => {
|
||||
socket.off('error:deleteRoom');
|
||||
socket.off('room:rooms');
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
const getRooms = () => {
|
||||
setRooms([]);
|
||||
setIsLoading(true);
|
||||
socket.emit('room:rooms');
|
||||
};
|
||||
|
||||
@@ -34,6 +45,15 @@ const JoinServer = () => {
|
||||
setJoining(true);
|
||||
};
|
||||
|
||||
const handleDeleteClick = roomId => {
|
||||
if (window.confirm('Are you sure you want to delete this server?')) {
|
||||
console.log('🗑️ Deleting room:', roomId);
|
||||
setIsLoading(true);
|
||||
setRooms([]); // Clear the list immediately
|
||||
socket.emit('room:delete', roomId);
|
||||
}
|
||||
};
|
||||
|
||||
const ServersTableWithLoading = withLoading(ServersTable);
|
||||
|
||||
return (
|
||||
@@ -51,6 +71,7 @@ const JoinServer = () => {
|
||||
isLoading={isLoading}
|
||||
rooms={rooms}
|
||||
handleJoinClick={handleJoinClick}
|
||||
handleDeleteClick={handleDeleteClick}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.serversTableContainer {
|
||||
display: flex;
|
||||
height: 500px;
|
||||
overflow: scroll;
|
||||
height: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
.refresh {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import lock from '../../../../images/login-page/lock.png';
|
||||
import styles from './ServersTable.module.css';
|
||||
|
||||
const ServerListTable = ({ rooms, handleJoinClick }) => {
|
||||
const ServerListTable = ({ rooms, handleJoinClick, handleDeleteClick }) => {
|
||||
const safeRooms = Array.isArray(rooms) ? rooms : [];
|
||||
return (
|
||||
<table className={styles.rooms}>
|
||||
<thead>
|
||||
@@ -14,15 +15,19 @@ const ServerListTable = ({ rooms, handleJoinClick }) => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rooms.map((room, index) => {
|
||||
{safeRooms.map((room, index) => {
|
||||
if (!room) return null;
|
||||
return room.started ? null : (
|
||||
<tr key={index}>
|
||||
<td>{room.private ? <img src={lock} alt='private' /> : null}</td>
|
||||
<td className={styles.roomName}>{room.name}</td>
|
||||
<td>{`${room.players.length}/4`}</td>
|
||||
<td>{`${(room.players && room.players.length) || 0}/4`}</td>
|
||||
<td>{room.isStarted ? 'started' : 'waiting'}</td>
|
||||
<td className={styles.lastColumn}>
|
||||
<button onClick={() => handleJoinClick(room)}>Join</button>
|
||||
<button onClick={() => handleDeleteClick(room._id)} className={styles.deleteBtn}>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
.roomName {
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: left !important;
|
||||
}
|
||||
.rooms > thead > tr :nth-child(2) {
|
||||
@@ -30,11 +32,22 @@
|
||||
.rooms {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.lastColumn {
|
||||
width: 70px;
|
||||
width: 130px;
|
||||
}
|
||||
.firstColumn {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.deleteBtn {
|
||||
margin-left: 5px;
|
||||
background-color: #ff4444;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 60 KiB |
+1
-1
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import App from './App.js';
|
||||
|
||||
const container = document.getElementById('root');
|
||||
const root = ReactDOM.createRoot(container);
|
||||
|
||||
@@ -0,0 +1,562 @@
|
||||
You’ve identified the Mongo volume that’s causing the auth issue.
|
||||
|
||||
What this means
|
||||
local mern-ludo_mongo_data
|
||||
|
||||
|
||||
This volume still exists, so Mongo is:
|
||||
|
||||
reusing old, broken initialization
|
||||
|
||||
not recreating the admin user
|
||||
|
||||
continuing to fail authentication
|
||||
|
||||
We now need to delete this volume so Mongo can initialize cleanly.
|
||||
|
||||
✅ Do this EXACTLY (safe + correct)
|
||||
🔴 Step 1: Stop containers
|
||||
docker compose down
|
||||
|
||||
🔴 Step 2: Remove the Mongo volume
|
||||
docker volume rm mern-ludo_mongo_data
|
||||
|
||||
|
||||
If Docker complains it’s in use, run:
|
||||
|
||||
docker compose down -v
|
||||
|
||||
|
||||
(that removes volumes automatically)
|
||||
|
||||
✅ Step 3: Confirm it’s gone
|
||||
docker volume ls | grep ludo
|
||||
|
||||
|
||||
👉 This should return nothing.
|
||||
|
||||
✅ Step 4: Start ONLY Mongo first (important)
|
||||
docker compose up -d mongo
|
||||
|
||||
|
||||
Wait 10–15 seconds.
|
||||
|
||||
Check logs:
|
||||
|
||||
docker logs ludo-mongo --tail=30
|
||||
|
||||
|
||||
You should NOT see:
|
||||
|
||||
bad file
|
||||
|
||||
Unable to acquire security key
|
||||
|
||||
crash/restart loops
|
||||
|
||||
✅ Step 5: Test authentication (this MUST work)
|
||||
docker exec -it ludo-mongo mongosh -u admin -p adminpassword --authenticationDatabase admin
|
||||
|
||||
|
||||
If you get a > prompt → 🎉 auth is fixed
|
||||
|
||||
✅ Step 6: Initialize replica set (ONCE)
|
||||
|
||||
Inside mongosh:
|
||||
|
||||
rs.initiate({
|
||||
_id: "rs0",
|
||||
members: [{ _id: 0, host: "mongo:27017" }]
|
||||
})
|
||||
|
||||
|
||||
Then:
|
||||
|
||||
rs.status().members[0].stateStr
|
||||
|
||||
|
||||
Must be:
|
||||
|
||||
"PRIMARY"
|
||||
|
||||
✅ Step 7: Start the app
|
||||
docker compose up -d app
|
||||
|
||||
|
||||
(or just)
|
||||
|
||||
docker compose up -d
|
||||
|
||||
|
||||
Then:
|
||||
|
||||
docker restart mern-ludo-app
|
||||
|
||||
🎯 Expected final result
|
||||
Mongo
|
||||
|
||||
Auth works
|
||||
|
||||
Replica set PRIMARY
|
||||
|
||||
No errors in logs
|
||||
|
||||
Backend
|
||||
MongoDB connected
|
||||
Server listening on port 18081
|
||||
|
||||
Browser
|
||||
|
||||
App loads
|
||||
|
||||
No crashes
|
||||
|
||||
No noscript / auth errors
|
||||
|
||||
🧾 One-line status
|
||||
|
||||
The Mongo volume still existed; deleting it allows Mongo to create the admin user correctly.
|
||||
|
||||
Once Step 5 succeeds, you are fully unblocked.
|
||||
Run Step 2 now and tell me when mongosh login works 👍
|
||||
|
||||
|
||||
You’re very close, and the remaining issue is not random.
|
||||
|
||||
What the two messages really mean (important)
|
||||
1️⃣ These messages are NOT the main problem ❌
|
||||
Collection [local.oplog.rs] not found
|
||||
ReadConcernMajorityNotAvailableYet
|
||||
|
||||
|
||||
These appear when:
|
||||
|
||||
replica set is not initiated yet
|
||||
|
||||
PRIMARY is not elected yet
|
||||
|
||||
They are expected noise before rs.initiate() succeeds.
|
||||
|
||||
👉 We’ll fix them automatically once the replica set is initialized.
|
||||
|
||||
2️⃣ THIS is the real blocker 🔴
|
||||
MongoServerError: Authentication failed
|
||||
|
||||
|
||||
This means 100%:
|
||||
|
||||
🔐 MongoDB is running with authentication enabled,
|
||||
❌ but the admin user does not exist.
|
||||
|
||||
This is why:
|
||||
|
||||
backend can’t connect
|
||||
|
||||
mongosh -u admin fails
|
||||
|
||||
oplog can’t be created
|
||||
|
||||
replica set can’t complete
|
||||
|
||||
Why the admin user does NOT exist (root cause)
|
||||
|
||||
Mongo creates the root user only once, on first successful startup, when:
|
||||
|
||||
/data/db is empty
|
||||
|
||||
Mongo reaches a stable running state
|
||||
|
||||
Earlier, Mongo:
|
||||
|
||||
crashed due to keyFile issues
|
||||
|
||||
restarted several times
|
||||
|
||||
skipped user creation
|
||||
|
||||
but auth stayed enabled
|
||||
|
||||
Result:
|
||||
|
||||
Mongo requires auth, but no users exist → permanent auth failure
|
||||
|
||||
This is a classic Mongo + replica set + keyFile edge case.
|
||||
|
||||
✅ The ONLY correct fix (clean & guaranteed)
|
||||
|
||||
We must let Mongo start once without auth, create the user manually, then re-enable auth.
|
||||
|
||||
This is the official MongoDB recovery method.
|
||||
|
||||
🔥 DO THIS STEP-BY-STEP (no skipping)
|
||||
STEP 1️⃣ Stop everything
|
||||
docker compose down
|
||||
|
||||
STEP 2️⃣ Temporarily DISABLE auth (Mongo only)
|
||||
|
||||
Edit docker-compose.yml mongo service:
|
||||
|
||||
🔴 TEMPORARY CHANGE
|
||||
|
||||
Comment out these lines for now:
|
||||
|
||||
# environment:
|
||||
# MONGO_INITDB_ROOT_USERNAME: admin
|
||||
# MONGO_INITDB_ROOT_PASSWORD: adminpassword
|
||||
|
||||
|
||||
And change the command to NO keyFile, NO auth:
|
||||
|
||||
command:
|
||||
- mongod
|
||||
- --replSet
|
||||
- rs0
|
||||
- --bind_ip_all
|
||||
|
||||
|
||||
(Leave volumes as-is.)
|
||||
|
||||
STEP 3️⃣ Start Mongo only
|
||||
docker compose up -d mongo
|
||||
|
||||
|
||||
Wait ~10 seconds.
|
||||
|
||||
STEP 4️⃣ Connect WITHOUT auth (this must work)
|
||||
docker exec -it ludo-mongo mongosh
|
||||
|
||||
|
||||
If you get a prompt → good.
|
||||
|
||||
STEP 5️⃣ Create admin user MANUALLY
|
||||
|
||||
Inside mongosh:
|
||||
|
||||
use admin
|
||||
db.createUser({
|
||||
user: "admin",
|
||||
pwd: "adminpassword",
|
||||
roles: [ { role: "root", db: "admin" } ]
|
||||
})
|
||||
|
||||
|
||||
You must see:
|
||||
|
||||
{ ok: 1 }
|
||||
|
||||
STEP 6️⃣ Initialize replica set (NOW it will work)
|
||||
rs.initiate({
|
||||
_id: "rs0",
|
||||
members: [{ _id: 0, host: "mongo:27017" }]
|
||||
})
|
||||
|
||||
|
||||
Wait ~5–10 seconds, then:
|
||||
|
||||
rs.status().members[0].stateStr
|
||||
|
||||
|
||||
Must be:
|
||||
|
||||
"PRIMARY"
|
||||
|
||||
|
||||
Exit mongosh:
|
||||
|
||||
exit
|
||||
|
||||
STEP 7️⃣ Re-enable auth + keyFile (FINAL config)
|
||||
|
||||
Restore your correct mongo config:
|
||||
|
||||
environment:
|
||||
MONGO_INITDB_ROOT_USERNAME: admin
|
||||
MONGO_INITDB_ROOT_PASSWORD: adminpassword
|
||||
|
||||
command:
|
||||
- bash
|
||||
- -c
|
||||
- |
|
||||
cp /tmp/mongo-keyfile /data/db/keyfile &&
|
||||
chown mongodb:mongodb /data/db/keyfile &&
|
||||
chmod 400 /data/db/keyfile &&
|
||||
exec mongod --replSet rs0 --keyFile /data/db/keyfile --bind_ip_all
|
||||
|
||||
STEP 8️⃣ Restart clean
|
||||
docker compose down
|
||||
docker compose up -d
|
||||
|
||||
STEP 9️⃣ Test auth (this WILL work now)
|
||||
docker exec -it ludo-mongo mongosh -u admin -p adminpassword --authenticationDatabase admin
|
||||
|
||||
STEP 🔟 Start / restart backend
|
||||
docker restart mern-ludo-app
|
||||
|
||||
✅ What you should see now
|
||||
Mongo
|
||||
|
||||
Auth works
|
||||
|
||||
PRIMARY elected
|
||||
|
||||
local.oplog.rs exists
|
||||
|
||||
No auth errors
|
||||
|
||||
Backend
|
||||
MongoDB connected
|
||||
Server listening on port 18081
|
||||
|
||||
App
|
||||
|
||||
Loads normally
|
||||
|
||||
No crashes
|
||||
|
||||
Change Streams work
|
||||
|
||||
🧠 One-line final diagnosis
|
||||
|
||||
MongoDB authentication was enabled before any user existed; manual user creation was required to recover.
|
||||
|
||||
This is the last MongoDB trap — once done, you will not hit this again.
|
||||
|
||||
MongoServerError[NotWritablePrimary]: not primary
|
||||
|
||||
Perfect — that error is expected at this exact moment, and it tells us precisely what to do next 👍
|
||||
You didn’t break anything.
|
||||
|
||||
What this error means (very clear)
|
||||
MongoServerError[NotWritablePrimary]: not primary
|
||||
|
||||
|
||||
This means:
|
||||
|
||||
MongoDB is running as a replica set member,
|
||||
but no PRIMARY has been elected yet.
|
||||
|
||||
Until a node becomes PRIMARY:
|
||||
|
||||
❌ you cannot create users
|
||||
|
||||
❌ you cannot write data
|
||||
|
||||
❌ db.createUser() will fail
|
||||
|
||||
So this is not an auth issue anymore.
|
||||
|
||||
✅ Correct order (important)
|
||||
|
||||
When running Mongo as a replica set, the order must be:
|
||||
|
||||
Start Mongo
|
||||
|
||||
Initiate replica set
|
||||
|
||||
Wait until PRIMARY
|
||||
|
||||
Create users
|
||||
|
||||
Enable auth (already planned)
|
||||
|
||||
Right now, you’re at step 2½.
|
||||
|
||||
✅ DO THIS NOW (inside mongosh)
|
||||
|
||||
You are already connected with:
|
||||
|
||||
docker exec -it ludo-mongo mongosh
|
||||
|
||||
|
||||
At the > prompt, run THIS FIRST 👇
|
||||
|
||||
🔥 Step 1: Initiate replica set
|
||||
rs.initiate({
|
||||
_id: "rs0",
|
||||
members: [
|
||||
{ _id: 0, host: "mongo:27017" }
|
||||
]
|
||||
})
|
||||
|
||||
Expected output
|
||||
{ ok: 1 }
|
||||
|
||||
🔥 Step 2: Wait a few seconds, then verify PRIMARY
|
||||
rs.status().members[0].stateStr
|
||||
|
||||
|
||||
You must see:
|
||||
|
||||
"PRIMARY"
|
||||
|
||||
|
||||
If it still says STARTUP or SECONDARY, wait 5 seconds and run again.
|
||||
|
||||
✅ ONLY AFTER PRIMARY → create admin user
|
||||
|
||||
Now run:
|
||||
|
||||
use admin
|
||||
|
||||
db.createUser({
|
||||
user: "admin",
|
||||
pwd: "adminpassword",
|
||||
roles: [ { role: "root", db: "admin" } ]
|
||||
})
|
||||
|
||||
Expected:
|
||||
{ ok: 1 }
|
||||
|
||||
🧠 Why this happened (important lesson)
|
||||
|
||||
You started Mongo with:
|
||||
|
||||
mongod --replSet rs0
|
||||
|
||||
|
||||
That automatically puts Mongo into replica set mode, but:
|
||||
|
||||
replica set is inactive
|
||||
|
||||
no PRIMARY exists yet
|
||||
|
||||
Mongo rejects all writes until a PRIMARY is elected.
|
||||
|
||||
This is correct MongoDB behavior.
|
||||
|
||||
✅ After user creation (next steps preview)
|
||||
|
||||
Once { ok: 1 } appears:
|
||||
|
||||
Exit mongosh:
|
||||
|
||||
exit
|
||||
|
||||
|
||||
Re-enable auth + keyFile in docker-compose (as discussed)
|
||||
|
||||
Restart containers
|
||||
|
||||
Verify:
|
||||
|
||||
docker exec -it ludo-mongo mongosh -u admin -p adminpassword --authenticationDatabase admin
|
||||
|
||||
🎯 One-line status update
|
||||
|
||||
MongoDB is in replica-set mode but not PRIMARY yet — initialize the replica set first.
|
||||
|
||||
👉 Action for you now
|
||||
|
||||
Run rs.initiate(...), wait for PRIMARY, then retry db.createUser.
|
||||
|
||||
Tell me what rs.status().members[0].stateStr prints, and we’ll finish this cleanly 💪
|
||||
|
||||
|
||||
```yaml
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
mongo:
|
||||
image: mongo:7.0
|
||||
container_name: ludo-mongo
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- ludo-net
|
||||
environment:
|
||||
MONGO_INITDB_ROOT_USERNAME: admin
|
||||
MONGO_INITDB_ROOT_PASSWORD: adminpassword
|
||||
volumes:
|
||||
- ludo-mongo-data:/data/db
|
||||
- ./mongo-keyfile:/tmp/mongo-keyfile:ro
|
||||
command:
|
||||
- bash
|
||||
- -c
|
||||
- |
|
||||
cp /tmp/mongo-keyfile /data/db/keyfile &&
|
||||
chown mongodb:mongodb /data/db/keyfile &&
|
||||
chmod 400 /data/db/keyfile &&
|
||||
exec mongod --replSet rs0 --keyFile /data/db/keyfile --bind_ip_all
|
||||
ports:
|
||||
- "27017:27017"
|
||||
|
||||
app:
|
||||
build: .
|
||||
container_name: mern-ludo-app
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- ludo-net
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "18081:8080"
|
||||
depends_on:
|
||||
- mongo
|
||||
|
||||
networks:
|
||||
ludo-net:
|
||||
|
||||
volumes:
|
||||
ludo-mongo-data:
|
||||
```
|
||||
|
||||
Step 2️⃣ Recreate keyfile cleanly (one last time)
|
||||
cd ~/mern-ludo
|
||||
rm -f mongo-keyfile
|
||||
openssl rand -base64 756 > mongo-keyfile
|
||||
chmod 400 mongo-keyfile
|
||||
|
||||
|
||||
Verify:
|
||||
|
||||
wc -c mongo-keyfile
|
||||
|
||||
Step 3️⃣ Full reset (MANDATORY)
|
||||
docker compose down -v
|
||||
docker compose up -d --build
|
||||
|
||||
|
||||
Wait ~10 seconds.
|
||||
|
||||
Step 4️⃣ Confirm Mongo is RUNNING (not restarting)
|
||||
docker ps
|
||||
|
||||
|
||||
You should NOT see Restarting.
|
||||
|
||||
Step 5️⃣ Now exec WILL work
|
||||
docker exec -it ludo-mongo mongosh -u admin -p adminpassword --authenticationDatabase admin
|
||||
|
||||
|
||||
If you get a shell → 🎉 Mongo is fixed
|
||||
|
||||
Step 6️⃣ Initialize replica set (ONCE)
|
||||
rs.initiate({
|
||||
_id: "rs0",
|
||||
members: [{ _id: 0, host: "mongo:27017" }]
|
||||
})
|
||||
|
||||
|
||||
Check:
|
||||
|
||||
rs.status().members[0].stateStr
|
||||
// PRIMARY
|
||||
|
||||
Step 7️⃣ Restart app
|
||||
docker restart mern-ludo-app
|
||||
|
||||
|
||||
Your Change Stream error will now be gone permanently.
|
||||
|
||||
🧠 Why ALL previous attempts failed
|
||||
Attempt Why it failed
|
||||
Bind mount keyfile Ownership mismatch
|
||||
chmod 400 only Not enough
|
||||
DietPi ARM Stricter FS behavior
|
||||
Mongo 7 Enforces keyfile rules hard
|
||||
|
||||
This copy + chown pattern solves all of them.
|
||||
|
||||
✅ One-line final diagnosis
|
||||
|
||||
MongoDB rejected the keyfile because bind-mounted files keep host ownership, and Mongo runs as a non-root user.
|
||||
|
||||
Reference in New Issue
Block a user