30 Commits

Author SHA1 Message Date
Kali Gallon b40dad8f57 ^^._, work in progress, small changes 2026-04-02 15:00:36 +02:00
Kali Gallon 23cca7a249 Merge branch 'Rendu' of github.com:OlaketalAmigo/Transcendence into Rendu 2026-04-02 14:56:50 +02:00
Kali Gallon e1e529b3ca ^^._, work in progress, small changes 2026-04-02 14:55:07 +02:00
Thomas Fauve-Piot e96c16819d Merge branch 'Rendu' of github.com:OlaketalAmigo/Transcendence into Rendu 2026-04-02 12:59:19 +02:00
Thomas Fauve-Piot aefb858247 Overflow game.css 2026-04-02 12:55:56 +02:00
Yannis Antoine a9f81b4d65 5 point par lettre bonne 2026-04-02 12:53:46 +02:00
Georges-Leonard Prunet 4b3909c1a3 auto log out + clean leave 2026-04-02 12:52:06 +02:00
Georges-Leonard Prunet 4fa835b62a clean 2026-04-02 11:58:46 +02:00
Thomas Fauve-Piot b31436a40a Wiskas 2026-04-02 11:32:21 +02:00
Thomas f9587c5cfa Initialize README with project details and instructions
Added project overview, instructions, features, dependencies, team information, project management, technical stack, and modules details to README.
2026-04-01 19:01:20 +02:00
Thomas Fauve-Piot 44a0ffe743 Added mentions legales 2026-04-01 18:22:02 +02:00
Thomas Fauve-Piot 0f0e777e6e Chat + legal mentions missing 2026-04-01 17:58:57 +02:00
Georges-Leonard Prunet c96629b704 Fix lobby player leaving 2026-03-31 15:59:02 +02:00
Georges-Leonard Prunet 41612f5d39 https + volume 2026-03-31 14:21:11 +02:00
kalips003 e1573ba9f0 ^^._, work in progress, small changes 2026-03-31 05:35:04 +02:00
kalips003 b9c4c817f8 ^^._, work in progress, small changes 2026-03-30 22:33:13 +02:00
kalips003 384363c8dd ^^._, work in progress, small changes 2026-03-30 17:19:14 +02:00
Kali Gallon def9918047 ^^._, work in progress, small changes 2026-03-30 01:36:33 +02:00
Kali Gallon cafa0cccc4 ^^._, work in progress, small changes 2026-03-27 23:17:31 +01:00
kalips003 8b907d5a86 transfer 2026-03-27 20:17:21 +01:00
H3XploR 13f93fb332 https installed 2026-03-24 14:29:04 +01:00
gprunet 801750da96 Notifs + Logout + delete Avatar 2026-03-22 18:26:50 +01:00
H3XploR 82623b0078 Merge pull request #21 from OlaketalAmigo/modular_code
cleaned
2026-03-22 13:48:56 +01:00
H3XploR d3e2d9bdf9 cleaned 2026-03-22 13:48:16 +01:00
H3XploR 9c1e8e03bb Merge pull request #20 from OlaketalAmigo/TETRIS
Tetris
2026-03-20 23:39:05 +01:00
Georges-Leonard Prunet 55c241fd61 notification for login/register 2026-03-20 17:57:17 +01:00
Georges-Leonard Prunet 592bb38c0d fixed next drawer 2026-03-20 17:29:06 +01:00
H3XploR 72bc9ea628 added shield 2026-03-19 14:38:56 +01:00
H3XploR 557cf23f71 reset before join 2026-03-19 14:14:20 +01:00
H3XploR b51b711b10 ajout de theme 2026-03-19 14:00:20 +01:00
189 changed files with 2630 additions and 2241 deletions
-10
View File
@@ -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
+142
View File
@@ -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**
![Database Schema required ... ](./postgresql-architecture.jpg)
**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
+3 -10
View File
@@ -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, "Allclean"), $(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
+3
View File
@@ -0,0 +1,3 @@
INTRA_CLIENT_ID=u-s4t2ud-c226cd35cd1ac08a4c6668deee1c64d7d67a13a766aee672acafd4a1522d483c
INTRA_CLIENT_SECRET=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
+3 -5
View File
@@ -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:
+1 -1
View File
@@ -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,
+8
View File
@@ -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 ./
+9 -2
View File
@@ -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';
@@ -11,9 +12,14 @@ import playerStatsRouter from './routes/player_stats.js';
import {waitForDb, createTables, runMigrations, ensureOauthClient} from './db.js'; import {waitForDb, createTables, runMigrations, ensureOauthClient} from './db.js';
import setupSocketIO from './services/socket.js'; import setupSocketIO from './services/socket.js';
import avatarService from './services/avatar.js'; import avatarService from './services/avatar.js';
import intraRouter from './routes/intra.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:
@@ -48,6 +54,7 @@ async function startServer()
app.use('/api/avatar', avatarRouter); app.use('/api/avatar', avatarRouter);
app.use('/api/friends', friendsRouter); app.use('/api/friends', friendsRouter);
app.use('/api/stats', playerStatsRouter); app.use('/api/stats', playerStatsRouter);
app.use('/api/intra', intraRouter);
app.get('/api', (req, res) => res.send('Backend running')); app.get('/api', (req, res) => res.send('Backend running'));
server.listen(3001, () => server.listen(3001, () =>
+11
View File
@@ -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}&` +
+1 -1
View File
@@ -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);
@@ -0,0 +1,53 @@
// routes/intra.js
import express from 'express';
const router = express.Router();
let token;
async function set_token() {
try {
const response = await fetch("https://api.intra.42.fr/oauth/token", {
method: "POST",
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: process.env.INTRA_CLIENT_ID,
client_secret: process.env.INTRA_CLIENT_SECRET
}),
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
});
token = await response.json();
setTimeout(set_token, (token.expires_in - 60) * 1000);
} catch (e) {
console.error("Token error:", e);
}
}
set_token();
router.get('/profile/:login', async (req, res) => {
try {
const response = await fetch(
`https://api.intra.42.fr/v2/users/${req.params.login}`,
{
headers: {
Authorization: `Bearer ${token.access_token}`
}
}
);
if (!response.ok) {
return res.status(response.status).json({ error: 'User not found' });
}
res.json(await response.json());
} catch (e) {
res.status(500).json({ error: 'Failed to fetch profile' });
}
});
export default router;
+25 -1
View File
@@ -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);
+139 -37
View File
@@ -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');
@@ -82,6 +140,47 @@ async function saveRoundPoints(currentScores, roundStartScores) {
} }
} }
function handlePlayerDeparture(io, roomId, username) {
const gameState = gameRooms.get(roomId);
if (!gameState || !gameState.isPlaying) return;
if (!Array.isArray(gameState.players)) return;
const leavingIndex = gameState.players.indexOf(username);
if (leavingIndex === -1) return;
const wasDrawer = gameState.drawer === username;
gameState.players = gameState.players.filter(p => p !== username);
if (gameState.scores) {
delete gameState.scores[username];
}
if (gameState.currentPlayerIndex >= leavingIndex) {
gameState.currentPlayerIndex = Math.max(0, gameState.currentPlayerIndex - 1);
}
if (gameState.currentPlayerIndex >= gameState.players.length) {
gameState.currentPlayerIndex = 0;
}
if (wasDrawer && gameState.players.length > 0) {
stopRoomTimer(roomId);
const newDrawer = gameState.players[gameState.currentPlayerIndex];
gameState.drawer = newDrawer;
gameState.currentWord = '';
gameState.revealedLetters = [];
gameState.revealedWord = [];
gameState.guessedLetters = [];
gameState.wrongGuesses = 0;
io.to(roomId).emit('game-drawer-changed', {
newDrawer: newDrawer,
reason: 'drawer_left',
message: `${username} (dessinateur) a quitte, ${newDrawer} devient le nouveau dessinateur`
});
startRoomTimer(io, roomId, 60);
}
}
function setupSocketIO(io) function setupSocketIO(io)
{ {
ioInstance = io; ioInstance = io;
@@ -192,7 +291,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,11 +303,21 @@ 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,
userId: socket.user.userId userId: socket.user.userId
}); });
handlePlayerDeparture(io, roomId, socket.user.username);
socket.leave(roomId); socket.leave(roomId);
console.log(`${socket.user.username} left ${roomId}`); console.log(`${socket.user.username} left ${roomId}`);
@@ -268,7 +379,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 +502,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('_');
@@ -503,7 +616,7 @@ function setupSocketIO(io)
// Points: 10 per letter found, -5 for wrong guess // Points: 10 per letter found, -5 for wrong guess
if (success) { if (success) {
points = lettersFound * 10; points = lettersFound * 5;
gameState.scores[username] += points; gameState.scores[username] += points;
} else { } else {
points = -5; points = -5;
@@ -552,6 +665,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,
@@ -600,40 +715,7 @@ function setupSocketIO(io)
message: `${username} a quitté la partie` message: `${username} a quitté la partie`
}); });
const gameState = gameRooms.get(roomId); handlePlayerDeparture(io, roomId, username);
if (gameState)
{
const wasDrawer = gameState.drawer === username;
gameState.players = gameState.players.filter(p => p !== username);
delete gameState.scores[username];
io.to(roomId).emit('scores-updated', gameState.scores);
// If the drawer left and there are still enough players, choose a new drawer
if (wasDrawer && gameState.players.length >= 1)
{
// Pick the next player as the new drawer
gameState.currentPlayerIndex = gameState.currentPlayerIndex % gameState.players.length;
const newDrawer = gameState.players[gameState.currentPlayerIndex];
gameState.drawer = newDrawer;
// Reset the word state for the new round
gameState.currentWord = '';
gameState.revealedLetters = [];
gameState.revealedWord = [];
gameState.guessedLetters = [];
gameState.wrongGuesses = 0;
console.log(`Drawer ${username} left, new drawer is ${newDrawer}`);
io.to(roomId).emit('game-drawer-changed', {
newDrawer: newDrawer,
reason: 'drawer_left',
message: `${username} (dessinateur) a quitté, ${newDrawer} devient le nouveau dessinateur`
});
}
}
await checkAndStopSinglePlayerGame(io, roomId, dbRoomId); await checkAndStopSinglePlayerGame(io, roomId, dbRoomId);
@@ -652,6 +734,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 +813,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,11 +950,20 @@ 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,
userId: socket.user.userId userId: socket.user.userId
}); });
handlePlayerDeparture(io, roomId, socket.user.username);
// Get updated player list and broadcast // Get updated player list and broadcast
if (dbRoomId) { if (dbRoomId) {
+16 -1
View File
@@ -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;"]
+15 -5
View File
@@ -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";
} }
+201
View File
@@ -0,0 +1,201 @@
/**
* Application entry point
* Initializes windows and handles menu interactions
*/
import { windowRegistry } from './core/windows.js';
import { API, STORAGE_KEYS } from './core/config.js';
import { eventBus, Events } from './core/events.js';
import { LoginWindow } from './windows/login.js';
import { LogoutWindow } from './windows/logout.js';
import { GlobalChat } from './windows/global_chat.js';
import { AvatarWindow } from './windows/avatar.js';
import { FriendsWindow } from './windows/friends.js';
import { GameRoomWindow } from './windows/game_room.js';
import { StatsWindow } from './windows/stats.js';
/**
* Main application class
* Handles initialization and menu interactions
*/
class App {
constructor() {
this.invalidateStaleToken();
this.initWindows();
this.initMenu();
this.initPage();
this.initEasterEgg();
this.colorizeUI();
}
async invalidateStaleToken() {
const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
if (!token) return;
if (this.isJwtExpired(token)) {
localStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN);
eventBus.emit(Events.USER_LOGGED_OUT);
return;
}
try {
const response = await fetch(API.STATS.ME, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (response.status === 401) {
localStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN);
eventBus.emit(Events.USER_LOGGED_OUT);
setTimeout(() => window.location.reload(), 500);
}
} catch (error) {
console.warn('Token validation skipped:', error);
}
}
isJwtExpired(token) {
try {
const payload = this.decodeJwtPayload(token);
if (!payload || !payload.exp) return false;
const now = Math.floor(Date.now() / 1000);
return payload.exp <= now;
} catch (error) {
return false;
}
}
decodeJwtPayload(token) {
const parts = token.split('.');
if (parts.length < 2) return null;
const base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=');
return JSON.parse(atob(padded));
}
/**
* Initializes all windows
*/
initWindows() {
new LoginWindow();
new GlobalChat();
new AvatarWindow();
new FriendsWindow();
new GameRoomWindow();
new StatsWindow();
new LogoutWindow();
}
/**
* Initializes the main menu
* Uses event delegation instead of IDs
*/
initMenu() {
const menu = document.querySelector('.menu');
if (!menu) {
console.warn('Menu not found');
return;
}
const actionMap = {
'login': 'login',
'chat': 'chat',
'avatar': 'avatar',
'friends': 'friends',
'logout': 'logout'
};
// Event delegation on the menu
menu.addEventListener('click', (e) => {
const button = e.target.closest('.menu__item');
if (!button) return;
const action = button.dataset.action;
// Actions with associated windows
if (actionMap[action]) {
windowRegistry.toggle(actionMap[action]);
return;
}
});
}
initPage() {
const page = document.querySelector('.page');
if (!page) {
return;
}
// Event delegation on the menu
page.addEventListener('click', (e) => {
const button = e.target.closest('.page__item');
if (!button) return;
const action = button.dataset.action;
if (action === 'gameroom') {
const gameRoomWindow = windowRegistry.get('gameroom');
windowRegistry.toggle('gameroom');
gameRoomWindow.loadRooms();
if (gameRoomWindow?.currentTab === 'browse') {
gameRoomWindow.loadRooms();
}
return;
}
});
}
/**
* Initializes the easter egg button
*/
initEasterEgg() {
const easterEgg = document.querySelector('.easter-egg');
if (easterEgg) {
easterEgg.addEventListener('click', () => {
alert('DONT CLICK!');
});
}
}
colorizeUI() {
const elements = document.querySelectorAll(".title, .menu__item, .game__item, .page__item");
const colorizeText = (el) => {
const text = el.textContent;
el.innerHTML = "";
const baseHue = Math.random() * 360;
// 🎲 random step = makes rainbow "scrambled"
const step = (Math.random() * 60) + 10; // 10 → 70
// 🎲 random direction (left or right rainbow)
const direction = Math.random() < 0.5 ? 1 : -1;
[...text].forEach((char, i) => {
const span = document.createElement("span");
span.textContent = char;
const hue = baseHue + (i * step * direction);
span.style.color = `hsl(${hue}, 90%, 60%)`;
span.style.textShadow = `1px 1px 0 rgba(0,0,0,0.3)`;
el.appendChild(span);
});
};
elements.forEach(colorizeText);
}
}
// Start the application when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => new App());
} else {
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 437 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 438 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 517 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

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',

Some files were not shown because too many files have changed in this diff Show More