Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3124239374 | |||
| b31436a40a | |||
| c066fdc31c | |||
| f9587c5cfa | |||
| 44a0ffe743 | |||
| 0f0e777e6e | |||
| c96629b704 | |||
| 41612f5d39 | |||
| e1573ba9f0 | |||
| b9c4c817f8 | |||
| 384363c8dd | |||
| def9918047 | |||
| cafa0cccc4 | |||
| 8b907d5a86 | |||
| 13f93fb332 | |||
| 801750da96 | |||
| 82623b0078 | |||
| d3e2d9bdf9 | |||
| 9c1e8e03bb | |||
| 55c241fd61 | |||
| 592bb38c0d | |||
| 72bc9ea628 | |||
| 557cf23f71 | |||
| b51b711b10 |
@@ -1,10 +0,0 @@
|
|||||||
POSTGRES_PASSWORD=coucou
|
|
||||||
JWT_SECRET=superlongsecretkeyatleast32characterspleasenevercommitthis
|
|
||||||
POSTGRES_DB=database
|
|
||||||
POSTGRES_HOST=database
|
|
||||||
POSTGRES_USER=user
|
|
||||||
GITHUB_CLIENT_ID=Ov23li6ovg3fzec5IO5D
|
|
||||||
GITHUB_CLIENT_SECRET=0345e959e8f0e9f784061c5c90ee227ddb2ef9ab
|
|
||||||
GITHUB_CALLBACK_URL=http://localhost:8080/api/auth/github/callback
|
|
||||||
|
|
||||||
pogpog
|
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
*This project has been created as part of the 42 curriculum by agallon, gprunet, yantoine and tfauve-p*
|
||||||
|
|
||||||
|
**DESCRIPTION**
|
||||||
|
|
||||||
|
For starters, ft_transcendence is a wonderful project based on building a web-application running from Docker containers where the goal is, for the first time to do whatever we want, yet still we need to follow multiples criteria based on a number of points to grind to set the project as finished.
|
||||||
|
|
||||||
|
For such a project our group thought about the "CATETRIBBL.IO". We chose to make a web application featuring multiples games such as Tetris one of the very first game ever developed and Skkribl.io the amazing drawing game !
|
||||||
|
|
||||||
|
But beware ! A mysterious noble cat named Wiskas The Third is gone ... It is said that he's been lurking around trapping 42's students into infinite conversation known as "tunnel". If you see him please report to us as soon as possible !
|
||||||
|
|
||||||
|
**INSTRUCTIONS**
|
||||||
|
|
||||||
|
Like every 42 project you will need to git clone it into a valid repository, then add our functional .env file at the root of the repository. After all that, make use of the "make" command and watch our fabulous containers building themselves ! Look for https://localhost:8443/ once it's built, remember that you need to login in order to play on our web app !
|
||||||
|
|
||||||
|
Outside of 42 environment you will obviously need Docker and Make installed.
|
||||||
|
|
||||||
|
**RESOURCES**
|
||||||
|
|
||||||
|
- https://www.geeksforgeeks.org
|
||||||
|
- https://developer.mozilla.org/fr/docs/Web/JavaScript
|
||||||
|
- https://www.w3schools.com/js/
|
||||||
|
- https://www.tigerdata.com/learn/postgres-cheat-sheet
|
||||||
|
- https://www.programiz.com/css/button-styling
|
||||||
|
- https://developer.mozilla.org/fr/docs/Web/CSS
|
||||||
|
- https://chatgpt.com/
|
||||||
|
- https://www.gimp.org/tutorials/
|
||||||
|
|
||||||
|
AI was mostly used to ask questions and deepen understanding, it was also used to generate multiple samples of what we could do front-end wise.
|
||||||
|
|
||||||
|
**FEATURES:**
|
||||||
|
|
||||||
|
- Login
|
||||||
|
- Avatar
|
||||||
|
- Global Chat
|
||||||
|
- Skribbl.io + Spectator mode
|
||||||
|
- Tetris + Duels
|
||||||
|
- Wiskas the Third
|
||||||
|
|
||||||
|
Use of the framework Express for the back-end because its compatible with jsonwebtoken(JWT) and contains solid and well tested features.
|
||||||
|
|
||||||
|
**DEPENDENCIES**
|
||||||
|
|
||||||
|
- "express": "^4.18.2",
|
||||||
|
- "pg": "^8.11.3",
|
||||||
|
- "bcrypt": "^5.1.0",
|
||||||
|
- "jsonwebtoken": "^9.0.2",
|
||||||
|
- "dotenv": "^17.2.3",
|
||||||
|
- "socket.io": "^4.6.1",
|
||||||
|
- "cors": "^2.8.5",
|
||||||
|
- "passport": "0.7.0",
|
||||||
|
- "passport-github2": "0.1.12",
|
||||||
|
- "express-session": "1.18.0",
|
||||||
|
- "multer": "^1.4.5-lts.1",
|
||||||
|
- "file-type": "^19.0.0"
|
||||||
|
|
||||||
|
**TEAM INFORMATION**
|
||||||
|
|
||||||
|
Tfauve-p : The project manager, is in lead of organizing all the meeting with the team which changed over time, including then some recruitment. There was some adjustments to make over our vision of the project while coding it on GitHub.
|
||||||
|
|
||||||
|
Yantoine : The project owner, is in lead of both games, Tetris and Skkribl.io, made core decisions on features about these and got the work completed. His communication skills were very important due to the front-end / back-end relationship needed in order to achieve this glorious project.
|
||||||
|
|
||||||
|
Gprunet : The technical lead, is in charge of the back-end, made some strong decisions on the architectures of the project in terms of technology used. Created the entire database and most of the foundation of this project such as the builder files.
|
||||||
|
|
||||||
|
Agallon : Developer in the front-end action, he joined the team after the project was done but managed to innovate and brought to life the marvelous Wiskas the Third. Furthermore he also cared about the integrity of the web application and greatly improved the user experience through logical decisions.
|
||||||
|
|
||||||
|
**PROJECT MANAGEMENT**
|
||||||
|
|
||||||
|
The task's sharing was based on our own advance of the 42 cursus, meaning that Gprunet and Yantoine started coding the app sooner than Tfauve-p and Agallon.
|
||||||
|
|
||||||
|
Gprunet: Back-end + spectator mode
|
||||||
|
Yantoine: Github auth, games and Front/Back sockets
|
||||||
|
Tfauve-p: Front-end Designer
|
||||||
|
Agallon: Adjustements on Front-end and some new features.
|
||||||
|
|
||||||
|
|
||||||
|
**TECHNICAL STACK**
|
||||||
|
|
||||||
|
Front-end: JavaScript, HTML, CSS, NGINX
|
||||||
|
Back-end: JavaScript, Express, JWT, multer, etc...
|
||||||
|
Database: PostgreSQL because it uses a permissibe open-source licence and is feature-rich and powerful for the scale of our project
|
||||||
|
|
||||||
|
Since python doesn't have many front-end framework we opted to use JavaScript for both front and back.
|
||||||
|
After learning about JWT and learning that Express had a great synergy with it the choice was natural.
|
||||||
|
|
||||||
|
|
||||||
|
**DATABASES SCHEMA**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
**FEATURES LIST**
|
||||||
|
|
||||||
|
- 2 Games
|
||||||
|
- One talking Cat
|
||||||
|
- Friends chat
|
||||||
|
-
|
||||||
|
|
||||||
|
**MODULES**
|
||||||
|
|
||||||
|
Total : 23pts ( 14pts for 100% 19pts for 125% )
|
||||||
|
|
||||||
|
- WEB
|
||||||
|
|
||||||
|
Minor : Use a back end framework
|
||||||
|
|
||||||
|
Major : Implement real-time features
|
||||||
|
|
||||||
|
Major : Allow users to interact with others
|
||||||
|
|
||||||
|
Major : A public API to interact with the database
|
||||||
|
|
||||||
|
Minor : A complete notification system for all creation, update and deletion account
|
||||||
|
|
||||||
|
- ACCESSIBILITY
|
||||||
|
|
||||||
|
Minor : Support for additional browsers
|
||||||
|
|
||||||
|
- USER MANAGEMENT
|
||||||
|
|
||||||
|
Major : Standard user management and authentication
|
||||||
|
|
||||||
|
Minor : Game statistics and match history ???
|
||||||
|
|
||||||
|
Minor : Implement remote authentication
|
||||||
|
|
||||||
|
- GAMING AND USER EXPERIENCE
|
||||||
|
|
||||||
|
Major : Implement a complete web-based game where users can play against each other
|
||||||
|
|
||||||
|
Major : Remote players, Enable two players on separate computers to play the same game
|
||||||
|
|
||||||
|
Major : Multiplayer game
|
||||||
|
|
||||||
|
Major : Add another game with user history and matchmaking
|
||||||
|
|
||||||
|
Minor : Advanced chat features ????
|
||||||
|
|
||||||
|
Minor : Game customization options
|
||||||
|
|
||||||
|
Minor : Spectator mode for games
|
||||||
|
|
||||||
|
|
||||||
@@ -1,24 +1,17 @@
|
|||||||
all :
|
all : up
|
||||||
@$(call random_shmol_cat, "hELLO", "nice human corrector", $(CLS), )
|
|
||||||
@docker compose -f ./docker-compose.yml up -d
|
|
||||||
|
|
||||||
no_cache :
|
up :
|
||||||
@docker compose -f ./docker-compose.yml build --no-cache
|
@docker compose -f ./docker-compose.yml build --no-cache
|
||||||
@docker compose -f ./docker-compose.yml up -d
|
@docker compose -f ./docker-compose.yml up -d
|
||||||
|
|
||||||
clean :
|
clean :
|
||||||
@$(call print_cat, $(CLEAR), $(C_225), $(C_320), $(C_450), $(call pad_word, 10, "Objects"), $(call pad_word, 12, "Exterminated"));
|
|
||||||
@docker compose -f ./docker-compose.yml down -t 1
|
@docker compose -f ./docker-compose.yml down -t 1
|
||||||
|
|
||||||
fclean :
|
fclean :
|
||||||
@$(call print_cat, $(CLEAR), $(C_120), $(C_300), $(C_210), $(call pad_word, 10, "All⠀clean"), $(call pad_word, 12, "Miaster"));
|
|
||||||
@docker compose -f ./docker-compose.yml down -v -t 1
|
@docker compose -f ./docker-compose.yml down -v -t 1
|
||||||
@docker system prune -af --volumes
|
@docker system prune -af --volumes
|
||||||
|
|
||||||
re : fclean no_cache
|
re : fclean up
|
||||||
@$(call print_cat, $(CLEAR), $(C_120), $(C_300), $(C_210), $(call pad_word, 10, "Re-Doing"), $(call pad_word, 12, "Miaster"));
|
|
||||||
|
|
||||||
.PHONY : all no_cache clean fclean re
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
volumes:
|
volumes:
|
||||||
data:
|
pgdata:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
transcendence:
|
transcendence:
|
||||||
@@ -12,7 +12,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- data:/var/lib/postgresql/data/pg15/
|
- pgdata:/var/lib/postgresql
|
||||||
env_file:
|
env_file:
|
||||||
- ../.env
|
- ../.env
|
||||||
networks:
|
networks:
|
||||||
@@ -24,8 +24,6 @@ services:
|
|||||||
build: ./srcs/backend
|
build: ./srcs/backend
|
||||||
expose:
|
expose:
|
||||||
- "3001"
|
- "3001"
|
||||||
# ports:
|
|
||||||
# - "3001:3001"
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- database
|
- database
|
||||||
volumes:
|
volumes:
|
||||||
@@ -40,7 +38,7 @@ services:
|
|||||||
container_name: frontend
|
container_name: frontend
|
||||||
build: ./srcs/frontend/
|
build: ./srcs/frontend/
|
||||||
ports:
|
ports:
|
||||||
- "8080:80"
|
- "8443:443"
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ async function createTables()
|
|||||||
status VARCHAR(20) DEFAULT 'waiting',
|
status VARCHAR(20) DEFAULT 'waiting',
|
||||||
max_players INT DEFAULT 8,
|
max_players INT DEFAULT 8,
|
||||||
current_round INT DEFAULT 0,
|
current_round INT DEFAULT 0,
|
||||||
max_rounds INT DEFAULT 3,
|
max_rounds INT DEFAULT 5,
|
||||||
round_duration INT DEFAULT 90,
|
round_duration INT DEFAULT 90,
|
||||||
created_at TIMESTAMP DEFAULT NOW(),
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
started_at TIMESTAMP,
|
started_at TIMESTAMP,
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
FROM node:20-alpine
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
RUN apk add --no-cache openssl
|
||||||
|
RUN mkdir -p /etc/backend/.ssl
|
||||||
|
RUN openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
||||||
|
-keyout /etc/backend/.ssl/key.pem \
|
||||||
|
-out /etc/backend/.ssl/cert.pem \
|
||||||
|
-subj "/CN=localhost" \
|
||||||
|
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import http from 'http';
|
import https from 'https';
|
||||||
|
import fs from 'fs';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import {Server} from 'socket.io';
|
import {Server} from 'socket.io';
|
||||||
import authRouter from './routes/auth.js';
|
import authRouter from './routes/auth.js';
|
||||||
@@ -13,7 +14,11 @@ import setupSocketIO from './services/socket.js';
|
|||||||
import avatarService from './services/avatar.js';
|
import avatarService from './services/avatar.js';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const server = http.createServer(app);
|
const httpsOptions = {
|
||||||
|
key: fs.readFileSync('/etc/backend/.ssl/key.pem'),
|
||||||
|
cert: fs.readFileSync('/etc/backend/.ssl/cert.pem')
|
||||||
|
};
|
||||||
|
const server = https.createServer(httpsOptions, app);
|
||||||
const io = new Server(server,
|
const io = new Server(server,
|
||||||
{
|
{
|
||||||
cors:
|
cors:
|
||||||
|
|||||||
@@ -26,6 +26,17 @@ router.post('/login', async(req, res) =>
|
|||||||
res.status(result.status).json(result.data);
|
res.status(result.status).json(result.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post('/logout', async(req, res) =>
|
||||||
|
{
|
||||||
|
const authHeader = req.headers['authorization'];
|
||||||
|
const token = authHeader && authHeader.split(' ')[1];
|
||||||
|
if (!token)
|
||||||
|
return (res.status(401).json({error: 'Missing token'}));
|
||||||
|
|
||||||
|
const result = await authService.logout(token);
|
||||||
|
res.status(result.status).json(result.data);
|
||||||
|
});
|
||||||
|
|
||||||
router.get('/github', (req, res) => {
|
router.get('/github', (req, res) => {
|
||||||
const githubAuthUrl = `https://github.com/login/oauth/authorize?` +
|
const githubAuthUrl = `https://github.com/login/oauth/authorize?` +
|
||||||
`client_id=${process.env.GITHUB_CLIENT_ID}&` +
|
`client_id=${process.env.GITHUB_CLIENT_ID}&` +
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ router.post('/upload', authenticateToken, upload.single('avatar'), async(req, re
|
|||||||
res.status(result.status).json(result.data);
|
res.status(result.status).json(result.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.delete('/', authenticateToken, async(req, res) =>
|
router.delete('/delete', authenticateToken, async(req, res) =>
|
||||||
{
|
{
|
||||||
const result = await avatarService.deleteAvatar(req.user.userId);
|
const result = await avatarService.deleteAvatar(req.user.userId);
|
||||||
res.status(result.status).json(result.data);
|
res.status(result.status).json(result.data);
|
||||||
|
|||||||
@@ -2,6 +2,30 @@ import bcrypt from 'bcrypt';
|
|||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import {query} from '../db.js';
|
import {query} from '../db.js';
|
||||||
|
|
||||||
|
async function logout(token)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!token)
|
||||||
|
return ({status: 400, data: {error: 'Missing token'}});
|
||||||
|
try
|
||||||
|
{
|
||||||
|
jwt.verify(token, process.env.JWT_SECRET);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return ({status: 401, data: {error: 'Invalid token'}});
|
||||||
|
}
|
||||||
|
|
||||||
|
return ({status: 200, data: {message: 'Logged out'}});
|
||||||
|
}
|
||||||
|
catch (err)
|
||||||
|
{
|
||||||
|
console.error(err);
|
||||||
|
return ({status: 500, data: {error: 'Server error'}});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function login(username, password)
|
async function login(username, password)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -60,4 +84,4 @@ async function register(username, password)
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {register, login};
|
export default {register, login, logout};
|
||||||
|
|||||||
@@ -69,6 +69,9 @@ async function deleteAvatar(userId) {
|
|||||||
if (currentAvatar === null)
|
if (currentAvatar === null)
|
||||||
return ({status: 404, data: {error: 'User not found'}});
|
return ({status: 404, data: {error: 'User not found'}});
|
||||||
|
|
||||||
|
if (currentAvatar === DEFAULT_AVATAR)
|
||||||
|
return ({status: 400, data: {error: 'Cannot delete default avatar'}});
|
||||||
|
|
||||||
// Reset the avatar to the default one
|
// Reset the avatar to the default one
|
||||||
await setAvatar(DEFAULT_AVATAR, userId);
|
await setAvatar(DEFAULT_AVATAR, userId);
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,63 @@ async function broadcastRoomsList(io) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function startRoomTimer(io, roomId, seconds)
|
||||||
|
{
|
||||||
|
const gameState = gameRooms.get(roomId);
|
||||||
|
if (!gameState) return;
|
||||||
|
|
||||||
|
if (gameState.timerInterval)
|
||||||
|
clearInterval(gameState.timerInterval);
|
||||||
|
|
||||||
|
gameState.timerSeconds = seconds;
|
||||||
|
|
||||||
|
gameState.timerInterval = setInterval(() => {
|
||||||
|
gameState.timerSeconds--;
|
||||||
|
|
||||||
|
if (gameState.timerSeconds < 0)
|
||||||
|
gameState.timerSeconds = 0;
|
||||||
|
|
||||||
|
if (gameState.timerSeconds <= 0)
|
||||||
|
{
|
||||||
|
io.to(roomId).emit('game-timer-sync', {
|
||||||
|
remaining: 0
|
||||||
|
});
|
||||||
|
clearInterval(gameState.timerInterval);
|
||||||
|
gameState.timerInterval = null;
|
||||||
|
io.to(roomId).emit('game-timer-ended', { message: 'Temps écoulé !' });
|
||||||
|
|
||||||
|
gameState.currentPlayerIndex = (gameState.currentPlayerIndex + 1) % gameState.players.length;
|
||||||
|
const nextDrawer = gameState.players[gameState.currentPlayerIndex];
|
||||||
|
gameState.drawer = nextDrawer;
|
||||||
|
|
||||||
|
|
||||||
|
gameState.currentWord = '';
|
||||||
|
gameState.revealedLetters = [];
|
||||||
|
gameState.revealedWord = [];
|
||||||
|
gameState.guessedLetters = [];
|
||||||
|
gameState.wrongGuesses = 0;
|
||||||
|
|
||||||
|
io.to(roomId).emit('game-new-round', {
|
||||||
|
drawer: nextDrawer
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
io.to(roomId).emit('game-timer-sync', {
|
||||||
|
remaining: gameState.timerSeconds
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopRoomTimer(roomId)
|
||||||
|
{
|
||||||
|
const gameState = gameRooms.get(roomId);
|
||||||
|
if (!gameState || !gameState.timerInterval) return;
|
||||||
|
clearInterval(gameState.timerInterval);
|
||||||
|
gameState.timerInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if a playing game has only 1 player left and auto-stop it
|
// Check if a playing game has only 1 player left and auto-stop it
|
||||||
async function checkAndStopSinglePlayerGame(io, roomId, dbRoomId) {
|
async function checkAndStopSinglePlayerGame(io, roomId, dbRoomId) {
|
||||||
if (!dbRoomId) return;
|
if (!dbRoomId) return;
|
||||||
@@ -43,6 +100,7 @@ async function checkAndStopSinglePlayerGame(io, roomId, dbRoomId) {
|
|||||||
const players = await gameRoomService.getRoomPlayers(dbRoomId);
|
const players = await gameRoomService.getRoomPlayers(dbRoomId);
|
||||||
if (players.length <= 1) {
|
if (players.length <= 1) {
|
||||||
console.log(`Room ${dbRoomId} has only ${players.length} player(s) left, ending game`);
|
console.log(`Room ${dbRoomId} has only ${players.length} player(s) left, ending game`);
|
||||||
|
stopRoomTimer(roomId);
|
||||||
|
|
||||||
// Update room status to 'ended'
|
// Update room status to 'ended'
|
||||||
await gameRoomService.updateRoomStatus(dbRoomId, 'waiting');
|
await gameRoomService.updateRoomStatus(dbRoomId, 'waiting');
|
||||||
@@ -192,7 +250,9 @@ function setupSocketIO(io)
|
|||||||
revealedLetters: gameState.revealedLetters,
|
revealedLetters: gameState.revealedLetters,
|
||||||
revealedWord: gameState.revealedWord || [],
|
revealedWord: gameState.revealedWord || [],
|
||||||
guessedLetters: gameState.guessedLetters,
|
guessedLetters: gameState.guessedLetters,
|
||||||
players: gameState.players
|
players: gameState.players,
|
||||||
|
scores: gameState.scores || {},
|
||||||
|
timer: gameState.timerSeconds || 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -202,6 +262,15 @@ function setupSocketIO(io)
|
|||||||
if (socket.gameRoomId) {
|
if (socket.gameRoomId) {
|
||||||
const roomId = socket.gameRoomId;
|
const roomId = socket.gameRoomId;
|
||||||
const dbRoomId = socket.gameRoomDbId;
|
const dbRoomId = socket.gameRoomDbId;
|
||||||
|
const userId = socket.user.userId;
|
||||||
|
|
||||||
|
if (dbRoomId && userId) {
|
||||||
|
try {
|
||||||
|
await gameRoomService.leaveRoom(dbRoomId, userId);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error removing player from room on socket leave:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
socket.to(roomId).emit('game-player-left', {
|
socket.to(roomId).emit('game-player-left', {
|
||||||
username: socket.user.username,
|
username: socket.user.username,
|
||||||
@@ -268,7 +337,8 @@ function setupSocketIO(io)
|
|||||||
revealedWord: gameState.revealedWord || [],
|
revealedWord: gameState.revealedWord || [],
|
||||||
guessedLetters: gameState.guessedLetters,
|
guessedLetters: gameState.guessedLetters,
|
||||||
players: gameState.players,
|
players: gameState.players,
|
||||||
scores: gameState.scores || {}
|
scores: gameState.scores || {},
|
||||||
|
timer: gameState.timerSeconds || 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -390,6 +460,7 @@ function setupSocketIO(io)
|
|||||||
const gameState = gameRooms.get(roomId);
|
const gameState = gameRooms.get(roomId);
|
||||||
if (!gameState) return;
|
if (!gameState) return;
|
||||||
|
|
||||||
|
startRoomTimer(io, roomId, 60);
|
||||||
gameState.currentWord = data.word.toLowerCase();
|
gameState.currentWord = data.word.toLowerCase();
|
||||||
gameState.revealedLetters = new Array(data.word.length).fill(false);
|
gameState.revealedLetters = new Array(data.word.length).fill(false);
|
||||||
gameState.revealedWord = new Array(data.word.length).fill('_');
|
gameState.revealedWord = new Array(data.word.length).fill('_');
|
||||||
@@ -552,6 +623,8 @@ function setupSocketIO(io)
|
|||||||
// Update round start scores for next round
|
// Update round start scores for next round
|
||||||
gameState.roundStartScores = { ...gameState.scores };
|
gameState.roundStartScores = { ...gameState.scores };
|
||||||
|
|
||||||
|
stopRoomTimer(roomId);
|
||||||
|
|
||||||
io.to(roomId).emit('game-word-found', {
|
io.to(roomId).emit('game-word-found', {
|
||||||
word: gameState.currentWord,
|
word: gameState.currentWord,
|
||||||
winner: username,
|
winner: username,
|
||||||
@@ -613,6 +686,7 @@ function setupSocketIO(io)
|
|||||||
// If the drawer left and there are still enough players, choose a new drawer
|
// If the drawer left and there are still enough players, choose a new drawer
|
||||||
if (wasDrawer && gameState.players.length >= 1)
|
if (wasDrawer && gameState.players.length >= 1)
|
||||||
{
|
{
|
||||||
|
stopRoomTimer(roomId);
|
||||||
// Pick the next player as the new drawer
|
// Pick the next player as the new drawer
|
||||||
gameState.currentPlayerIndex = gameState.currentPlayerIndex % gameState.players.length;
|
gameState.currentPlayerIndex = gameState.currentPlayerIndex % gameState.players.length;
|
||||||
const newDrawer = gameState.players[gameState.currentPlayerIndex];
|
const newDrawer = gameState.players[gameState.currentPlayerIndex];
|
||||||
@@ -632,6 +706,7 @@ function setupSocketIO(io)
|
|||||||
reason: 'drawer_left',
|
reason: 'drawer_left',
|
||||||
message: `${username} (dessinateur) a quitté, ${newDrawer} devient le nouveau dessinateur`
|
message: `${username} (dessinateur) a quitté, ${newDrawer} devient le nouveau dessinateur`
|
||||||
});
|
});
|
||||||
|
startRoomTimer(io, roomId, 60);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -652,6 +727,7 @@ function setupSocketIO(io)
|
|||||||
socket.on('game-end', async () => {
|
socket.on('game-end', async () => {
|
||||||
const roomId = socket.gameRoomId;
|
const roomId = socket.gameRoomId;
|
||||||
if (!roomId) return;
|
if (!roomId) return;
|
||||||
|
stopRoomTimer(roomId);
|
||||||
|
|
||||||
// Update room status to 'waiting' in database
|
// Update room status to 'waiting' in database
|
||||||
const dbRoomId = socket.gameRoomDbId;
|
const dbRoomId = socket.gameRoomDbId;
|
||||||
@@ -730,6 +806,16 @@ function setupSocketIO(io)
|
|||||||
_tetrisRelayToOpponent(socket, 'tetris:lines-cleared', data);
|
_tetrisRelayToOpponent(socket, 'tetris:lines-cleared', data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Relay pur : shield-activated → adversaire uniquement
|
||||||
|
socket.on('tetris:shield-activated', () => {
|
||||||
|
_tetrisRelayToOpponent(socket, 'tetris:shield-activated', {});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Relay pur : shield-deactivated → adversaire uniquement
|
||||||
|
socket.on('tetris:shield-deactivated', () => {
|
||||||
|
_tetrisRelayToOpponent(socket, 'tetris:shield-deactivated', {});
|
||||||
|
});
|
||||||
|
|
||||||
// start-duel → relayé aux DEUX joueurs de la room (inclut l'émetteur)
|
// start-duel → relayé aux DEUX joueurs de la room (inclut l'émetteur)
|
||||||
socket.on('tetris:start-duel', () => {
|
socket.on('tetris:start-duel', () => {
|
||||||
const code = socket.tetrisRoomCode;
|
const code = socket.tetrisRoomCode;
|
||||||
@@ -857,6 +943,14 @@ function setupSocketIO(io)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
if (dbRoomId && socket.user.userId) {
|
||||||
|
try {
|
||||||
|
await gameRoomService.leaveRoom(dbRoomId, socket.user.userId);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error removing disconnected player from room:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Regular player disconnect
|
// Regular player disconnect
|
||||||
socket.to(roomId).emit('game-player-left', {
|
socket.to(roomId).emit('game-player-left', {
|
||||||
username: socket.user.username,
|
username: socket.user.username,
|
||||||
|
|||||||
@@ -1,5 +1,20 @@
|
|||||||
FROM nginx:alpine
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
RUN apk add --no-cache openssl && \
|
||||||
|
mkdir -p /etc/nginx/ssl && \
|
||||||
|
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
||||||
|
-keyout /etc/nginx/ssl/key.pem \
|
||||||
|
-out /etc/nginx/ssl/cert.pem \
|
||||||
|
-subj "/CN=localhost" \
|
||||||
|
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
|
||||||
|
|
||||||
|
#ADDED
|
||||||
|
RUN rm -f /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
COPY src /usr/share/nginx/html
|
COPY src /usr/share/nginx/html
|
||||||
|
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
EXPOSE 80
|
|
||||||
|
EXPOSE 443
|
||||||
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 443 ssl;
|
||||||
|
|
||||||
|
ssl_certificate /etc/nginx/ssl/cert.pem;
|
||||||
|
ssl_certificate_key /etc/nginx/ssl/key.pem;
|
||||||
|
error_page 497 =301 https://$host:8443$request_uri;
|
||||||
|
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
@@ -11,27 +15,33 @@ server {
|
|||||||
|
|
||||||
# Backend API
|
# Backend API
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass http://backend:3001;
|
proxy_pass https://backend:3001;
|
||||||
|
proxy_ssl_verify off;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Socket.IO WebSocket proxying
|
# Socket.IO WebSocket proxying
|
||||||
location /socket.io/ {
|
location /socket.io/ {
|
||||||
proxy_pass http://backend:3001;
|
proxy_pass https://backend:3001;
|
||||||
|
proxy_ssl_verify off;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
|
proxy_read_timeout 3600s;
|
||||||
|
proxy_send_timeout 3600s;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /avatar/ {
|
location /avatar/ {
|
||||||
proxy_pass http://backend:3001/avatar/;
|
proxy_pass https://backend:3001/avatar/;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
proxy_ssl_verify off;
|
||||||
proxy_hide_header Content-Type;
|
proxy_hide_header Content-Type;
|
||||||
add_header Cache-Control "public, max-age=3600";
|
add_header Cache-Control "public, max-age=3600";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,14 @@
|
|||||||
* Application entry point
|
* Application entry point
|
||||||
* Initializes windows and handles menu interactions
|
* Initializes windows and handles menu interactions
|
||||||
*/
|
*/
|
||||||
import { windowRegistry } from './windows.js';
|
import { windowRegistry } from './core/windows.js';
|
||||||
import { LoginWindow } from './login.js';
|
import { LoginWindow } from './windows/login.js';
|
||||||
import { GlobalChat } from './global_chat.js';
|
import { LogoutWindow } from './windows/logout.js';
|
||||||
import { AvatarWindow } from './avatar.js';
|
import { GlobalChat } from './windows/global_chat.js';
|
||||||
import { FriendsWindow } from './friends.js';
|
import { AvatarWindow } from './windows/avatar.js';
|
||||||
import { GameRoomWindow } from './game_room.js';
|
import { FriendsWindow } from './windows/friends.js';
|
||||||
import { StatsWindow } from './stats.js';
|
import { GameRoomWindow } from './windows/game_room.js';
|
||||||
|
import { StatsWindow } from './windows/stats.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main application class
|
* Main application class
|
||||||
@@ -16,7 +17,6 @@ import { StatsWindow } from './stats.js';
|
|||||||
*/
|
*/
|
||||||
class App {
|
class App {
|
||||||
constructor() {
|
constructor() {
|
||||||
console.log("APP STARTED");
|
|
||||||
this.initWindows();
|
this.initWindows();
|
||||||
this.initMenu();
|
this.initMenu();
|
||||||
this.initPage();
|
this.initPage();
|
||||||
@@ -34,6 +34,7 @@ class App {
|
|||||||
new FriendsWindow();
|
new FriendsWindow();
|
||||||
new GameRoomWindow();
|
new GameRoomWindow();
|
||||||
new StatsWindow();
|
new StatsWindow();
|
||||||
|
new LogoutWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,7 +52,8 @@ class App {
|
|||||||
'login': 'login',
|
'login': 'login',
|
||||||
'chat': 'chat',
|
'chat': 'chat',
|
||||||
'avatar': 'avatar',
|
'avatar': 'avatar',
|
||||||
'friends': 'friends'
|
'friends': 'friends',
|
||||||
|
'logout': 'logout'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Event delegation on the menu
|
// Event delegation on the menu
|
||||||
@@ -76,10 +78,6 @@ class App {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const actionMap = {
|
|
||||||
'gameroom': 'gameroom'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Event delegation on the menu
|
// Event delegation on the menu
|
||||||
page.addEventListener('click', (e) => {
|
page.addEventListener('click', (e) => {
|
||||||
const button = e.target.closest('.page__item');
|
const button = e.target.closest('.page__item');
|
||||||
@@ -87,9 +85,14 @@ class App {
|
|||||||
|
|
||||||
const action = button.dataset.action;
|
const action = button.dataset.action;
|
||||||
|
|
||||||
// Actions with associated windows
|
if (action === 'gameroom') {
|
||||||
if (actionMap[action]) {
|
const gameRoomWindow = windowRegistry.get('gameroom');
|
||||||
windowRegistry.toggle(actionMap[action]);
|
windowRegistry.toggle('gameroom');
|
||||||
|
gameRoomWindow.loadRooms();
|
||||||
|
|
||||||
|
if (gameRoomWindow?.currentTab === 'browse') {
|
||||||
|
gameRoomWindow.loadRooms();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,34 +113,34 @@ class App {
|
|||||||
|
|
||||||
colorizeUI() {
|
colorizeUI() {
|
||||||
|
|
||||||
const elements = document.querySelectorAll(".title, .menu__item, .game__item, .page__item");
|
const elements = document.querySelectorAll(".title, .menu__item, .game__item, .page__item");
|
||||||
|
|
||||||
const colorizeText = (el) => {
|
const colorizeText = (el) => {
|
||||||
const text = el.textContent;
|
const text = el.textContent;
|
||||||
el.innerHTML = "";
|
el.innerHTML = "";
|
||||||
|
|
||||||
const baseHue = Math.random() * 360;
|
const baseHue = Math.random() * 360;
|
||||||
|
|
||||||
// 🎲 random step = makes rainbow "scrambled"
|
// 🎲 random step = makes rainbow "scrambled"
|
||||||
const step = (Math.random() * 60) + 10; // 10 → 70
|
const step = (Math.random() * 60) + 10; // 10 → 70
|
||||||
|
|
||||||
// 🎲 random direction (left or right rainbow)
|
// 🎲 random direction (left or right rainbow)
|
||||||
const direction = Math.random() < 0.5 ? 1 : -1;
|
const direction = Math.random() < 0.5 ? 1 : -1;
|
||||||
|
|
||||||
[...text].forEach((char, i) => {
|
[...text].forEach((char, i) => {
|
||||||
const span = document.createElement("span");
|
const span = document.createElement("span");
|
||||||
span.textContent = char;
|
span.textContent = char;
|
||||||
|
|
||||||
const hue = baseHue + (i * step * direction);
|
const hue = baseHue + (i * step * direction);
|
||||||
|
|
||||||
span.style.color = `hsl(${hue}, 90%, 60%)`;
|
span.style.color = `hsl(${hue}, 90%, 60%)`;
|
||||||
|
|
||||||
span.style.textShadow = `1px 1px 0 rgba(0,0,0,0.3)`;
|
span.style.textShadow = `1px 1px 0 rgba(0,0,0,0.3)`;
|
||||||
|
|
||||||
el.appendChild(span);
|
el.appendChild(span);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
elements.forEach(colorizeText);
|
elements.forEach(colorizeText);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -147,4 +150,4 @@ if (document.readyState === 'loading') {
|
|||||||
document.addEventListener('DOMContentLoaded', () => new App());
|
document.addEventListener('DOMContentLoaded', () => new App());
|
||||||
} else {
|
} else {
|
||||||
new App();
|
new App();
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 437 KiB |
|
Before Width: | Height: | Size: 438 KiB |
|
Before Width: | Height: | Size: 517 KiB |
|
Before Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
@@ -6,12 +6,14 @@
|
|||||||
export const API = {
|
export const API = {
|
||||||
AUTH: {
|
AUTH: {
|
||||||
LOGIN: '/api/auth/login',
|
LOGIN: '/api/auth/login',
|
||||||
|
LOGOUT: '/api/auth/logout',
|
||||||
REGISTER: '/api/auth/register',
|
REGISTER: '/api/auth/register',
|
||||||
GITHUB: '/api/auth/github'
|
GITHUB: '/api/auth/github'
|
||||||
},
|
},
|
||||||
AVATAR: {
|
AVATAR: {
|
||||||
GET: '/api/avatar/me',
|
GET: '/api/avatar/me',
|
||||||
UPLOAD: '/api/avatar/upload'
|
UPLOAD: '/api/avatar/upload',
|
||||||
|
DELETE: '/api/avatar/delete'
|
||||||
},
|
},
|
||||||
FRIENDS: {
|
FRIENDS: {
|
||||||
LIST: '/api/friends',
|
LIST: '/api/friends',
|
||||||
@@ -82,6 +82,7 @@ export const Events = {
|
|||||||
|
|
||||||
// Avatar
|
// Avatar
|
||||||
AVATAR_UPDATED: 'avatar:updated',
|
AVATAR_UPDATED: 'avatar:updated',
|
||||||
|
AVATAR_DELETED: 'avatar:deleted',
|
||||||
|
|
||||||
// Chat
|
// Chat
|
||||||
CHAT_CONNECTED: 'chat:connected',
|
CHAT_CONNECTED: 'chat:connected',
|
||||||