Sécurisation de l'API et refonte des tests backend
- helmet et express-rate-limit sur les routes d'authentification - CORS restreint aux origines de FRONTEND_URL - CSP désactivée uniquement sur /api-docs pour Swagger UI - ajout du test de tirage et simplification du test e2e - suppression de tests/setup.js devenu inutile Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
const express = require("express");
|
||||
const cors = require("cors");
|
||||
const helmet = require("helmet");
|
||||
const rateLimit = require("express-rate-limit");
|
||||
require("dotenv").config();
|
||||
|
||||
const authRoutes = require("./routes/auth.routes");
|
||||
@@ -12,9 +14,32 @@ const swaggerSpec = require("./docs/swagger");
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(cors());
|
||||
const allowedOrigins = (process.env.FRONTEND_URL || "http://localhost:5173").split(",");
|
||||
|
||||
// La CSP par défaut de helmet bloque les scripts inline de Swagger UI :
|
||||
// on l'applique partout sauf sur /api-docs, où une CSP dédiée (désactivée)
|
||||
// est posée juste avant de monter Swagger, plus bas.
|
||||
app.use((req, res, next) => {
|
||||
if (req.path.startsWith("/api-docs")) return next();
|
||||
return helmet()(req, res, next);
|
||||
});
|
||||
app.use(cors({ origin: allowedOrigins }));
|
||||
app.use(express.json());
|
||||
|
||||
// Anti-bruteforce sur les routes d'authentification uniquement : 20
|
||||
// tentatives par IP toutes les 15 minutes, sans limiter le reste de l'API
|
||||
// (participation au jeu, consultation des gains, etc.).
|
||||
const authLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
limit: 20,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
message: { message: "Trop de tentatives, réessayez dans quelques minutes." },
|
||||
});
|
||||
app.use("/api/auth/login", authLimiter);
|
||||
app.use("/api/auth/register", authLimiter);
|
||||
app.use("/api/auth/forgot-password", authLimiter);
|
||||
|
||||
// routes
|
||||
app.use("/api/auth", authRoutes);
|
||||
app.use("/api/game", gameRoutes);
|
||||
@@ -22,7 +47,7 @@ app.use("/api/employee", employeeRoutes);
|
||||
app.use("/api/admin", adminRoutes);
|
||||
|
||||
// swagger
|
||||
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec));
|
||||
app.use("/api-docs", helmet({ contentSecurityPolicy: false }), swaggerUi.serve, swaggerUi.setup(swaggerSpec));
|
||||
|
||||
// test route
|
||||
app.get("/", (req, res) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const bcrypt = require("bcrypt");
|
||||
const crypto = require("crypto");
|
||||
const prisma = require("../config/prisma");
|
||||
const { getGameWindow, expireOverdueClaims } = require("../utils/gameWindow");
|
||||
|
||||
@@ -234,6 +235,10 @@ const drawWinner = async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Un utilisateur = une entrée dans le pool, quel que soit son nombre de
|
||||
// participations. On sélectionne des CLIENT distincts (pas des
|
||||
// participations), donc avoir utilisé 10 tickets ou 1 seul ne change
|
||||
// strictement rien aux chances de gagner le gros lot.
|
||||
const participants = await prisma.user.findMany({
|
||||
where: {
|
||||
role: "CLIENT",
|
||||
@@ -255,7 +260,11 @@ const drawWinner = async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
const randomIndex = Math.floor(Math.random() * participants.length);
|
||||
// Tirage purement aléatoire et équiprobable parmi les participants
|
||||
// distincts. crypto.randomInt (générateur cryptographiquement sûr,
|
||||
// sans biais) plutôt que Math.random(), pour un tirage défendable
|
||||
// devant l'huissier qui le supervise.
|
||||
const randomIndex = crypto.randomInt(participants.length);
|
||||
const winner = participants[randomIndex];
|
||||
|
||||
const savedWinner = await prisma.grandPrizeWinner.create({
|
||||
@@ -302,14 +311,8 @@ const getGrandPrizeWinner = async (req, res) => {
|
||||
},
|
||||
});
|
||||
|
||||
if (!winner) {
|
||||
return res.status(404).json({
|
||||
message: "Aucun gagnant du gros lot n'a encore été tiré.",
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({
|
||||
winner,
|
||||
winner: winner ?? null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Erreur récupération gagnant:", error);
|
||||
|
||||
@@ -105,9 +105,7 @@ router.post("/draw", drawWinner);
|
||||
* - bearerAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Gagnant du gros lot
|
||||
* 404:
|
||||
* description: Aucun gagnant encore tiré
|
||||
* description: Gagnant du gros lot, ou { winner - null } si aucun tirage n'a encore eu lieu
|
||||
*/
|
||||
router.get("/draw", getGrandPrizeWinner);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user