From 5bdf7f04426405b3a6c2c00553f19876c1b3e657 Mon Sep 17 00:00:00 2001 From: gprunet Date: Wed, 21 Jan 2026 16:11:28 +0100 Subject: [PATCH] GROS FIX + avatar --- srcs/backend/avatar/default.png | Bin 0 -> 2595 bytes srcs/backend/index.js | 4 + srcs/backend/middleware/auth.js | 2 +- srcs/backend/package.json | 4 +- srcs/backend/routes/avatar.js | 50 ++++++++++ srcs/backend/routes/game_room.js | 2 +- srcs/backend/services/avatar.js | 147 +++++++++++++++++++++++++++++ srcs/backend/services/game_room.js | 24 +++-- 8 files changed, 220 insertions(+), 13 deletions(-) create mode 100644 srcs/backend/avatar/default.png create mode 100644 srcs/backend/routes/avatar.js create mode 100644 srcs/backend/services/avatar.js diff --git a/srcs/backend/avatar/default.png b/srcs/backend/avatar/default.png new file mode 100644 index 0000000000000000000000000000000000000000..28f6a850b50d53156607dabe5cb5477ee6e762a7 GIT binary patch literal 2595 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 qsNcu?UNufS9`8l`ANdhZ res.send('Backend running')); server.listen(3001, () => diff --git a/srcs/backend/middleware/auth.js b/srcs/backend/middleware/auth.js index c86cc0c..0e5f8e7 100644 --- a/srcs/backend/middleware/auth.js +++ b/srcs/backend/middleware/auth.js @@ -1,7 +1,7 @@ import jwt from 'jsonwebtoken'; -//Check si le webtoken est valide +// Check if the webtoken is valid export default function authMiddleware(req, res, next) { const header = req.headers.authorization; diff --git a/srcs/backend/package.json b/srcs/backend/package.json index 98bd860..623851a 100644 --- a/srcs/backend/package.json +++ b/srcs/backend/package.json @@ -11,6 +11,8 @@ "cors": "^2.8.5", "passport": "0.7.0", "passport-github2": "0.1.12", - "express-session": "1.18.0" + "express-session": "1.18.0", + "multer": "^1.4.5-lts.1", + "file-type": "^19.0.0" } } \ No newline at end of file diff --git a/srcs/backend/routes/avatar.js b/srcs/backend/routes/avatar.js new file mode 100644 index 0000000..7e26560 --- /dev/null +++ b/srcs/backend/routes/avatar.js @@ -0,0 +1,50 @@ +import express from 'express'; +import multer from 'multer'; +import avatarService from '../services/avatar.js'; +import {authenticateToken} from '../middleware/auth.js'; + +const router = express.Router(); + +// Configue multer to use RAM +const storage = multer.memoryStorage(); +const upload = multer +({ + storage: storage, + limits: + { + fileSize: 5 * 1024 * 1024 // 5mb + } +}); + +router.post('/upload', authenticateToken, upload.single('avatar'), async(req, res) => +{ + if (!req.file) + return res.status(400).json({ error: 'No file uploaded' }); + + const result = await avatarService.uploadAvatar(req.user.userId, req.file); + res.status(result.status).json(result.data); +}); + +router.delete('/', authenticateToken, async(req, res) => +{ + const result = await avatarService.deleteAvatar(req.user.userId); + res.status(result.status).json(result.data); +}); + +router.get('/me', authenticateToken, async(req, res) => +{ + const result = await avatarService.getAvatarUrl(req.user.userId); + res.status(result.status).json(result.data); +}); + +router.get('/user/:userId', async(req, res) => +{ + const userId = parseInt(req.params.userId); + if (isNaN(userId)) + return res.status(400).json({ error: 'Invalid user ID' }); + + const result = await avatarService.getAvatarUrl(userId); + res.status(result.status).json(result.data); +}); + +export default router; diff --git a/srcs/backend/routes/game_room.js b/srcs/backend/routes/game_room.js index 925bdc4..0a98719 100644 --- a/srcs/backend/routes/game_room.js +++ b/srcs/backend/routes/game_room.js @@ -54,7 +54,7 @@ router.post('/', authenticateToken, async(req, res) => const {name} = req.body; if (!name) return (res.status(400).json({error: 'Room name required'})); - const room = await gameRoomService.createRoom(name); + const room = await gameRoomService.createRoom(name, req.user.userId); res.status(201).json(room); } catch(err) diff --git a/srcs/backend/services/avatar.js b/srcs/backend/services/avatar.js new file mode 100644 index 0000000..5af32ec --- /dev/null +++ b/srcs/backend/services/avatar.js @@ -0,0 +1,147 @@ +import {query} from '../db.js'; +import path from 'path'; +import fs from 'fs'; +import {fileURLToPath} from 'url'; +import {fileTypeFromBuffer} from 'file-type'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const AVATAR_DIR = path.join(__dirname, '../avatar'); +const DEFAULT_AVATAR = '/avatar/default.png'; +const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5mb +const ALLOWED_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; + +// Verify that the avatar folder already exists +if (!fs.existsSync(AVATAR_DIR)) +{ + fs.mkdirSync(AVATAR_DIR, { recursive: true }); +} + +async function uploadAvatar(userId, file) +{ + try + { + // File check (type and size) + if (!file) + return ({status: 400, data: {error: 'No file provided'}}); + + if (!ALLOWED_TYPES.includes(file.mimetype)) + return ({status: 400, data: { error: 'Invalid file type. Only JPEG, PNG, GIF, and WebP allowed'}}); + + const fileType = await fileTypeFromBuffer(file.buffer); + if (!fileType || !['image/jpeg', 'image/png', 'image/gif', 'image/webp'].includes(fileType.mime)) + return ({status: 400, data: {error: 'Invalid file content. File does not match allowed image types'}}); + + if (file.size > MAX_FILE_SIZE) + return ({status: 400, data: {error: 'File too large. Maximum size is 5MB'}}); + + + const currentAvatar = await getCurrentAvatar(userId); + if (currentAvatar === null) + return ({status: 404, data: {error: 'User not found'}}); + + // Create a unique name for the new avatar to avoid duplicates + const fileExt = path.extname(file.originalname); + const fileName = `user_${userId}_${Date.now()}${fileExt}`; + const avatarPath = `/avatar/${fileName}`; + + // Save the new avatar in the folder + const filePath = path.join(AVATAR_DIR, fileName); + fs.writeFileSync(filePath, file.buffer); + + await setAvatar(avatarPath, userId); + + deleteNonDefault(currentAvatar); + return ({status: 200, data: {avatar_url: avatarPath}}); + } + catch (err) + { + console.error('Avatar upload error:', err); + return { status: 500, data: { error: 'Server error' } }; + } +} + +async function deleteAvatar(userId) { + try + { + const currentAvatar = await getCurrentAvatar(userId); + if (currentAvatar === null) + return ({status: 404, data: {error: 'User not found'}}); + + // Reset the avatar to the default one + await setAvatar(DEFAULT_AVATAR, userId); + + deleteNonDefault(currentAvatar); + return ({status: 200, data: {avatar_url: DEFAULT_AVATAR}}); + } + catch (err) + { + console.error('Avatar delete error:', err); + return ({status: 500, data: {error: 'Server error'}}); + } +} + +async function setAvatar(newAvatar, userId) +{ + await query + ( + 'UPDATE users SET avatar_url = $1 WHERE id = $2', + [newAvatar, userId] + ); +} + +async function getCurrentAvatar(userId) +{ + const res = await query + ( + 'SELECT avatar_url FROM users WHERE id = $1', + [userId] + ); + if (res.rows.length === 0) + return (null); + return (res.rows[0].avatar_url); +} + +function deleteNonDefault(curAvatar) +{ + if (curAvatar && curAvatar !== DEFAULT_AVATAR) + { + const fileName = path.basename(curAvatar); + const filePath = path.join(AVATAR_DIR, fileName); + + if (fs.existsSync(filePath)) + fs.unlinkSync(filePath); + } +} + +async function getAvatarUrl(userId) +{ + try + { + const result = await query + ( + 'SELECT avatar_url FROM users WHERE id = $1', + [userId] + ); + + if (result.rows.length === 0) + return ({status: 404, data: {error: 'User not found'}}); + + return ({status: 200, data: {avatar_url: result.rows[0].avatar_url}}); + } + catch (err) + { + console.error('Get avatar error:', err); + return ({status: 500, data: {error: 'Server error'}}); + } +} + +export default +{ + uploadAvatar, + deleteAvatar, + getAvatarUrl, + AVATAR_DIR, + DEFAULT_AVATAR +}; diff --git a/srcs/backend/services/game_room.js b/srcs/backend/services/game_room.js index e6e76de..393858d 100644 --- a/srcs/backend/services/game_room.js +++ b/srcs/backend/services/game_room.js @@ -1,15 +1,22 @@ import {query} from '../db.js'; -// Creer la room avec comme seul parametre le nom -// max_players, status et ses autres variables sont aux valeurs definis dans db.js -async function createRoom(name) +// Create the room with name as the only parameter +// max_players, status and the other variables have their default values defined in db.js +async function createRoom(name, userId) { const result = await query ( `INSERT INTO game_rooms (name) VALUES ($1) RETURNING *`, [name] ); - return (result.rows[0]); + const room = result.rows[0]; + + await query + ( + 'INSERT INTO game_players (room_id, user_id) VALUES ($1, $2)', + [room.id, userId] + ); + return (room); } async function getRoomById(roomId) @@ -22,9 +29,7 @@ async function getRoomById(roomId) return (result.rows[0]); } -//Liste toutes les rooms en attente -//ainsi que le nombre de joueurs dans chaque room -//utile pour montrer toutes les rooms joignables +// List all the waiting rooms and the player amount in each of them async function listActiveRooms() { const result = await query @@ -87,9 +92,8 @@ async function leaveRoom(roomId, userId) } } -//Renvoie la liste des joueurs trie selon leur score -//Cette liste donne egalement l'info sur qui dessine actuellement -//Utile pour le jeu en lui meme et le scoreboard de la game +// List the players in the room and their scores +// Useful for the scoreboard and also tell which player is currently drawing async function getRoomPlayers(roomId) { const result = await query