mirror of
https://github.com/cassoule/flopobot_v2.git
synced 2026-03-18 21:40:27 +01:00
feat: new backend handled login
This commit is contained in:
72
package-lock.json
generated
72
package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
63
src/server/middleware/auth.js
Normal file
63
src/server/middleware/auth.js
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
116
src/server/routes/auth.js
Normal file
116
src/server/routes/auth.js
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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" });
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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." });
|
||||
|
||||
@@ -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) ---
|
||||
|
||||
Reference in New Issue
Block a user