From 3b0a6191473e4d2750b354ffbcabb263ad51d4dc Mon Sep 17 00:00:00 2001 From: Maxime Letoffet Date: Tue, 2 Sep 2025 18:59:29 +0200 Subject: [PATCH] Added : - adminUser : resetPwd & resetToken Fix : - Challenge point update (User view) - Challenge free : we can see teh reseon - Challenge validation (forget to add in the refactoring) --- backend/server.ts | 1 + backend/src/controllers/auth.controller.ts | 25 +- backend/src/routes/auth.routes.ts | 4 + backend/src/services/auth.service.ts | 9 + backend/src/services/challenge.service.ts | 15 +- .../AdminChallenge/adminChalengeList.tsx | 229 +++++++++++++++--- .../adminChallengeValidatedList.tsx | 161 ++++++------ frontend/src/components/Admin/adminUser.tsx | 68 ++++++ .../components/challenge/challengeList.tsx | 5 +- .../src/interfaces/challenge.interface.ts | 18 ++ frontend/src/pages/admin.tsx | 59 ++++- .../src/services/requests/auth.service.ts | 7 + 12 files changed, 480 insertions(+), 121 deletions(-) diff --git a/backend/server.ts b/backend/server.ts index 0bea7a9..db858b0 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -46,6 +46,7 @@ async function startServer() { // Utilisation des routes d'authentification app.use('/api/auth', authRoutes); + app.use('/api/authadmin',authenticateUser, authRoutes); app.use('/api/role',authenticateUser, roleRoutes); app.use('/api/user',authenticateUser, userRoutes); app.use('/api/team',authenticateUser, teamRoutes); diff --git a/backend/src/controllers/auth.controller.ts b/backend/src/controllers/auth.controller.ts index 4e2e904..baf8af4 100644 --- a/backend/src/controllers/auth.controller.ts +++ b/backend/src/controllers/auth.controller.ts @@ -3,6 +3,7 @@ import * as auth_service from '../services/auth.service'; import * as user_service from '../services/user.service'; import * as email_service from '../services/email.service'; import * as role_service from '../services/role.service'; +import * as registration_service from '../services/registration.service'; import bigInt from 'big-integer'; import { Error, Ok, Unauthorized } from '../utils/responses'; import { decodeToken } from '../utils/token'; @@ -221,4 +222,26 @@ export const resetPasswordUser = async (req: Request, res: Response) => { Error(res, { msg: 'Token invalid or expire' }); return } -} \ No newline at end of file +} + +export const renewToken = async (req: Request, res: Response) => { + const { userId } = req.body; + + try { + + const userToken = await registration_service.getRegistrationByUserId(userId); + + if(userToken){ + await auth_service.deleteUserRegistrationToken(userId); + } + + const newToken = await auth_service.createRegistrationToken(userId) + + Ok(res, { + msg: 'Token renouvelé, vous pouvez renvoyer un email de bienvenu avec ce lien : https://integration.utt.fr/Register?token=' + newToken, + }); + } catch (err) { + Error(res, { msg: err.message }); + } +}; + diff --git a/backend/src/routes/auth.routes.ts b/backend/src/routes/auth.routes.ts index 76a44fb..560a5f4 100644 --- a/backend/src/routes/auth.routes.ts +++ b/backend/src/routes/auth.routes.ts @@ -1,5 +1,6 @@ import express from 'express'; import * as authController from '../controllers/auth.controller'; +import { checkRole } from '../middlewares/user.middleware'; const authRouter = express.Router(); @@ -14,4 +15,7 @@ authRouter.get("/istokenvalid", authController.isTokenValid); authRouter.post('/resetpassworduser', authController.resetPasswordUser) authRouter.post('/requestpassworduser', authController.requestPasswordUser) +//Admin reset token +authRouter.post('/admin/renewtoken', checkRole("Admin", []), authController.renewToken); + export default authRouter; diff --git a/backend/src/services/auth.service.ts b/backend/src/services/auth.service.ts index f487ff9..f634d79 100644 --- a/backend/src/services/auth.service.ts +++ b/backend/src/services/auth.service.ts @@ -168,3 +168,12 @@ export const createRegistrationToken = async (userId: number) => { return token; }; +export const deleteUserRegistrationToken = async (userId: number) => { + try{ + await db.delete(registrationSchema).where(eq(registrationSchema.user_id, userId)); + return; + } + catch(error){ + throw new Error(error); + } +}; diff --git a/backend/src/services/challenge.service.ts b/backend/src/services/challenge.service.ts index bf672ce..601be30 100644 --- a/backend/src/services/challenge.service.ts +++ b/backend/src/services/challenge.service.ts @@ -103,9 +103,11 @@ export const validateChallenge = async ({ // 5. Ajouter ou retirer des points manuellement export const modifyFactionPoints = async ({ - factionId, - points, - adminId, + title, + factionId, + points, + reason, + adminId }: { title : string; factionId: number; @@ -116,8 +118,10 @@ export const modifyFactionPoints = async ({ }) => { + const newchall = await createChallenge(title, reason, "Free", points, adminId) + const newChallengeValidationPoints = { - challenge_id: 1,//TO CHANGE TO 1 IN PROD + challenge_id: newchall.id, validated_by_admin_id: adminId, validated_at: new Date(), points: points, @@ -204,7 +208,7 @@ export const getValidatedChallenges = async () => { challenge_id: challengeValidationSchema.challenge_id, challenge_name : challengeSchema.title, challenge_categorie : challengeSchema.category, - challenge_descrpition : challengeSchema.description, + challenge_description : challengeSchema.description, points: challengeValidationSchema.points, validated_at: challengeValidationSchema.validated_at, target_user_id: challengeValidationSchema.target_user_id, @@ -241,6 +245,7 @@ export const getTotalFactionPoints = async (factionId: number): Promise .from(challengeValidationSchema).where(eq(challengeValidationSchema.target_faction_id, factionId)); // Récupérer le total des points + const totalPoints = Number(result[0]?.totalPoints) || 0; return totalPoints; } catch (error) { diff --git a/frontend/src/components/Admin/AdminChallenge/adminChalengeList.tsx b/frontend/src/components/Admin/AdminChallenge/adminChalengeList.tsx index bf2fa71..a4a05d2 100644 --- a/frontend/src/components/Admin/AdminChallenge/adminChalengeList.tsx +++ b/frontend/src/components/Admin/AdminChallenge/adminChalengeList.tsx @@ -1,66 +1,225 @@ +import { useMemo, useState } from "react"; import { Card } from "../../ui/card"; import { Button } from "../../ui/button"; import { Challenge } from "../../../interfaces/challenge.interface"; -import { deleteChallenge } from "../../../services/requests/challenge.service"; +import { deleteChallenge, validateChallenge } from "../../../services/requests/challenge.service"; +import { Trash2, Edit, CheckCircle2, Search } from "lucide-react"; +import Select, { SingleValue } from "react-select"; +import { Team } from "../../../interfaces/team.interface"; +import { Faction } from "../../../interfaces/faction.interface"; +import { User } from "../../../interfaces/user.interface"; import Swal from "sweetalert2"; -import { Trash2, Edit } from "lucide-react"; +import { Input } from "../../ui/input"; interface Props { challenges: Challenge[]; refreshChallenges: () => void; onEdit: (c: Challenge) => void; + teams: Team[]; + factions: Faction[]; + users: User[]; } -const AdminChallengeList = ({ challenges, refreshChallenges, onEdit }: Props) => { +type ValidationTarget = "user" | "team" | "faction"; + +const AdminChallengeList = ({ challenges, refreshChallenges, onEdit, teams, factions, users }: Props) => { + const [showValidationFormForId, setShowValidationFormForId] = useState(null); + const [validationType, setValidationType] = useState(null); + const [selectedTargetId, setSelectedTargetId] = useState(null); + const [searchTerm, setSearchTerm] = useState(""); + + + const filteredChallenges = useMemo(() => { + return challenges.filter( + (c) => + c.title.toLowerCase().includes(searchTerm.toLowerCase()) || + c.description.toLowerCase().includes(searchTerm.toLowerCase()) || + c.category.toLowerCase().includes(searchTerm.toLowerCase()) + ); + }, [challenges, searchTerm]); + const handleDelete = async (id: number) => { const confirm = await Swal.fire({ + title: "Supprimer ce challenge ?", + text: "Cette action est irréversible 🚨", icon: "warning", - title: "Supprimer ?", - text: "Cette action est irréversible", showCancelButton: true, + confirmButtonColor: "#e3342f", + cancelButtonColor: "#6b7280", confirmButtonText: "Oui, supprimer", cancelButtonText: "Annuler", }); if (!confirm.isConfirmed) return; - await deleteChallenge(id); - Swal.fire({ icon: "success", title: "Challenge supprimé !" }); - refreshChallenges(); + try { + await deleteChallenge(id); + Swal.fire("Supprimé ✅", "Le challenge a bien été supprimé.", "success"); + refreshChallenges(); + } catch (err) { + Swal.fire("Erreur ❌", "Impossible de supprimer le challenge.", "error"); + } + }; + + const handleValidate = async () => { + if (!showValidationFormForId || !validationType || !selectedTargetId) return; + + try { + const res = await validateChallenge({ + challengeId: showValidationFormForId, + type: validationType, + targetId: selectedTargetId, + }); + + Swal.fire({ + icon: "success", + title: "Challenge validé ✅", + text: res.message, + timer: 2000, + showConfirmButton: false, + }); + + setShowValidationFormForId(null); + setValidationType(null); + setSelectedTargetId(null); + refreshChallenges(); + } catch (err) { + console.error("Erreur lors de la validation du challenge", err); + Swal.fire({ + icon: "error", + title: "Erreur ❌", + text: "Impossible de valider ce challenge. Réessaie plus tard.", + }); + } }; return (

📜 Challenges

-
- {challenges.map((c) => ( -
-
-

{c.title}

-

{c.description}

-

Catégorie : {c.category}

-

Points : {c.points}

-
-
- - + {/* 🔎 Barre de recherche */} +
+ + setSearchTerm(e.target.value)} + className="flex-1" + /> +
+ + {/* Liste filtrée */} +
+ {filteredChallenges.length > 0 ? ( + filteredChallenges.map((c) => ( +
+
+

{c.title}

+

{c.description}

+

Catégorie : {c.category}

+

Points : {c.points}

+
+ +
+ + + +
+ + {showValidationFormForId === c.id && ( +
+

✅ Valider le challenge

+ + setSelectedTargetId(Number(option?.value))} + options={users.map((u: User) => ({ + value: u.userId, + label: `${u.firstName} ${u.lastName}`, + }))} + /> + )} + + {validationType === "team" && ( + setSelectedTargetId(Number(option?.value))} + options={factions.map((f: Faction) => ({ + value: f.factionId, + label: f.name, + }))} + /> + )} + +
+ + +
+
+ )}
-
- ))} + )) + ) : ( +

Aucun challenge trouvé.

+ )}
); diff --git a/frontend/src/components/Admin/AdminChallenge/adminChallengeValidatedList.tsx b/frontend/src/components/Admin/AdminChallenge/adminChallengeValidatedList.tsx index 8facf02..dad8c74 100644 --- a/frontend/src/components/Admin/AdminChallenge/adminChallengeValidatedList.tsx +++ b/frontend/src/components/Admin/AdminChallenge/adminChallengeValidatedList.tsx @@ -1,26 +1,46 @@ -import { useState, useEffect } from "react"; +import { useMemo, useState } from "react"; import { Button } from "../../ui/button"; -import { getAllChallengesValidates, unvalidateChallenge } from "../../../services/requests/challenge.service"; +import { Input } from "../../ui/input"; +import { unvalidateChallenge } from "../../../services/requests/challenge.service"; import Swal from "sweetalert2"; +import { ValidatedChallenge } from "../../../interfaces/challenge.interface"; +import { Search } from "lucide-react"; +interface Props { + validatedChallenges: ValidatedChallenge[]; + fetchValidatedChallenges: () => void | Promise; +} -export const AdminValidatedChallengesList = () => { - const [validatedChallenges, setValidatedChallenges] = useState([]); +export const AdminValidatedChallengesList = ({ + validatedChallenges, + fetchValidatedChallenges, +}: Props) => { + const [search, setSearch] = useState(""); - const fetchValidatedChallenges = async () => { - try { - const challenges = await getAllChallengesValidates(); - setValidatedChallenges(challenges); - } catch { - Swal.fire("Erreur", "Impossible de récupérer les challenges validés", "error"); - } - }; - - useEffect(() => { - fetchValidatedChallenges(); - }, []); + const filtered = useMemo(() => { + const q = search.toLowerCase(); + return validatedChallenges.filter((c) => + [ + c.challenge_name, + c.challenge_categorie, + c.challenge_description, + c.target_user_firstname ?? "", + c.target_user_lastname ?? "", + c.target_team_name ?? "", + c.target_faction_name ?? "", + ] + .join(" ") + .toLowerCase() + .includes(q) + ); + }, [validatedChallenges, search]); - const handleUnvalidate = async (challengeId: number, factionId: number, teamId: number, userId: number) => { + const handleUnvalidate = async ( + challengeId: number, + factionId: number | null, + teamId: number | null, + userId: number | null + ) => { const confirm = await Swal.fire({ title: "Confirmer la dévalidation ?", text: "Cette action retirera la validation du challenge.", @@ -31,71 +51,76 @@ export const AdminValidatedChallengesList = () => { confirmButtonText: "Oui, dévalider", cancelButtonText: "Annuler", }); - if (!confirm.isConfirmed) return; try { - const result = await unvalidateChallenge({ challengeId, factionId, teamId, userId }); - Swal.fire("Succès", result.message, "success"); - fetchValidatedChallenges(); + const res = await unvalidateChallenge({ + challengeId, + factionId: factionId ?? 0, + teamId: teamId ?? 0, + userId: userId ?? 0, + }); + Swal.fire({ icon: "success", title: "Dévalidé", text: res.message, timer: 1800, showConfirmButton: false }); + await fetchValidatedChallenges(); } catch { - Swal.fire("Erreur", "❌ Une erreur est survenue lors de la dévalidation", "error"); + Swal.fire({ icon: "error", title: "Erreur", text: "Impossible de dévalider ce challenge." }); } }; return ( -
-
-

📋 Challenges validés

+
+
+

📋 Challenges validés

- {validatedChallenges.length === 0 ? ( -

Aucun challenge validé pour le moment.

- ) : ( -
- {validatedChallenges.map((challenge) => ( -
-
-
-

{challenge.challenge_name}

-

{challenge.challenge_categorie}

-

{challenge.challenge_description}

-
+ {/* Recherche */} +
+ + setSearch(e.target.value)} + className="border-none focus:ring-0 bg-transparent flex-1" + /> +
-
-

- Points : {challenge.points} -

-

- Validé le :{" "} - {new Date(challenge.validated_at).toLocaleDateString()} -

-
+ {/* Grille */} + {filtered.length === 0 ? ( +

Aucun challenge validé trouvé.

+ ) : ( +
+ {filtered.map((c) => ( +
+
+

{c.challenge_name}

+

{c.challenge_categorie}

+

{c.challenge_description}

-
-
-

Destinataire :

-

{challenge.target_faction_name}

-

{challenge.target_team_name}

-

- {challenge.target_user_firstname} {challenge.target_user_lastname} -

-
+
+

Points : {c.points}

+

+ Validé le : {new Date(c.validated_at).toLocaleDateString()} +

+
- +
+

Destinataire :

+ {c.target_faction_name &&

{c.target_faction_name}

} + {c.target_team_name &&

{c.target_team_name}

} + {(c.target_user_firstname || c.target_user_lastname) && ( +

{c.target_user_firstname} {c.target_user_lastname}

+ )}
+ +
))}
diff --git a/frontend/src/components/Admin/adminUser.tsx b/frontend/src/components/Admin/adminUser.tsx index 794b319..7d05185 100644 --- a/frontend/src/components/Admin/adminUser.tsx +++ b/frontend/src/components/Admin/adminUser.tsx @@ -11,6 +11,7 @@ import { syncnewStudent, } from "../../services/requests/user.service"; import { User } from "../../interfaces/user.interface"; +import { renewTokenUser, requestPasswordUser } from "../../services/requests/auth.service"; const permissionOptions = [ { value: "Admin", label: "Admin" }, @@ -110,6 +111,59 @@ export const AdminUser = () => { } }; + const handleRenewToken = async () => { + if (!selectedUser) return; + + const result = await Swal.fire({ + title: `Renouveler le token de ${selectedUser.firstName} ${selectedUser.lastName} ?`, + text: "Un nouveau token sera généré.", + icon: "question", + showCancelButton: true, + confirmButtonColor: "#2563eb", + cancelButtonColor: "#d33", + confirmButtonText: "Oui, renouveler", + cancelButtonText: "Annuler", + }); + + if (result.isConfirmed) { + const res = await renewTokenUser(selectedUser.userId); + + Swal.fire({ + icon: "success", + title: "Token renouvelé 🔑", + text: res.message, + confirmButtonColor: "#16a34a", + }); + } + }; + + const handleRequestPassword = async () => { + if (!selectedUser) return; + + const result = await Swal.fire({ + title: `Envoyer une demande de reset password à ${selectedUser.email} ?`, + text: "L'utilisateur recevra un email pour réinitialiser son mot de passe.", + icon: "question", + showCancelButton: true, + confirmButtonColor: "#2563eb", + cancelButtonColor: "#d33", + confirmButtonText: "Oui, envoyer", + cancelButtonText: "Annuler", + }); + + if (result.isConfirmed) { + const res = await requestPasswordUser(selectedUser.email); + + Swal.fire({ + icon: "success", + title: "Email envoyé 📩", + text: res.msg, + confirmButtonColor: "#16a34a", + }); + } + }; + + return ( @@ -210,6 +264,20 @@ export const AdminUser = () => { > 🗑 Supprimer + +
)} diff --git a/frontend/src/components/challenge/challengeList.tsx b/frontend/src/components/challenge/challengeList.tsx index 0f04ae7..9c5a35d 100644 --- a/frontend/src/components/challenge/challengeList.tsx +++ b/frontend/src/components/challenge/challengeList.tsx @@ -44,7 +44,8 @@ export const UserChallengeList = () => { const fetchChallenges = async () => { try { const challenges = await getAllChallenges(); - setAvailableChallenges(challenges); + const challengesFiltered = challenges.filter((c : Challenge) => c.category != "Free") + setAvailableChallenges(challengesFiltered); } catch (err) { console.error("Erreur lors du chargement des challenges", err); } @@ -66,7 +67,7 @@ export const UserChallengeList = () => { await Promise.all( fetchedFactions.map(async (faction: Faction) => { const res = await getFactionsPoints(faction.factionId); - points[faction.factionId] = res.points ?? 0; + points[faction.factionId] = Number(res); }) ); setFactionPoints(points); diff --git a/frontend/src/interfaces/challenge.interface.ts b/frontend/src/interfaces/challenge.interface.ts index 9ebe4bb..a0ed96b 100644 --- a/frontend/src/interfaces/challenge.interface.ts +++ b/frontend/src/interfaces/challenge.interface.ts @@ -9,4 +9,22 @@ export interface Challenge { updatedAt: string; // Date de mise à jour status: "open" | "closed" | "completed"; // Statut du challenge } + +export interface ValidatedChallenge { + challenge_id: number; + challenge_name: string; + challenge_categorie: string; + challenge_description: string; + points: number; + validated_at: string; // ISO date string + target_user_id: number | null; + target_team_id: number | null; + target_faction_id: number | null; + target_user_firstname: string | null; + target_user_lastname: string | null; + target_team_name: string | null; + target_faction_name: string | null; +} + + \ No newline at end of file diff --git a/frontend/src/pages/admin.tsx b/frontend/src/pages/admin.tsx index 57fba2c..d472fc1 100644 --- a/frontend/src/pages/admin.tsx +++ b/frontend/src/pages/admin.tsx @@ -15,8 +15,8 @@ import { AdminRolePointsManager } from "../components/Admin/adminGames"; import ChallengeEditor from "../components/Admin/AdminChallenge/adminChallengeEditor"; import AdminChallengeList from "../components/Admin/AdminChallenge/adminChalengeList"; import { useEffect, useRef, useState } from "react"; -import { Challenge } from "../interfaces/challenge.interface"; -import { getAllChallenges } from "../services/requests/challenge.service"; +import { Challenge, ValidatedChallenge } from "../interfaces/challenge.interface"; +import { getAllChallenges, getAllChallengesValidates } from "../services/requests/challenge.service"; import { AdminChallengeAddPointsForm } from "../components/Admin/AdminChallenge/adminChallengeAddPointsForm"; import { AdminValidatedChallengesList } from "../components/Admin/AdminChallenge/adminChallengeValidatedList"; import { TentAdmin } from "../components/Admin/adminTent"; @@ -30,7 +30,11 @@ import PermanenceList from "../components/Admin/AdminPerm/adminPermList"; import { Permanence } from "../interfaces/permanence.interface"; import { User } from "../interfaces/user.interface"; import { getAllPermanences } from "../services/requests/permanence.service"; -import { getUsersAdmin } from "../services/requests/user.service"; +import { getUsers, getUsersAdmin } from "../services/requests/user.service"; +import { Team } from "../interfaces/team.interface"; +import { Faction } from "../interfaces/faction.interface"; +import { getAllTeams } from "../services/requests/team.service"; +import { getAllFactionsUser } from "../services/requests/faction.service"; @@ -218,21 +222,47 @@ export const AdminPagePerm: React.FC = () => { export const AdminPageChall: React.FC = () => { + const [challenges, setChallenges] = useState([]); + const [validatedChallenges, setValidatedChallenges] = useState([]); + const [users, setUsers] = useState([]); + const [teams, setTeams] = useState([]); + const [factions, setFactions] = useState([]); const [editingChallenge, setEditingChallenge] = useState(null); const editorRef = useRef(null); - const fetchChallenges = async () => { + const fetchChallengesUsersTeamsFactions = async () => { try { - const res = await getAllChallenges(); - setChallenges(res); + + const challsRes = await getAllChallenges(); + const usersRes = await getUsers(); + const teamsRes = await getAllTeams(); + const factionsRes = await getAllFactionsUser(); + + const challsResFiltered = challsRes.filter((c : Challenge) => c.category != "Free") + setChallenges(challsResFiltered); + setUsers(usersRes); + setTeams(teamsRes); + setFactions(factionsRes); + } catch (err) { console.error("Erreur chargement challenges", err); } }; + const fetchValidatedChallenges = async () => { + try { + const res = await getAllChallengesValidates(); + setValidatedChallenges(res); + } catch (err) { + console.error("Erreur chargement challenges validés", err); + } +}; + + useEffect(() => { - fetchChallenges(); + fetchChallengesUsersTeamsFactions(); + fetchValidatedChallenges(); }, []); const handleEdit = (challenge: Challenge) => { @@ -254,7 +284,7 @@ export const AdminPageChall: React.FC = () => { @@ -269,8 +299,14 @@ export const AdminPageChall: React.FC = () => {
{ + fetchChallengesUsersTeamsFactions(); + fetchValidatedChallenges(); + }} onEdit={handleEdit} + users={users} + teams={teams} + factions={factions} />
@@ -296,7 +332,10 @@ export const AdminPageChall: React.FC = () => { > {/* Liste des challenges validés */}
- +
diff --git a/frontend/src/services/requests/auth.service.ts b/frontend/src/services/requests/auth.service.ts index 055a0df..adf8f16 100644 --- a/frontend/src/services/requests/auth.service.ts +++ b/frontend/src/services/requests/auth.service.ts @@ -66,5 +66,12 @@ export const resetPasswordUser = async(token : string, password: string)=>{ export const requestPasswordUser = async(user_email : string)=>{ const response = await api.post('auth/requestpassworduser', {user_email}); + return response?.data +} + +export const renewTokenUser = async(userId : number)=>{ + console.log(userId) + const response = await api.post('authadmin/admin/renewtoken', {userId}); + return response?.data } \ No newline at end of file