From f0aa536b7b2e7f2ab461e64770cd8c56f9825672 Mon Sep 17 00:00:00 2001 From: Arthur Dodin Date: Tue, 21 Apr 2026 00:27:15 +0200 Subject: [PATCH 1/3] feat(shotgun): secure password flow and add admin ranking --- backend/src/controllers/event.controller.ts | 167 +++++++++++------- backend/src/routes/event.routes.ts | 19 +- backend/src/services/event.service.ts | 92 +++++----- backend/src/utils/responses.ts | 8 + backend/src/utils/secret.ts | 15 +- docker-compose.yml | 9 +- frontend/src/App.tsx | 4 +- frontend/src/components/Admin/adminEvent.tsx | 2 +- .../src/components/Admin/adminShotgun.tsx | 116 ++++++++++++ frontend/src/components/Admin/adminTeam.tsx | 14 ++ frontend/src/components/navbar.tsx | 19 +- .../src/components/shotgun/shotgunSection.tsx | 30 ++-- frontend/src/interfaces/event.interface.ts | 25 +++ frontend/src/pages/admin.tsx | 13 ++ .../src/services/requests/event.service.ts | 40 +++-- frontend/tsconfig.node.json | 12 +- 16 files changed, 421 insertions(+), 164 deletions(-) create mode 100644 frontend/src/components/Admin/adminShotgun.tsx create mode 100644 frontend/src/interfaces/event.interface.ts diff --git a/backend/src/controllers/event.controller.ts b/backend/src/controllers/event.controller.ts index 72399f4..7a9e5b4 100644 --- a/backend/src/controllers/event.controller.ts +++ b/backend/src/controllers/event.controller.ts @@ -1,166 +1,215 @@ import { Request, Response } from "express"; -import { Accepted, Error, Ok, Unauthorized } from "../utils/responses"; +import { Accepted, Error, Ok, Teapot, Unauthorized } from "../utils/responses"; import * as event_service from "../services/event.service"; import * as team_service from "../services/team.service"; import { Event } from "../schemas/Basic/event.schema"; +import { shotgun_password } from "../utils/secret"; + +type AuthenticatedRequest = Request & { user?: { userId?: number } }; export const checkShotgunStatus = async (req: Request, res: Response) => { - try{ + try { const status = await event_service.getEventsStatus(); - Ok(res, ({data: status?.shotgun_open })); - }catch(error){ - Error(res, {msg :"Error while catching shotgun status :" + error}) + Ok(res, ({ data: { status: Boolean(status?.shotgun_open), password: Boolean(status?.shotgun_open) ? shotgun_password : "" } })); + + } catch (error) { + Error(res, { msg: "Error while catching shotgun status :" + error }) } }; export const checkPreRegisterStatus = async (req: Request, res: Response) => { - try{ + try { const status = await event_service.getEventsStatus(); - Ok(res, ({data: status?.pre_registration_open})); + Ok(res, ({ data: status?.pre_registration_open })); - }catch(error){ - Error(res, {msg :"Error while catching shotgun status :" + error}) + } catch (error) { + Error(res, { msg: "Error while catching pre-registration status :" + error }) } }; export const checkSDIStatus = async (req: Request, res: Response) => { - try{ + try { const status = await event_service.getEventsStatus(); - Ok(res, ({data: status?.sdi_open})); + Ok(res, ({ data: status?.sdi_open })); - }catch(error){ - Error(res, {msg :"Error while catching SDI status :" + error}) + } catch (error) { + Error(res, { msg: "Error while catching SDI status :" + error }) } }; export const checkWEIStatus = async (req: Request, res: Response) => { - try{ + try { const status = await event_service.getEventsStatus(); - Ok(res, ({data: status?.wei_open})); + Ok(res, ({ data: status?.wei_open })); - }catch(error){ - Error(res, {msg :"Error while catching WEI status :" + error}) + } catch (error) { + Error(res, { msg: "Error while catching WEI status :" + error }) } }; export const checkFoodStatus = async (req: Request, res: Response) => { - try{ + try { const status = await event_service.getEventsStatus(); - Ok(res, ({data: status?.food_open})); + Ok(res, ({ data: status?.food_open })); - }catch(error){ - Error(res, {msg :"Error while catching Food status :" + error}) + } catch (error) { + Error(res, { msg: "Error while catching Food status :" + error }) } }; export const checkChallStatus = async (req: Request, res: Response) => { - try{ + try { const status = await event_service.getEventsStatus(); - Ok(res, ({data: status?.chall_open})); + Ok(res, ({ data: status?.chall_open })); + + } catch (error) { + Error(res, { msg: "Error while catching Challenge status :" + error }) + } +}; - }catch(error){ - Error(res, {msg :"Error while catching Challenge status :" + error}) +export const getShotgunAttempts = async (req: Request, res: Response) => { + try { + const shotgunAttempts = await event_service.getAllTeamShotguns(); + const shotgunAttemptsWithLeaders = await Promise.all( + shotgunAttempts.map(async (attempt) => { + if (!attempt.teamId) { + return { ...attempt, leaderCount: 0 }; + } + + const teamUsers = await team_service.getTeamUsers(attempt.teamId); + const leaderCount = teamUsers.filter((user) => user.permission !== "Nouveau").length; + + return { ...attempt, leaderCount }; + }) + ); + + Ok(res, { data: shotgunAttemptsWithLeaders }); + } catch (error) { + Error(res, { msg: "Erreur lors de la récupération des tentatives shotgun : " + error }); } }; export const shotgunAttempt = async (req: Request, res: Response) => { - const userId = req.user.userId; - + const { password } = req.body as { password?: string }; + + const userId = (req as AuthenticatedRequest).user?.userId; + + console.debug("Received shotgun attempt with password:", password); // Debug + console.debug("Authenticated user ID:", userId); // Debug + + if (!userId) { + console.debug("User not authenticated."); // Debug + Unauthorized(res, { msg: "Utilisateur non authentifié." }); + return; + } + + if (!shotgun_password) { + console.debug("Shotgun password not configured on server."); // Debug + Error(res, { msg: "Mot de passe shotgun non configuré côté serveur." }); + return; + } + + if (password !== shotgun_password) { + console.debug("Incorrect shotgun password provided."); // Debug + Teapot(res, { msg: "Le mot de passe shotgun est incorrect." }); + return; + } + const status = await event_service.getEventsStatus(); if (!status?.shotgun_open) { - Unauthorized(res, { msg: "Le shotgun est fermé." }); - return; + Unauthorized(res, { msg: "Le shotgun est fermé." }); + return; } - try{ + try { const userTeam = await team_service.getUserTeam(userId) - if(!userTeam){ + if (!userTeam) { Error(res, { msg: "Erreur : Tu n'as pas d'équipe !" }); return; } const alreadyShotgun = await event_service.alreadyShotgun(userTeam) - if(alreadyShotgun){ + if (alreadyShotgun) { Accepted(res, { msg: "Votre équipe est déjà dans le shotgun." }); return; } await event_service.validateShotgun(userTeam); - Ok(res, { msg: "Shotgun validé !"}); + Ok(res, { msg: "Shotgun validé !" }); return; - }catch(error){ - Error(res, {msg :"Erreur pendant le shotguns : "+ error}); + } catch (error) { + Error(res, { msg: "Erreur pendant le shotguns : " + error }); return; } }; export const togglePreRegistration = async (req: Request, res: Response) => { const { preRegistrationOpen } = req.body; - + try { - const result = await event_service.updatepreRegistrationStatus(preRegistrationOpen); - Ok(res, { msg: "Paramètres mis à jour.", data : result}); + const result = await event_service.updatepreRegistrationStatus(preRegistrationOpen); + Ok(res, { msg: "Paramètres mis à jour.", data: result }); } catch (error) { - Error(res, { msg: "Erreur lors de la mise à jour." }); + Error(res, { msg: "Erreur lors de la mise à jour." }); } }; export const toggleShotgun = async (req: Request, res: Response) => { const { shotgunOpen } = req.body; - + try { - const result = await event_service.updateShotgunStatus(shotgunOpen); - Ok(res,{ msg: "Paramètres mis à jour.", data: result }); + const result = await event_service.updateShotgunStatus(shotgunOpen); + Ok(res, { msg: "Paramètres mis à jour.", data: result }); } catch (error) { - Error(res,{ msg: "Erreur lors de la mise à jour." }); + Error(res, { msg: "Erreur lors de la mise à jour." }); } }; export const toggleSDI = async (req: Request, res: Response) => { const { sdiOpen } = req.body; - + try { - const result = await event_service.updateSDIStatus(sdiOpen); - Ok(res,{ msg: "Paramètres mis à jour.", data: result }); + const result = await event_service.updateSDIStatus(sdiOpen); + Ok(res, { msg: "Paramètres mis à jour.", data: result }); } catch (error) { - Error(res,{ msg: "Erreur lors de la mise à jour." }); + Error(res, { msg: "Erreur lors de la mise à jour." }); } }; export const toggleWEI = async (req: Request, res: Response) => { const { weiOpen } = req.body; - + try { - const result = await event_service.updateWEIStatus(weiOpen); - Ok(res,{ msg: "Paramètres mis à jour.", data: result }); + const result = await event_service.updateWEIStatus(weiOpen); + Ok(res, { msg: "Paramètres mis à jour.", data: result }); } catch (error) { - Error(res,{ msg: "Erreur lors de la mise à jour." }); + Error(res, { msg: "Erreur lors de la mise à jour." }); } }; export const toggleFood = async (req: Request, res: Response) => { const { foodOpen } = req.body; - + try { - const result = await event_service.updateFoodStatus(foodOpen); - Ok(res,{ msg: "Paramètres mis à jour.", data: result }); + const result = await event_service.updateFoodStatus(foodOpen); + Ok(res, { msg: "Paramètres mis à jour.", data: result }); } catch (error) { - Error(res,{ msg: "Erreur lors de la mise à jour." }); + Error(res, { msg: "Erreur lors de la mise à jour." }); } }; export const toggleChall = async (req: Request, res: Response) => { const { challOpen } = req.body; - + try { - const result = await event_service.updateChallStatus(challOpen); - Ok(res,{ msg: "Paramètres mis à jour.", data: result }); + const result = await event_service.updateChallStatus(challOpen); + Ok(res, { msg: "Paramètres mis à jour.", data: result }); } catch (error) { - Error(res,{ msg: "Erreur lors de la mise à jour." }); + Error(res, { msg: "Erreur lors de la mise à jour." }); } }; \ No newline at end of file diff --git a/backend/src/routes/event.routes.ts b/backend/src/routes/event.routes.ts index 62c60d4..2c66c8c 100644 --- a/backend/src/routes/event.routes.ts +++ b/backend/src/routes/event.routes.ts @@ -6,21 +6,22 @@ import { authenticateUser } from '../middlewares/auth.middleware'; const eventRouter = express.Router(); // User routes -eventRouter.get("/user/shotgunstatus",checkRole("Student",[]), eventController.checkShotgunStatus); -eventRouter.get("/user/preregisterstatus",checkRole("Student",[]), eventController.checkPreRegisterStatus); +eventRouter.get("/user/shotgunstatus", checkRole("Student", []), eventController.checkShotgunStatus); +eventRouter.get("/user/preregisterstatus", checkRole("Student", []), eventController.checkPreRegisterStatus); eventRouter.get("/user/sdistatus", eventController.checkSDIStatus); eventRouter.get("/user/weistatus", eventController.checkWEIStatus); eventRouter.get("/user/foodstatus", eventController.checkFoodStatus); eventRouter.get("/user/challstatus", eventController.checkChallStatus); -eventRouter.post("/user/shotgunattempt",checkRole("Student",[]), eventController.shotgunAttempt); +eventRouter.post("/user/shotgunattempt", checkRole("Student", []), eventController.shotgunAttempt); // Admin routes -eventRouter.post("/admin/shotguntoggle",checkRole("Admin",[]),eventController.toggleShotgun); -eventRouter.post("/admin/preregistrationtoggle",checkRole("Admin",[]), eventController.togglePreRegistration); -eventRouter.post("/admin/sditoggle",checkRole("Admin",[]),eventController.toggleSDI); -eventRouter.post("/admin/weitoggle",checkRole("Admin",[]), eventController.toggleWEI); -eventRouter.post("/admin/foodtoggle",checkRole("Admin",[]), eventController.toggleFood); -eventRouter.post("/admin/challtoggle",checkRole("Admin",[]), eventController.toggleChall); +eventRouter.post("/admin/shotguntoggle", checkRole("Admin", []), eventController.toggleShotgun); +eventRouter.get("/admin/shotgunattempts", checkRole("Admin", ["Respo CE"]), eventController.getShotgunAttempts); +eventRouter.post("/admin/preregistrationtoggle", checkRole("Admin", []), eventController.togglePreRegistration); +eventRouter.post("/admin/sditoggle", checkRole("Admin", []), eventController.toggleSDI); +eventRouter.post("/admin/weitoggle", checkRole("Admin", []), eventController.toggleWEI); +eventRouter.post("/admin/foodtoggle", checkRole("Admin", []), eventController.toggleFood); +eventRouter.post("/admin/challtoggle", checkRole("Admin", []), eventController.toggleChall); export default eventRouter; \ No newline at end of file diff --git a/backend/src/services/event.service.ts b/backend/src/services/event.service.ts index b3003d6..e6cac91 100644 --- a/backend/src/services/event.service.ts +++ b/backend/src/services/event.service.ts @@ -1,16 +1,16 @@ import { db } from "../database/db"; -import { eq } from "drizzle-orm"; +import { asc, eq } from "drizzle-orm"; import { eventSchema } from "../schemas/Basic/event.schema"; import { teamShotgunSchema } from "../schemas/Relational/teamshotgun.schema"; import { teamSchema } from "../schemas/Basic/team.schema"; export const getEventsStatus = async () => { - const events = await db.select().from(eventSchema); - if (events.length > 0) { - return events[0]; // Renvoie le premier événement s'il existe - } else { - return null; // ou une valeur par défaut - } + const events = await db.select().from(eventSchema); + if (events.length > 0) { + return events[0]; // Renvoie le premier événement s'il existe + } else { + return null; // ou une valeur par défaut + } }; export const validateShotgun = async (teamId: number) => { @@ -20,62 +20,64 @@ export const validateShotgun = async (teamId: number) => { }; export const alreadyShotgun = async (teamId: number) => { - const shotgunTeam = await db.select({shotgunId : teamShotgunSchema.id}) + const shotgunTeam = await db.select({ shotgunId: teamShotgunSchema.id }) .from(teamShotgunSchema) .where(eq(teamShotgunSchema.team_id, teamId)); - if(shotgunTeam[0]){ - return true - } - else{ - return false - } + if (shotgunTeam[0]) { + return true + } + else { + return false + } }; export const updatepreRegistrationStatus = async (preRegistrationOpen: boolean) => { - return await db.update(eventSchema) - .set({pre_registration_open: preRegistrationOpen}) - .returning(); + return await db.update(eventSchema) + .set({ pre_registration_open: preRegistrationOpen }) + .returning(); }; -export const updateShotgunStatus = async ( shotgunOpen: boolean) => { - return await db.update(eventSchema) - .set({ shotgun_open: shotgunOpen }) - .returning(); +export const updateShotgunStatus = async (shotgunOpen: boolean) => { + return await db.update(eventSchema) + .set({ shotgun_open: shotgunOpen }) + .returning(); }; export const getAllTeamShotguns = async () => { - return await db - .select({ - id: teamShotgunSchema.id, - timestamp: teamShotgunSchema.timestamp, - teamName: teamSchema.name, - teamType: teamSchema.type, - }) - .from(teamShotgunSchema) - .leftJoin(teamSchema, eq(teamShotgunSchema.team_id, teamSchema.id)); + return await db + .select({ + id: teamShotgunSchema.id, + teamId: teamShotgunSchema.team_id, + timestamp: teamShotgunSchema.timestamp, + teamName: teamSchema.name, + teamType: teamSchema.type, + }) + .from(teamShotgunSchema) + .leftJoin(teamSchema, eq(teamShotgunSchema.team_id, teamSchema.id)) + .orderBy(asc(teamShotgunSchema.timestamp), asc(teamShotgunSchema.id)); }; -export const updateSDIStatus = async ( sdiOpen: boolean) => { - return await db.update(eventSchema) - .set({ sdi_open: sdiOpen }) - .returning(); +export const updateSDIStatus = async (sdiOpen: boolean) => { + return await db.update(eventSchema) + .set({ sdi_open: sdiOpen }) + .returning(); }; -export const updateWEIStatus = async ( weiOpen: boolean) => { - return await db.update(eventSchema) - .set({ wei_open: weiOpen }) - .returning(); +export const updateWEIStatus = async (weiOpen: boolean) => { + return await db.update(eventSchema) + .set({ wei_open: weiOpen }) + .returning(); }; -export const updateFoodStatus = async ( foodOpen: boolean) => { - return await db.update(eventSchema) - .set({ food_open: foodOpen }) - .returning(); +export const updateFoodStatus = async (foodOpen: boolean) => { + return await db.update(eventSchema) + .set({ food_open: foodOpen }) + .returning(); }; export const updateChallStatus = async (challOpen: boolean) => { - return await db.update(eventSchema) - .set({ chall_open: challOpen }) - .returning(); + return await db.update(eventSchema) + .set({ chall_open: challOpen }) + .returning(); }; \ No newline at end of file diff --git a/backend/src/utils/responses.ts b/backend/src/utils/responses.ts index 7326283..fe6fda0 100644 --- a/backend/src/utils/responses.ts +++ b/backend/src/utils/responses.ts @@ -28,6 +28,11 @@ export const Unauthorized = (res: Response, details: { data?: any, msg?: string res.status(Code.UNAUTHORIZED).json(new HttpResponse(Code.OK, msg, details.data)); }; +export const Teapot = (res: Response, details: { data?: any, msg?: string }) => { + const msg = details.msg || "I'm a teapot"; + res.status(Code.IM_A_TEAPOT).json(new HttpResponse(Code.IM_A_TEAPOT, msg, details.data)); +}; + export enum Code { OK = 200, ACCEPTED = 202, @@ -35,6 +40,7 @@ export enum Code { BAD_REQUEST = 400, UNAUTHORIZED = 401, CREATED = 201, + IM_A_TEAPOT = 418, ISE = 500 } @@ -64,6 +70,8 @@ export class HttpResponse { return 'INTERNAL_SERVER_ERROR'; case Code.UNAUTHORIZED: return 'INTERNAL_SERVER_ERROR'; + case Code.IM_A_TEAPOT: + return 'IM_A_TEAPOT'; } } } diff --git a/backend/src/utils/secret.ts b/backend/src/utils/secret.ts index 52fc996..924cabd 100644 --- a/backend/src/utils/secret.ts +++ b/backend/src/utils/secret.ts @@ -9,13 +9,13 @@ export const cas_validate_url = process.env.CAS_VALIDATE_URL || "default"; export const service_url = process.env.SERVICE_URL || "default"; export const dev_db_url = process.env.DATABASE_URL || "default"; export const postgres_password = process.env.POSTGRES_PASSWORD || "default"; -export const postgres_user = process.env.POSTGRES_USER|| "default"; -export const postgres_port = process.env.POSTGRES_PORT|| "default"; -export const postgres_db = process.env.POSTGRES_DB|| "default"; -export const postgres_host = process.env.POSTGRES_HOST|| "default"; -export const google_client_id =process.env.GOOGLE_CLIENT_ID || "default"; -export const google_client_secret =process.env.GOOGLE_CLIENT_SECRET || "default"; -export const google_client_uri =process.env.GOOGLE_REDIRECT_URI || "default"; +export const postgres_user = process.env.POSTGRES_USER || "default"; +export const postgres_port = process.env.POSTGRES_PORT || "default"; +export const postgres_db = process.env.POSTGRES_DB || "default"; +export const postgres_host = process.env.POSTGRES_HOST || "default"; +export const google_client_id = process.env.GOOGLE_CLIENT_ID || "default"; +export const google_client_secret = process.env.GOOGLE_CLIENT_SECRET || "default"; +export const google_client_uri = process.env.GOOGLE_REDIRECT_URI || "default"; export const spreadsheet_id = process.env.SPREADSHEET_ID || "default"; export const api_utt_username = process.env.API_UTT_USERNAME || "default"; export const api_utt_password = process.env.API_UTT_PASSWORD || "default"; @@ -28,3 +28,4 @@ export const zimbra_password = process.env.ZIMBRA_PASSWORD || "default"; export const discord_client_id = process.env.DISCORD_CLIENT_ID || "default"; export const discord_client_secret = process.env.DISCORD_CLIENT_SECRET || "default"; export const discord_redirect_uri = process.env.DISCORD_REDIRECT_URI || "default"; +export const shotgun_password = process.env.SHOTGUN_PASSWORD || ""; diff --git a/docker-compose.yml b/docker-compose.yml index 50fffd8..764d957 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,6 +26,7 @@ services: POSTGRES_PASSWORD: password POSTGRES_DB: integration-dev PORT: 4001 + SHOTGUN_PASSWORD: siropdekiwi ports: - "4001:4001" volumes: @@ -35,7 +36,13 @@ services: - db networks: - app-network - command: ["sh", "-c", "until pg_isready -h db -p 5432; do echo 'Waiting for DB...'; sleep 2; done && npx drizzle-kit migrate && npm run dev"] + command: + [ + "sh", + "-c", + "until pg_isready -h db -p 5432; do echo 'Waiting for DB...'; sleep 2; + done && npx drizzle-kit migrate && npm run dev", + ] frontend: build: diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d462ac1..65ca47e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -19,7 +19,8 @@ import { AdminPageNews, AdminPageGames, AdminPageTent, - AdminPageBus + AdminPageBus, + AdminPageShotgun } from './pages/admin'; import ProtectedRoute from './components/utils/protectedroute'; @@ -90,6 +91,7 @@ const App: React.FC = () => { {/* ResposCE et Admin */} } /> + } /> } /> } /> diff --git a/frontend/src/components/Admin/adminEvent.tsx b/frontend/src/components/Admin/adminEvent.tsx index 3b1971d..1e6f90d 100644 --- a/frontend/src/components/Admin/adminEvent.tsx +++ b/frontend/src/components/Admin/adminEvent.tsx @@ -46,7 +46,7 @@ export const AdminEvents = () => { setStatuses({ preRegistration: preReg, - shotgun: shot, + shotgun: shot.status, sdi, wei, food, diff --git a/frontend/src/components/Admin/adminShotgun.tsx b/frontend/src/components/Admin/adminShotgun.tsx new file mode 100644 index 0000000..8ed53af --- /dev/null +++ b/frontend/src/components/Admin/adminShotgun.tsx @@ -0,0 +1,116 @@ +import { useEffect, useMemo, useState } from "react"; +import { Link } from "react-router-dom"; +import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; +import { Button } from "../ui/button"; +import { getShotgunAttemptsAdmin } from "../../services/requests/event.service"; +import { ShotgunAttemptRow } from "../../interfaces/event.interface"; + +const formatResponseTime = (timestamp: string | null, baseline: number | null): string => { + if (!timestamp || baseline === null) { + return "-"; + } + + const current = new Date(timestamp).getTime(); + if (Number.isNaN(current)) { + return "-"; + } + + const diffMs = Math.max(0, current - baseline); + const minutes = Math.floor(diffMs / 60000); + const seconds = Math.floor((diffMs % 60000) / 1000); + const milliseconds = diffMs % 1000; + + return `${minutes.toString().padStart(2, "0")}:${seconds + .toString() + .padStart(2, "0")}.${milliseconds.toString().padStart(3, "0")}`; +}; + +export const AdminShotgunRanking = () => { + const [attempts, setAttempts] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchShotgunAttempts = async () => { + try { + const data = await getShotgunAttemptsAdmin(); + setAttempts(data); + } catch { + setError("Impossible de récupérer les résultats du shotgun."); + } finally { + setLoading(false); + } + }; + + void fetchShotgunAttempts(); + }, []); + + const baselineTimestamp = useMemo(() => { + const firstWithTimestamp = attempts.find((entry) => entry.timestamp); + if (!firstWithTimestamp?.timestamp) { + return null; + } + const parsed = new Date(firstWithTimestamp.timestamp).getTime(); + return Number.isNaN(parsed) ? null : parsed; + }, [attempts]); + + return ( + + + + Shotgun - Ordre de réponse + + + +

+ Temps de réponse calculé par rapport a la première équipe ayant répondu. +

+ + {loading &&

Chargement...

} + {!loading && error &&

{error}

} + + {!loading && !error && ( +
+ + + + + + + + + + + + {attempts.map((attempt, index) => ( + + + + + + + + ))} + {attempts.length === 0 && ( + + + + )} + +
OrdreEquipeNb chefs d'equipeTemps de reponseAction
{index + 1}{attempt.teamName || "Equipe inconnue"}{attempt.leaderCount}{formatResponseTime(attempt.timestamp, baselineTimestamp)} + {attempt.teamId ? ( + + ) : ( + Aucun lien + )} +
+ Aucune equipe n'a encore repondu au shotgun. +
+
+ )} +
+
+ ); +}; diff --git a/frontend/src/components/Admin/adminTeam.tsx b/frontend/src/components/Admin/adminTeam.tsx index 920d738..836cab9 100644 --- a/frontend/src/components/Admin/adminTeam.tsx +++ b/frontend/src/components/Admin/adminTeam.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { useSearchParams } from "react-router-dom"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; @@ -22,6 +23,7 @@ import { User } from "../../interfaces/user.interface"; import Swal from "sweetalert2"; export const AdminTeamManagement = () => { + const [searchParams] = useSearchParams(); const [teams, setTeams] = useState([]); const [factions, setFactions] = useState([]); const [users, setUsers] = useState([]); @@ -51,6 +53,18 @@ export const AdminTeamManagement = () => { fetchData(); }, []); + useEffect(() => { + const teamIdParam = searchParams.get("teamId"); + if (!teamIdParam) { + return; + } + + const parsedId = Number(teamIdParam); + if (!Number.isNaN(parsedId)) { + setSelectedTeamId(parsedId); + } + }, [searchParams]); + useEffect(() => { const loadTeamDetails = async () => { if (!selectedTeamId) return; diff --git a/frontend/src/components/navbar.tsx b/frontend/src/components/navbar.tsx index 2693945..b4c422b 100644 --- a/frontend/src/components/navbar.tsx +++ b/frontend/src/components/navbar.tsx @@ -68,19 +68,20 @@ export const Navbar = () => { to: "#", icon: CogIcon, children: [ - { label: "Users", to: "/admin/users", rolesAllowed: ["Admin"] }, - { label: "Roles", to: "/admin/roles", rolesAllowed: ["Admin"] }, - { label: "Teams", to: "/admin/teams", rolesAllowed: ["Admin", "Respo CE"] }, - { label: "Factions", to: "/admin/factions", rolesAllowed: ["Admin", "Respo CE"] }, - { label: "Events", to: "/admin/events", rolesAllowed: ["Admin"] }, - { label: "Permanences", to: "/admin/permanences", rolesAllowed: ["Admin", "Respo CE"] }, + { label: "Bus", to: "/admin/bus", rolesAllowed: ["Admin"] }, { label: "Challenge", to: "/admin/challenge", rolesAllowed: ["Admin", "Arbitre"] }, - { label: "Export / Import", to: "/admin/export-import", rolesAllowed: ["Admin"] }, { label: "Email", to: "/admin/email", rolesAllowed: ["Admin"] }, + { label: "Events", to: "/admin/events", rolesAllowed: ["Admin"] }, + { label: "Export / Import", to: "/admin/export-import", rolesAllowed: ["Admin"] }, + { label: "Factions", to: "/admin/factions", rolesAllowed: ["Admin", "Respo CE"] }, + { label: "Games", to: "/admin/games", rolesAllowed: ["Admin"] }, { label: "News", to: "/admin/news", rolesAllowed: ["Admin", "Communication"] }, + { label: "Permanences", to: "/admin/permanences", rolesAllowed: ["Admin", "Respo CE"] }, + { label: "Roles", to: "/admin/roles", rolesAllowed: ["Admin"] }, + { label: "Shotgun", to: "/admin/shotgun", rolesAllowed: ["Admin", "Respo CE"] }, + { label: "Teams", to: "/admin/teams", rolesAllowed: ["Admin", "Respo CE"] }, { label: "Tentes", to: "/admin/tent", rolesAllowed: ["Admin"] }, - { label: "Bus", to: "/admin/bus", rolesAllowed: ["Admin"] }, - { label: "Games", to: "/admin/games", rolesAllowed: ["Admin"] }, + { label: "Users", to: "/admin/users", rolesAllowed: ["Admin"] }, ], }, { diff --git a/frontend/src/components/shotgun/shotgunSection.tsx b/frontend/src/components/shotgun/shotgunSection.tsx index 0c8436d..4a35380 100644 --- a/frontend/src/components/shotgun/shotgunSection.tsx +++ b/frontend/src/components/shotgun/shotgunSection.tsx @@ -1,5 +1,7 @@ import { useState, useEffect } from "react"; +import { AxiosError } from "axios"; import { checkShotgunStatus, attemptShotgun } from "../../services/requests/event.service"; +import { ApiErrorResponse } from "../../interfaces/event.interface"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; @@ -8,13 +10,13 @@ export const Shotgun = () => { const [status, setStatus] = useState(false); const [message, setMessage] = useState(""); const [inputValue, setInputValue] = useState(""); - - const predefinedShotgunPhrase = "siropdekiwi"; // Tu peux personnaliser ça évidemment ! + const [shotgunPassword, setShotgunPassword] = useState(""); useEffect(() => { const fetchStatus = async () => { - const shotgun_open = await checkShotgunStatus(); - setStatus(shotgun_open); + const shotgunStatus = await checkShotgunStatus(); + setStatus(shotgunStatus.status); + setShotgunPassword(shotgunStatus.password); }; fetchStatus(); }, []); @@ -22,16 +24,22 @@ export const Shotgun = () => { const handleShotgun = async (e: React.FormEvent) => { e.preventDefault(); - if (inputValue !== predefinedShotgunPhrase) { - setMessage("❌ Erreur : Phrase de Shotgun incorrecte."); + if (!shotgunPassword) { + setMessage("❌ Erreur : mot de passe shotgun indisponible."); + return; + } + + if (inputValue !== shotgunPassword) { + setMessage("❌ Erreur : Mot de passe de Shotgun incorrect."); return; } try { - const response = await attemptShotgun(); + const response = await attemptShotgun({ password: inputValue }); setMessage(response.message); - } catch (error: any) { - setMessage(error.response.data.message); + } catch (error) { + const axiosError = error as AxiosError; + setMessage(axiosError.response?.data?.message || "Une erreur est survenue."); } }; @@ -50,7 +58,7 @@ export const Shotgun = () => {

Mot à entrer :{" "} - {predefinedShotgunPhrase} + {shotgunPassword || "patience..."}

{!status && ( @@ -77,7 +85,7 @@ export const Shotgun = () => { {message && (

{ ); }; +export const AdminPageShotgun: React.FC = () => { + return ( + +

+ + + +
+ + ); +}; + export const AdminPageExport: React.FC = () => { return ( diff --git a/frontend/src/services/requests/event.service.ts b/frontend/src/services/requests/event.service.ts index 670a864..d1c7fc3 100644 --- a/frontend/src/services/requests/event.service.ts +++ b/frontend/src/services/requests/event.service.ts @@ -1,8 +1,9 @@ import api from '../api'; +import { ApiMessageResponse, ShotgunAttemptPayload, ShotgunAttemptRow, ShotgunStatusData } from '../../interfaces/event.interface'; -export const checkShotgunStatus = async () => { +export const checkShotgunStatus = async (): Promise => { - const response = await api.get("/event/user/shotgunstatus"); + const response = await api.get<{ data: ShotgunStatusData }>("/event/user/shotgunstatus"); return response.data.data; }; @@ -42,10 +43,17 @@ export const checkChallengeStatus = async () => { }; -export const attemptShotgun = async () => { +export const attemptShotgun = async (payload: ShotgunAttemptPayload): Promise => { - const response = await api.post("event/user/shotgunattempt"); - return response.data; + const response = await api.post("event/user/shotgunattempt", payload); + return response.data; + +}; + +export const getShotgunAttemptsAdmin = async (): Promise => { + + const response = await api.get<{ data: ShotgunAttemptRow[] }>("/event/admin/shotgunattempts"); + return response.data.data; }; @@ -53,45 +61,45 @@ export const toggleShotgun = async (shotgunOpen: boolean) => { const response = await api.post(`event/admin/shotguntoggle`, { shotgunOpen }); return response.data; - + }; export const togglePreRegistration = async (preRegistrationOpen: boolean) => { - const response = await api.post(`event/admin/preregistrationtoggle`, { preRegistrationOpen}); + const response = await api.post(`event/admin/preregistrationtoggle`, { preRegistrationOpen }); return response.data; - + }; export const toggleSDI = async (sdiOpen: boolean) => { - const response = await api.post(`event/admin/sditoggle`, { sdiOpen}); + const response = await api.post(`event/admin/sditoggle`, { sdiOpen }); return response.data; - + }; export const toggleWEI = async (weiOpen: boolean) => { - const response = await api.post(`event/admin/weitoggle`, { weiOpen}); + const response = await api.post(`event/admin/weitoggle`, { weiOpen }); return response.data; - + }; export const toggleFood = async (foodOpen: boolean) => { - const response = await api.post(`event/admin/foodtoggle`, { foodOpen}); + const response = await api.post(`event/admin/foodtoggle`, { foodOpen }); return response.data; - + }; export const toggleChallenge = async (challOpen: boolean) => { - const response = await api.post(`event/admin/challtoggle`, {challOpen}); + const response = await api.post(`event/admin/challtoggle`, { challOpen }); return response.data; - + }; \ No newline at end of file diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json index db0becc..87cfa44 100644 --- a/frontend/tsconfig.node.json +++ b/frontend/tsconfig.node.json @@ -2,17 +2,17 @@ "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "target": "ES2022", - "lib": ["ES2023"], + "lib": [ + "ES2023" + ], "module": "ESNext", "skipLibCheck": true, - /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", "noEmit": true, - /* Linting */ "strict": true, "noUnusedLocals": true, @@ -20,5 +20,7 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, - "include": ["vite.config.ts"] -} + "include": [ + "vite.config.ts" + ] +} \ No newline at end of file From 4a53b466bd7cf28d846a2325cb17c9ab7f1f386f Mon Sep 17 00:00:00 2001 From: Arthur Dodin Date: Tue, 21 Apr 2026 00:29:39 +0200 Subject: [PATCH 2/3] refactor(shotgun): remove debug logs from shotgunAttempt function --- backend/src/controllers/event.controller.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/backend/src/controllers/event.controller.ts b/backend/src/controllers/event.controller.ts index 7a9e5b4..2e1188c 100644 --- a/backend/src/controllers/event.controller.ts +++ b/backend/src/controllers/event.controller.ts @@ -98,23 +98,17 @@ export const shotgunAttempt = async (req: Request, res: Response) => { const userId = (req as AuthenticatedRequest).user?.userId; - console.debug("Received shotgun attempt with password:", password); // Debug - console.debug("Authenticated user ID:", userId); // Debug - if (!userId) { - console.debug("User not authenticated."); // Debug Unauthorized(res, { msg: "Utilisateur non authentifié." }); return; } if (!shotgun_password) { - console.debug("Shotgun password not configured on server."); // Debug Error(res, { msg: "Mot de passe shotgun non configuré côté serveur." }); return; } if (password !== shotgun_password) { - console.debug("Incorrect shotgun password provided."); // Debug Teapot(res, { msg: "Le mot de passe shotgun est incorrect." }); return; } From 6562b2296112b3071f00e19cff9db3988ccd6c00 Mon Sep 17 00:00:00 2001 From: Arthur Dodin Date: Tue, 21 Apr 2026 00:47:53 +0200 Subject: [PATCH 3/3] fix(routes): normalize frontend routes to lowercase --- frontend/src/App.tsx | 34 +++++++++++------------ frontend/src/components/auth/authForm.tsx | 6 ++-- frontend/src/components/navbar.tsx | 30 ++++++++++---------- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 65ca47e..d790b9d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -64,30 +64,30 @@ const App: React.FC = () => { {/* Public */} } /> - } /> - } /> + } /> + } /> } /> } /> } /> {/* Utilisateurs connectés */} - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> {/* Étudiant et Admin */} - } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> {/* ResposCE et Admin */} } /> diff --git a/frontend/src/components/auth/authForm.tsx b/frontend/src/components/auth/authForm.tsx index ade7eb6..eca0448 100644 --- a/frontend/src/components/auth/authForm.tsx +++ b/frontend/src/components/auth/authForm.tsx @@ -30,7 +30,7 @@ export const AuthForm = () => { const token = await loginUser(formData.email, formData.password); if (token) { localStorage.setItem("authToken", token); - window.location.href = "/Home"; + window.location.href = "/home"; } } catch (err: any) { console.error(err); @@ -47,7 +47,7 @@ export const AuthForm = () => { }; const handleCASLogin = async () => { - if (window.location.pathname === "/Home") return; + if (window.location.pathname === "/home") return; if (localStorage.getItem("casProcessed") === "true") return; localStorage.setItem("casProcessed", "true"); @@ -59,7 +59,7 @@ export const AuthForm = () => { try { const { token } = await handleCASTicket(ticket); localStorage.setItem("authToken", token); - setTimeout(() => window.location.href = "/Home", 300); + setTimeout(() => window.location.href = "/home", 300); } catch (error) { console.error("CAS Login failed:", error); setError("Authentification CAS échouée. Réessaie."); diff --git a/frontend/src/components/navbar.tsx b/frontend/src/components/navbar.tsx index b4c422b..c3a0f66 100644 --- a/frontend/src/components/navbar.tsx +++ b/frontend/src/components/navbar.tsx @@ -37,32 +37,32 @@ export const Navbar = () => { }, [pathname]); const navItems: NavItem[] = [ - { label: "Home", to: "/Home", icon: HomeIcon }, - { label: "Plannings", to: "/Plannings" }, - { label: "Parrainage", to: "/Parrainage" }, - { label: "Challenges", to: "/Challenges" }, - { label: "Mes Actus", to: "/News" }, + { label: "Home", to: "/home", icon: HomeIcon }, + { label: "Plannings", to: "/plannings" }, + { label: "Parrainage", to: "/parrainage" }, + { label: "Challenges", to: "/challenges" }, + { label: "Mes Actus", to: "/news" }, { label: "Permanences", to: "#", children: [ - { label: "Listes des permanences", to: "/PermanencesList", rolesAllowed: ["Admin", "Student"] }, - { label: "Mes permanences", to: "/MyPermanences", rolesAllowed: ["Admin", "Student"] }, - { label: "Faire l'appel", to: "/PermanencesAppeal", rolesAllowed: ["Admin", "Student"] }, + { label: "Listes des permanences", to: "/permanenceslist", rolesAllowed: ["Admin", "Student"] }, + { label: "Mes permanences", to: "/mypermanences", rolesAllowed: ["Admin", "Student"] }, + { label: "Faire l'appel", to: "/permanencesappeal", rolesAllowed: ["Admin", "Student"] }, ], }, { label: "Events", to: "#", children: [ - { label: "Shotgun", to: "/Shotgun", rolesAllowed: ["Admin", "Student"] }, - { label: "WEI", to: "/Wei" }, - { label: "SDI", to: "/SDI" }, - { label: "Repas", to: "/Food" }, - { label: "Defis Commissions", to: "/Games", rolesAllowed: ["Admin", "Student"] }, + { label: "Shotgun", to: "/shotgun", rolesAllowed: ["Admin", "Student"] }, + { label: "WEI", to: "/wei" }, + { label: "SDI", to: "/sdi" }, + { label: "Repas", to: "/food" }, + { label: "Defis Commissions", to: "/games", rolesAllowed: ["Admin", "Student"] }, ], }, - { label: "Mon compte", to: "/Profil", icon: UsersIcon }, + { label: "Mon compte", to: "/profil", icon: UsersIcon }, { label: "Admin", to: "#", @@ -130,7 +130,7 @@ export const Navbar = () => {