diff --git a/package-lock.json b/package-lock.json index 26d4997..d035ddb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "discord.js": "^14.25.1", "dotenv": "^16.0.3", "express": "^4.18.2", + "jsonwebtoken": "^9.0.3", "node-cron": "^3.0.3", "openai": "^4.104.0", "pnpm": "^10.29.2", @@ -2729,6 +2730,34 @@ "dev": true, "license": "MIT" }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -2796,6 +2825,42 @@ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2803,6 +2868,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/lodash.snakecase": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", @@ -3601,7 +3672,6 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" diff --git a/package.json b/package.json index a009a26..e539f23 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "discord.js": "^14.25.1", "dotenv": "^16.0.3", "express": "^4.18.2", + "jsonwebtoken": "^9.0.3", "node-cron": "^3.0.3", "openai": "^4.104.0", "pnpm": "^10.29.2", diff --git a/src/server/app.js b/src/server/app.js index ff182c6..9b69e21 100644 --- a/src/server/app.js +++ b/src/server/app.js @@ -12,6 +12,7 @@ import { getSocketIo } from "./socket.js"; import { blackjackRoutes } from "./routes/blackjack.js"; import { marketRoutes } from "./routes/market.js"; import { monkeRoutes } from "./routes/monke.js"; +import { authRoutes } from "./routes/auth.js"; // --- EXPRESS APP INITIALIZATION --- const app = express(); @@ -25,7 +26,7 @@ app.use((req, res, next) => { res.header("Access-Control-Allow-Origin", FLAPI_URL); res.header( "Access-Control-Allow-Headers", - "Content-Type, X-API-Key, ngrok-skip-browser-warning, Cache-Control, Pragma, Expires", + "Content-Type, Authorization, X-API-Key, ngrok-skip-browser-warning, Cache-Control, Pragma, Expires", ); next(); }); @@ -48,6 +49,9 @@ app.use("/public", express.static("public")); // --- API ROUTES --- +// Auth routes (Discord OAuth2, no client/io needed) +app.use("/api/auth", authRoutes()); + // General API routes (users, polls, etc.) app.use("/api", apiRoutes(client, io)); diff --git a/src/server/middleware/auth.js b/src/server/middleware/auth.js new file mode 100644 index 0000000..717bf1d --- /dev/null +++ b/src/server/middleware/auth.js @@ -0,0 +1,63 @@ +import jwt from "jsonwebtoken"; + +const JWT_SECRET = process.env.JWT_SECRET; + +/** + * Middleware that requires a valid JWT token in the Authorization header. + * Sets req.userId to the authenticated Discord user ID. + */ +export function requireAuth(req, res, next) { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return res.status(401).json({ error: "Authentication required." }); + } + + const token = authHeader.split("Bearer ")[1]; + try { + const payload = jwt.verify(token, JWT_SECRET); + req.userId = payload.discordId; + next(); + } catch (err) { + return res.status(401).json({ error: "Invalid or expired token." }); + } +} + +/** + * Optional auth middleware - attaches userId if token is present, but doesn't block. + * Useful for routes that work for both authenticated and unauthenticated users. + */ +export function optionalAuth(req, res, next) { + const authHeader = req.headers.authorization; + if (authHeader && authHeader.startsWith("Bearer ")) { + const token = authHeader.split("Bearer ")[1]; + try { + const payload = jwt.verify(token, JWT_SECRET); + req.userId = payload.discordId; + } catch { + // Token invalid, continue without userId + } + } + next(); +} + +/** + * Signs a JWT token for a given Discord user ID. + * @param {string} discordId - The Discord user ID. + * @returns {string} The signed JWT token. + */ +export function signToken(discordId) { + return jwt.sign({ discordId }, JWT_SECRET, { expiresIn: "7d" }); +} + +/** + * Verifies a JWT token and returns the payload. + * @param {string} token - The JWT token to verify. + * @returns {object|null} The decoded payload or null if invalid. + */ +export function verifyToken(token) { + try { + return jwt.verify(token, JWT_SECRET); + } catch { + return null; + } +} diff --git a/src/server/routes/api.js b/src/server/routes/api.js index 778a170..1dc5ad5 100644 --- a/src/server/routes/api.js +++ b/src/server/routes/api.js @@ -22,6 +22,7 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "disc import { emitDataUpdated, socketEmit, onGameOver } from "../socket.js"; import { handleCaseOpening } from "../../utils/marketNotifs.js"; import { drawCaseContent, drawCaseSkin, getSkinUpgradeProbs } from "../../utils/caseOpening.js"; +import { requireAuth } from "../middleware/auth.js"; // Create a new router instance const router = express.Router(); @@ -59,8 +60,8 @@ export function apiRoutes(client, io) { } }); - router.post("/register-user", async (req, res) => { - const { discordUserId } = req.body; + router.post("/register-user", requireAuth, async (req, res) => { + const discordUserId = req.userId; const discordUser = await client.users.fetch(discordUserId); try { @@ -104,8 +105,9 @@ export function apiRoutes(client, io) { } }); - router.post("/open-case", async (req, res) => { - const { userId, caseType } = req.body; + router.post("/open-case", requireAuth, async (req, res) => { + const userId = req.userId; + const { caseType } = req.body; let caseTypeVal; switch (caseType) { @@ -231,8 +233,8 @@ export function apiRoutes(client, io) { } }); - router.post("/skin/:uuid/instant-sell", async (req, res) => { - const { userId } = req.body; + router.post("/skin/:uuid/instant-sell", requireAuth, async (req, res) => { + const userId = req.userId; try { const skin = await skinService.getSkin(req.params.uuid); const skinData = skins.find((s) => s.uuid === skin.uuid); @@ -300,8 +302,8 @@ export function apiRoutes(client, io) { } }); - router.post("/skin-upgrade/:uuid", async (req, res) => { - const { userId } = req.body; + router.post("/skin-upgrade/:uuid", requireAuth, async (req, res) => { + const userId = req.userId; try { const skin = await skinService.getSkin(req.params.uuid); const skinData = skins.find((s) => s.uuid === skin.uuid); @@ -518,8 +520,8 @@ export function apiRoutes(client, io) { } }); - router.get("/user/:id/daily", async (req, res) => { - const { id } = req.params; + router.get("/user/:id/daily", requireAuth, async (req, res) => { + const id = req.userId; try { const akhy = await userService.getUser(id); if (!akhy) return res.status(404).json({ message: "Utilisateur introuvable" }); @@ -551,9 +553,9 @@ export function apiRoutes(client, io) { res.json({ activePolls }); }); - router.post("/timedout", async (req, res) => { + router.post("/timedout", requireAuth, async (req, res) => { try { - const { userId } = req.body; + const userId = req.userId; const guild = await client.guilds.fetch(process.env.GUILD_ID); const member = await guild.members.fetch(userId); res.status(200).json({ isTimedOut: member?.isCommunicationDisabled() || false }); @@ -564,8 +566,9 @@ export function apiRoutes(client, io) { // --- Shop & Interaction Routes --- - router.post("/change-nickname", async (req, res) => { - const { userId, nickname, commandUserId } = req.body; + router.post("/change-nickname", requireAuth, async (req, res) => { + const { userId, nickname } = req.body; + const commandUserId = req.userId; const commandUser = await userService.getUser(commandUserId); if (!commandUser) return res.status(404).json({ message: "Command user not found." }); if (commandUser.coins < 1000) return res.status(403).json({ message: "Pas assez de FlopoCoins (1000 requis)." }); @@ -614,8 +617,9 @@ export function apiRoutes(client, io) { } }); - router.post("/spam-ping", async (req, res) => { - const { userId, commandUserId } = req.body; + router.post("/spam-ping", requireAuth, async (req, res) => { + const { userId } = req.body; + const commandUserId = req.userId; const user = await userService.getUser(userId); const commandUser = await userService.getUser(commandUserId); @@ -671,8 +675,9 @@ export function apiRoutes(client, io) { res.status(200).json({ slowmodes: activeSlowmodes }); }); - router.post("/slowmode", async (req, res) => { - let { userId, commandUserId } = req.body; + router.post("/slowmode", requireAuth, async (req, res) => { + let { userId } = req.body; + const commandUserId = req.userId; const user = await userService.getUser(userId); const commandUser = await userService.getUser(commandUserId); @@ -761,8 +766,9 @@ export function apiRoutes(client, io) { // --- Time-Out Route --- - router.post("/timeout", async (req, res) => { - let { userId, commandUserId } = req.body; + router.post("/timeout", requireAuth, async (req, res) => { + let { userId } = req.body; + const commandUserId = req.userId; const user = await userService.getUser(userId); const commandUser = await userService.getUser(commandUserId); @@ -877,8 +883,9 @@ export function apiRoutes(client, io) { res.status(200).json({ predis: reversedPredis }); }); - router.post("/start-predi", async (req, res) => { - let { commandUserId, label, options, closingTime, payoutTime } = req.body; + router.post("/start-predi", requireAuth, async (req, res) => { + let { label, options, closingTime, payoutTime } = req.body; + const commandUserId = req.userId; const commandUser = await userService.getUser(commandUserId); @@ -973,8 +980,9 @@ export function apiRoutes(client, io) { return res.status(200).json({ message: `Ta prédi '${label}' a commencée !` }); }); - router.post("/vote-predi", async (req, res) => { - const { commandUserId, predi, amount, option } = req.body; + router.post("/vote-predi", requireAuth, async (req, res) => { + const { predi, amount, option } = req.body; + const commandUserId = req.userId; let warning = false; @@ -1041,8 +1049,9 @@ export function apiRoutes(client, io) { return res.status(200).send({ message: `Vote enregistré!` }); }); - router.post("/end-predi", async (req, res) => { - const { commandUserId, predi, confirm, winningOption } = req.body; + router.post("/end-predi", requireAuth, async (req, res) => { + const { predi, confirm, winningOption } = req.body; + const commandUserId = req.userId; const commandUser = await userService.getUser(commandUserId); if (!commandUser) return res.status(403).send({ message: "Oups, je ne te connais pas" }); @@ -1156,8 +1165,9 @@ export function apiRoutes(client, io) { return res.status(200).json({ message: "Prédi close" }); }); - router.post("/snake/reward", async (req, res) => { - const { discordId, score, isWin } = req.body; + router.post("/snake/reward", requireAuth, async (req, res) => { + const discordId = req.userId; + const { score, isWin } = req.body; console.log(`[SNAKE][SOLO]${discordId}: score=${score}, isWin=${isWin}`); try { const user = await userService.getUser(discordId); @@ -1181,8 +1191,9 @@ export function apiRoutes(client, io) { } }); - router.post("/queue/leave", async (req, res) => { - const { discordId, game, reason } = req.body; + router.post("/queue/leave", requireAuth, async (req, res) => { + const discordId = req.userId; + const { game, reason } = req.body; if (game === "snake" && (reason === "beforeunload" || reason === "route-leave")) { const lobby = Object.values(activeSnakeGames).find( (l) => (l.p1.id === discordId || l.p2.id === discordId) && !l.gameOver, @@ -1240,11 +1251,12 @@ export function apiRoutes(client, io) { res.json({ offers: COIN_OFFERS }); }); - router.post("/create-checkout-session", async (req, res) => { - const { userId, offerId } = req.body; + router.post("/create-checkout-session", requireAuth, async (req, res) => { + const userId = req.userId; + const { offerId } = req.body; - if (!userId || !offerId) { - return res.status(400).json({ error: "Missing required fields: userId, offerId" }); + if (!offerId) { + return res.status(400).json({ error: "Missing required field: offerId" }); } const offer = COIN_OFFERS.find((o) => o.id === offerId); diff --git a/src/server/routes/auth.js b/src/server/routes/auth.js new file mode 100644 index 0000000..0d0397c --- /dev/null +++ b/src/server/routes/auth.js @@ -0,0 +1,116 @@ +import express from "express"; +import axios from "axios"; +import { signToken } from "../middleware/auth.js"; +import * as userService from "../../services/user.service.js"; + +const router = express.Router(); + +const DISCORD_API = "https://discord.com/api/v10"; +const DISCORD_CLIENT_ID = process.env.APP_ID; +const DISCORD_CLIENT_SECRET = process.env.DISCORD_CLIENT_SECRET; +const API_URL = process.env.DEV_SITE === "true" ? process.env.API_URL_DEV : process.env.API_URL; +const FLAPI_URL = process.env.DEV_SITE === "true" ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL; +const REDIRECT_URI = `${API_URL}/api/auth/discord/callback`; + +/** + * GET /api/auth/discord + * Redirects the user to Discord's OAuth2 authorization page. + */ +router.get("/discord", (req, res) => { + const params = new URLSearchParams({ + client_id: DISCORD_CLIENT_ID, + redirect_uri: REDIRECT_URI, + response_type: "code", + scope: "identify", + }); + console.log("Redirecting to Discord OAuth2 with params:", params.toString()); + res.redirect(`${DISCORD_API}/oauth2/authorize?${params.toString()}`); +}); + +/** + * GET /api/auth/discord/callback + * Handles the OAuth2 callback from Discord. + * Exchanges the authorization code for tokens, fetches user info, + * creates a JWT, and redirects the user back to the frontend. + */ +router.get("/discord/callback", async (req, res) => { + const { code } = req.query; + if (!code) { + return res.status(400).json({ error: "Missing authorization code." }); + } + + try { + // Exchange the authorization code for an access token + const tokenResponse = await axios.post( + `${DISCORD_API}/oauth2/token`, + new URLSearchParams({ + client_id: DISCORD_CLIENT_ID, + client_secret: DISCORD_CLIENT_SECRET, + grant_type: "authorization_code", + code, + redirect_uri: REDIRECT_URI, + }), + { headers: { "Content-Type": "application/x-www-form-urlencoded" } }, + ); + + const { access_token } = tokenResponse.data; + + // Fetch the user's Discord profile + const userResponse = await axios.get(`${DISCORD_API}/users/@me`, { + headers: { Authorization: `Bearer ${access_token}` }, + }); + + const discordUser = userResponse.data; + + // Ensure the user exists in our database + const existingUser = await userService.getUser(discordUser.id); + if (existingUser) { + // Update avatar if it changed + const avatarUrl = discordUser.avatar + ? `https://cdn.discordapp.com/avatars/${discordUser.id}/${discordUser.avatar}.png?size=256` + : null; + if (avatarUrl) { + await userService.updateUserAvatar(discordUser.id, avatarUrl); + } + } + + // Sign a JWT with the verified Discord ID + const token = signToken(discordUser.id); + + // Redirect back to the frontend with the token + res.redirect(`${FLAPI_URL}/auth/callback?token=${token}&discordId=${discordUser.id}`); + } catch (error) { + console.error("Discord OAuth2 error:", error.response?.data || error.message); + res.redirect(`${FLAPI_URL}/auth/callback?error=auth_failed`); + } +}); + +/** + * GET /api/auth/me + * Returns the authenticated user's info. Requires a valid JWT. + */ +router.get("/me", async (req, res) => { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return res.status(401).json({ error: "Authentication required." }); + } + + const token = authHeader.split("Bearer ")[1]; + const { verifyToken } = await import("../middleware/auth.js"); + const payload = verifyToken(token); + if (!payload) { + return res.status(401).json({ error: "Invalid or expired token." }); + } + + const user = await userService.getUser(payload.discordId); + if (!user) { + console.warn("User not found for Discord ID in token:", payload.discordId); + return res.json({discordId: payload.discordId}); + } + + res.json({ user, discordId: user.id }); +}); + +export function authRoutes() { + return router; +} diff --git a/src/server/routes/blackjack.js b/src/server/routes/blackjack.js index 04859cd..3d2716a 100644 --- a/src/server/routes/blackjack.js +++ b/src/server/routes/blackjack.js @@ -20,6 +20,7 @@ import * as logService from "../../services/log.service.js"; import { client } from "../../bot/client.js"; import { emitToast, emitUpdate, emitPlayerUpdate } from "../socket.js"; import { EmbedBuilder, time } from "discord.js"; +import { requireAuth } from "../middleware/auth.js"; export function blackjackRoutes(io) { const router = express.Router(); @@ -123,9 +124,8 @@ export function blackjackRoutes(io) { // --- Public endpoints --- router.get("/", (req, res) => res.status(200).json({ room: snapshot(room) })); - router.post("/join", async (req, res) => { - const { userId } = req.body; - if (!userId) return res.status(400).json({ message: "userId required" }); + router.post("/join", requireAuth, async (req, res) => { + const userId = req.userId; if (room.players[userId]) return res.status(200).json({ message: "Already here" }); const user = await client.users.fetch(userId); @@ -191,9 +191,9 @@ export function blackjackRoutes(io) { return res.status(200).json({ message: "joined" }); }); - router.post("/leave", async (req, res) => { - const { userId } = req.body; - if (!userId || !room.players[userId]) return res.status(403).json({ message: "not in room" }); + router.post("/leave", requireAuth, async (req, res) => { + const userId = req.userId; + if (!room.players[userId]) return res.status(403).json({ message: "not in room" }); try { const guild = await client.guilds.fetch(process.env.GUILD_ID); @@ -238,8 +238,9 @@ export function blackjackRoutes(io) { } }); - router.post("/bet", async (req, res) => { - const { userId, amount } = req.body; + router.post("/bet", requireAuth, async (req, res) => { + const userId = req.userId; + const { amount } = req.body; const p = room.players[userId]; if (!p) return res.status(404).json({ message: "not in room" }); if (room.status !== "betting") return res.status(403).json({ message: "betting-closed" }); @@ -270,8 +271,8 @@ export function blackjackRoutes(io) { return res.status(200).json({ message: "bet-accepted" }); }); - router.post("/action/:action", async (req, res) => { - const { userId } = req.body; + router.post("/action/:action", requireAuth, async (req, res) => { + const userId = req.userId; const action = req.params.action; const p = room.players[userId]; if (!p) return res.status(404).json({ message: "not in room" }); diff --git a/src/server/routes/market.js b/src/server/routes/market.js index 6303630..60f7912 100644 --- a/src/server/routes/market.js +++ b/src/server/routes/market.js @@ -11,6 +11,7 @@ import * as logService from "../../services/log.service.js"; import * as marketService from "../../services/market.service.js"; import { emitMarketUpdate } from "../socket.js"; import { handleNewMarketOffer, handleNewMarketOfferBid } from "../../utils/marketNotifs.js"; +import { requireAuth } from "../middleware/auth.js"; // Create a new router instance const router = express.Router(); @@ -63,8 +64,9 @@ export function marketRoutes(client, io) { } }); - router.post("/place-offer", async (req, res) => { - const { seller_id, skin_uuid, starting_price, delay, duration, timestamp } = req.body; + router.post("/place-offer", requireAuth, async (req, res) => { + const seller_id = req.userId; + const { skin_uuid, starting_price, delay, duration, timestamp } = req.body; const now = Date.now(); try { const skin = await skinService.getSkin(skin_uuid); @@ -104,8 +106,9 @@ export function marketRoutes(client, io) { } }); - router.post("/offers/:id/place-bid", async (req, res) => { - const { buyer_id, bid_amount, timestamp } = req.body; + router.post("/offers/:id/place-bid", requireAuth, async (req, res) => { + const buyer_id = req.userId; + const { bid_amount, timestamp } = req.body; try { const offer = await marketService.getMarketOfferById(req.params.id); if (!offer) return res.status(404).send({ error: "Offer not found" }); diff --git a/src/server/routes/monke.js b/src/server/routes/monke.js index 424f3a7..6ef00f6 100644 --- a/src/server/routes/monke.js +++ b/src/server/routes/monke.js @@ -5,6 +5,7 @@ import { socketEmit } from "../socket.js"; import * as userService from "../../services/user.service.js"; import * as logService from "../../services/log.service.js"; import { init } from "openai/_shims/index.mjs"; +import { requireAuth } from "../middleware/auth.js"; const router = express.Router(); @@ -29,11 +30,9 @@ export function monkeRoutes(client, io) { return res.status(200).json({ userGamePath }); }); - router.post("/:userId/start", async (req, res) => { - const { userId } = req.params; + router.post("/:userId/start", requireAuth, async (req, res) => { + const userId = req.userId; const { initialBet } = req.body; - - if (!userId) return res.status(400).json({ error: "User ID is required" }); const user = await userService.getUser(userId); if (!user) return res.status(404).json({ error: "User not found" }); if (!initialBet) return res.status(400).json({ error: "Initial bet is required" }); @@ -61,11 +60,9 @@ export function monkeRoutes(client, io) { return res.status(200).json({ message: "Monke game started", userGamePath: monkePaths[userId] }); }); - router.post("/:userId/play", async (req, res) => { - const { userId } = req.params; + router.post("/:userId/play", requireAuth, async (req, res) => { + const userId = req.userId; const { choice, step } = req.body; - - if (!userId) return res.status(400).json({ error: "User ID is required" }); const user = await userService.getUser(userId); if (!user) return res.status(404).json({ error: "User not found" }); if (!monkePaths[userId]) return res.status(400).json({ error: "No active game found for this user" }); @@ -105,9 +102,8 @@ export function monkeRoutes(client, io) { } }); - router.post("/:userId/stop", async (req, res) => { - const { userId } = req.params; - if (!userId) return res.status(400).json({ error: "User ID is required" }); + router.post("/:userId/stop", requireAuth, async (req, res) => { + const userId = req.userId; const user = await userService.getUser(userId); if (!user) return res.status(404).json({ error: "User not found" }); if (!monkePaths[userId]) return res.status(400).json({ error: "No active game found for this user" }); diff --git a/src/server/routes/poker.js b/src/server/routes/poker.js index fd217ad..f36db1e 100644 --- a/src/server/routes/poker.js +++ b/src/server/routes/poker.js @@ -17,6 +17,7 @@ import { client } from "../../bot/client.js"; import { emitPokerToast, emitPokerUpdate } from "../socket.js"; import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js"; import { formatAmount } from "../../utils/index.js"; +import { requireAuth } from "../middleware/auth.js"; const { Hand } = pkg; @@ -44,9 +45,9 @@ export function pokerRoutes(client, io) { } }); - router.post("/create", async (req, res) => { - const { creatorId, minBet, fakeMoney } = req.body; - if (!creatorId) return res.status(400).json({ message: "Creator ID is required." }); + router.post("/create", requireAuth, async (req, res) => { + const creatorId = req.userId; + const { minBet, fakeMoney } = req.body; if (Object.values(pokerRooms).some((room) => room.host_id === creatorId || room.players[creatorId])) { return res.status(403).json({ message: "You are already in a poker room." }); @@ -125,9 +126,10 @@ export function pokerRoutes(client, io) { res.status(201).json({ roomId: id }); }); - router.post("/join", async (req, res) => { - const { userId, roomId } = req.body; - if (!userId || !roomId) return res.status(400).json({ message: "User ID and Room ID are required." }); + router.post("/join", requireAuth, async (req, res) => { + const userId = req.userId; + const { roomId } = req.body; + if (!roomId) return res.status(400).json({ message: "Room ID is required." }); if (!pokerRooms[roomId]) return res.status(404).json({ message: "Room not found." }); if (Object.values(pokerRooms).some((r) => r.players[userId] || r.queue[userId])) { return res.status(403).json({ message: "You are already in a room or queue." }); @@ -143,8 +145,9 @@ export function pokerRoutes(client, io) { res.status(200).json({ message: "Successfully joined." }); }); - router.post("/accept", async (req, res) => { - const { hostId, playerId, roomId } = req.body; + router.post("/accept", requireAuth, async (req, res) => { + const hostId = req.userId; + const { playerId, roomId } = req.body; const room = pokerRooms[roomId]; if (!room || room.host_id !== hostId || !room.queue[playerId]) { return res.status(403).json({ message: "Unauthorized or player not in queue." }); @@ -172,8 +175,9 @@ export function pokerRoutes(client, io) { res.status(200).json({ message: "Player accepted." }); }); - router.post("/leave", async (req, res) => { - const { userId, roomId } = req.body; + router.post("/leave", requireAuth, async (req, res) => { + const userId = req.userId; + const { roomId } = req.body; if (!pokerRooms[roomId]) return res.status(404).send({ message: "Table introuvable" }); if (!pokerRooms[roomId].players[userId]) return res.status(404).send({ message: "Joueur introuvable" }); @@ -223,8 +227,9 @@ export function pokerRoutes(client, io) { return res.status(200); }); - router.post("/kick", async (req, res) => { - const { commandUserId, userId, roomId } = req.body; + router.post("/kick", requireAuth, async (req, res) => { + const commandUserId = req.userId; + const { userId, roomId } = req.body; if (!pokerRooms[roomId]) return res.status(404).send({ message: "Table introuvable" }); if (!pokerRooms[roomId].players[commandUserId]) return res.status(404).send({ message: "Joueur introuvable" }); @@ -265,7 +270,7 @@ export function pokerRoutes(client, io) { // --- Game Action Endpoints --- - router.post("/start", async (req, res) => { + router.post("/start", requireAuth, async (req, res) => { const { roomId } = req.body; const room = pokerRooms[roomId]; if (!room) return res.status(404).json({ message: "Room not found." }); @@ -276,7 +281,7 @@ export function pokerRoutes(client, io) { }); // NEW: Endpoint to start the next hand - router.post("/next-hand", async (req, res) => { + router.post("/next-hand", requireAuth, async (req, res) => { const { roomId } = req.body; const room = pokerRooms[roomId]; if (!room || !room.waiting_for_restart) { @@ -286,8 +291,9 @@ export function pokerRoutes(client, io) { res.status(200).json({ message: "Next hand started." }); }); - router.post("/action/:action", async (req, res) => { - const { playerId, amount, roomId } = req.body; + router.post("/action/:action", requireAuth, async (req, res) => { + const playerId = req.userId; + const { amount, roomId } = req.body; const { action } = req.params; const room = pokerRooms[roomId]; diff --git a/src/server/routes/solitaire.js b/src/server/routes/solitaire.js index 97a8a55..e9d87fa 100644 --- a/src/server/routes/solitaire.js +++ b/src/server/routes/solitaire.js @@ -23,6 +23,7 @@ import * as userService from "../../services/user.service.js"; import * as logService from "../../services/log.service.js"; import * as solitaireService from "../../services/solitaire.service.js"; import { socketEmit } from "../socket.js"; +import { requireAuth } from "../middleware/auth.js"; // Create a new router instance const router = express.Router(); @@ -36,9 +37,9 @@ const router = express.Router(); export function solitaireRoutes(client, io) { // --- Game Initialization Endpoints --- - router.post("/start", (req, res) => { - const { userId, userSeed, hardMode } = req.body; - if (!userId) return res.status(400).json({ error: "User ID is required." }); + router.post("/start", requireAuth, (req, res) => { + const userId = req.userId; + const { userSeed, hardMode } = req.body; // If a game already exists for the user, return it instead of creating a new one. if (activeSolitaireGames[userId] && !activeSolitaireGames[userId].isSOTD) { @@ -78,8 +79,8 @@ export function solitaireRoutes(client, io) { res.json({ success: true, gameState }); }); - router.post("/start/sotd", async (req, res) => { - const { userId } = req.body; + router.post("/start/sotd", requireAuth, async (req, res) => { + const userId = req.userId; /*if (!userId || !getUser.get(userId)) { return res.status(404).json({ error: 'User not found.' }); }*/ @@ -138,16 +139,17 @@ export function solitaireRoutes(client, io) { } }); - router.post("/reset", (req, res) => { - const { userId } = req.body; + router.post("/reset", requireAuth, (req, res) => { + const userId = req.userId; if (activeSolitaireGames[userId]) { delete activeSolitaireGames[userId]; } res.json({ success: true, message: "Game reset." }); }); - router.post("/move", async (req, res) => { - const { userId, ...moveData } = req.body; + router.post("/move", requireAuth, async (req, res) => { + const userId = req.userId; + const { ...moveData } = req.body; const gameState = activeSolitaireGames[userId]; if (!gameState) return res.status(404).json({ error: "Game not found." }); @@ -176,8 +178,8 @@ export function solitaireRoutes(client, io) { } }); - router.post("/draw", (req, res) => { - const { userId } = req.body; + router.post("/draw", requireAuth, (req, res) => { + const userId = req.userId; const gameState = activeSolitaireGames[userId]; if (!gameState) return res.status(404).json({ error: "Game not found." }); @@ -192,8 +194,8 @@ export function solitaireRoutes(client, io) { res.json({ success: true, gameState }); }); - router.post("/undo", (req, res) => { - const { userId } = req.body; + router.post("/undo", requireAuth, (req, res) => { + const userId = req.userId; const gameState = activeSolitaireGames[userId]; if (!gameState) return res.status(404).json({ error: "Game not found." }); diff --git a/src/server/socket.js b/src/server/socket.js index f07a1fc..78c3f92 100644 --- a/src/server/socket.js +++ b/src/server/socket.js @@ -16,6 +16,7 @@ import { formatConnect4BoardForDiscord, } from "../game/various.js"; import { eloHandler } from "../game/elo.js"; +import { verifyToken } from "./middleware/auth.js"; // --- Module-level State --- let io; @@ -25,10 +26,23 @@ let io; export function initializeSocket(server, client) { io = server; + // Authenticate socket connections via JWT (optional - allows unauthenticated connections for health checks) + io.use((socket, next) => { + const token = socket.handshake.auth?.token; + if (token) { + const payload = verifyToken(token); + if (!payload) { + return next(new Error("Invalid or expired token")); + } + socket.userId = payload.discordId; + } + next(); + }); + io.on("connection", (socket) => { - socket.on("user-connected", async (userId) => { - if (!userId) return; - await refreshQueuesForUser(userId, client); + socket.on("user-connected", async () => { + if (!socket.userId) return; + await refreshQueuesForUser(socket.userId, client); }); registerTicTacToeEvents(socket, client); @@ -39,14 +53,13 @@ export function initializeSocket(server, client) { io.emit("blackjack:chat", data); }); - socket.on("tictactoe:queue:leave", async ({ discordId }) => await refreshQueuesForUser(discordId, client)); - socket.on("connect4:queue:leave", async ({ discordId }) => await refreshQueuesForUser(discordId, client)); - socket.on("snake:queue:leave", async ({ discordId }) => await refreshQueuesForUser(discordId, client)); + socket.on("tictactoe:queue:leave", async () => await refreshQueuesForUser(socket.userId, client)); + socket.on("connect4:queue:leave", async () => await refreshQueuesForUser(socket.userId, client)); + socket.on("snake:queue:leave", async () => await refreshQueuesForUser(socket.userId, client)); // catch tab kills / network drops socket.on("disconnecting", async () => { - const discordId = socket.handshake.auth?.discordId; // or your mapping - await refreshQueuesForUser(discordId, client); + await refreshQueuesForUser(socket.userId, client); }); socket.on("disconnect", () => { @@ -64,24 +77,24 @@ export function getSocketIo() { // --- Event Registration --- function registerTicTacToeEvents(socket, client) { - socket.on("tictactoeconnection", (e) => refreshQueuesForUser(e.id, client)); - socket.on("tictactoequeue", (e) => onQueueJoin(client, "tictactoe", e.playerId)); - socket.on("tictactoeplaying", (e) => onTicTacToeMove(client, e)); - socket.on("tictactoegameOver", (e) => onGameOver(client, "tictactoe", e.playerId, e.winner)); + socket.on("tictactoeconnection", () => refreshQueuesForUser(socket.userId, client)); + socket.on("tictactoequeue", () => onQueueJoin(client, "tictactoe", socket.userId)); + socket.on("tictactoeplaying", (e) => onTicTacToeMove(client, { ...e, playerId: socket.userId })); + socket.on("tictactoegameOver", (e) => onGameOver(client, "tictactoe", socket.userId, e.winner)); } function registerConnect4Events(socket, client) { - socket.on("connect4connection", (e) => refreshQueuesForUser(e.id, client)); - socket.on("connect4queue", (e) => onQueueJoin(client, "connect4", e.playerId)); - socket.on("connect4playing", (e) => onConnect4Move(client, e)); - socket.on("connect4NoTime", (e) => onGameOver(client, "connect4", e.playerId, e.winner, "(temps écoulé)")); + socket.on("connect4connection", () => refreshQueuesForUser(socket.userId, client)); + socket.on("connect4queue", () => onQueueJoin(client, "connect4", socket.userId)); + socket.on("connect4playing", (e) => onConnect4Move(client, { ...e, playerId: socket.userId })); + socket.on("connect4NoTime", (e) => onGameOver(client, "connect4", socket.userId, e.winner, "(temps écoulé)")); } function registerSnakeEvents(socket, client) { - socket.on("snakeconnection", (e) => refreshQueuesForUser(e.id, client)); - socket.on("snakequeue", (e) => onQueueJoin(client, "snake", e.playerId)); - socket.on("snakegamestate", (e) => onSnakeGameStateUpdate(client, e)); - socket.on("snakegameOver", (e) => onGameOver(client, "snake", e.playerId, e.winner)); + socket.on("snakeconnection", () => refreshQueuesForUser(socket.userId, client)); + socket.on("snakequeue", () => onQueueJoin(client, "snake", socket.userId)); + socket.on("snakegamestate", (e) => onSnakeGameStateUpdate(client, { ...e, playerId: socket.userId })); + socket.on("snakegameOver", (e) => onGameOver(client, "snake", socket.userId, e.winner)); } // --- Core Handlers (Preserving Original Logic) ---