diff --git a/srcs/backend/avatar/default.png b/srcs/backend/avatar/default.png new file mode 100644 index 0000000..28f6a85 Binary files /dev/null and b/srcs/backend/avatar/default.png differ diff --git a/srcs/backend/index.js b/srcs/backend/index.js index 790cf8d..608d53b 100644 --- a/srcs/backend/index.js +++ b/srcs/backend/index.js @@ -5,8 +5,10 @@ import {Server} from 'socket.io'; import authRouter from './routes/auth.js'; import chatRouter from './routes/global_chat.js'; import gameRoomRouter from './routes/game_room.js'; +// import avatarRouter from './routes/avatar.js'; import {waitForDb, createTables, ensureOauthClient} from './db.js'; import setupSocketIO from './services/socket.js'; +// import avatarService from './services/avatar.js'; const app = express(); const server = http.createServer(app); @@ -36,9 +38,11 @@ async function startServer() console.warn('OAuth client might already exist or failed to register:', e.message); } + // app.use('/avatar', express.static(avatarService.AVATAR_DIR)); app.use('/api/auth', authRouter); app.use('/api/global_chat', chatRouter); app.use('/api/rooms', gameRoomRouter); + // app.use('/api/avatar', avatarRouter); app.get('/api', (req, res) => 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