Skip to content

Commit 3b0a619

Browse files
committed
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)
1 parent 4cbaed8 commit 3b0a619

File tree

12 files changed

+480
-121
lines changed

12 files changed

+480
-121
lines changed

backend/server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ async function startServer() {
4646

4747
// Utilisation des routes d'authentification
4848
app.use('/api/auth', authRoutes);
49+
app.use('/api/authadmin',authenticateUser, authRoutes);
4950
app.use('/api/role',authenticateUser, roleRoutes);
5051
app.use('/api/user',authenticateUser, userRoutes);
5152
app.use('/api/team',authenticateUser, teamRoutes);

backend/src/controllers/auth.controller.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as auth_service from '../services/auth.service';
33
import * as user_service from '../services/user.service';
44
import * as email_service from '../services/email.service';
55
import * as role_service from '../services/role.service';
6+
import * as registration_service from '../services/registration.service';
67
import bigInt from 'big-integer';
78
import { Error, Ok, Unauthorized } from '../utils/responses';
89
import { decodeToken } from '../utils/token';
@@ -221,4 +222,26 @@ export const resetPasswordUser = async (req: Request, res: Response) => {
221222
Error(res, { msg: 'Token invalid or expire' });
222223
return
223224
}
224-
}
225+
}
226+
227+
export const renewToken = async (req: Request, res: Response) => {
228+
const { userId } = req.body;
229+
230+
try {
231+
232+
const userToken = await registration_service.getRegistrationByUserId(userId);
233+
234+
if(userToken){
235+
await auth_service.deleteUserRegistrationToken(userId);
236+
}
237+
238+
const newToken = await auth_service.createRegistrationToken(userId)
239+
240+
Ok(res, {
241+
msg: 'Token renouvelé, vous pouvez renvoyer un email de bienvenu avec ce lien : https://integration.utt.fr/Register?token=' + newToken,
242+
});
243+
} catch (err) {
244+
Error(res, { msg: err.message });
245+
}
246+
};
247+

backend/src/routes/auth.routes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import express from 'express';
22
import * as authController from '../controllers/auth.controller';
3+
import { checkRole } from '../middlewares/user.middleware';
34

45
const authRouter = express.Router();
56

@@ -14,4 +15,7 @@ authRouter.get("/istokenvalid", authController.isTokenValid);
1415
authRouter.post('/resetpassworduser', authController.resetPasswordUser)
1516
authRouter.post('/requestpassworduser', authController.requestPasswordUser)
1617

18+
//Admin reset token
19+
authRouter.post('/admin/renewtoken', checkRole("Admin", []), authController.renewToken);
20+
1721
export default authRouter;

backend/src/services/auth.service.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,12 @@ export const createRegistrationToken = async (userId: number) => {
168168
return token;
169169
};
170170

171+
export const deleteUserRegistrationToken = async (userId: number) => {
172+
try{
173+
await db.delete(registrationSchema).where(eq(registrationSchema.user_id, userId));
174+
return;
175+
}
176+
catch(error){
177+
throw new Error(error);
178+
}
179+
};

backend/src/services/challenge.service.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,11 @@ export const validateChallenge = async ({
103103

104104
// 5. Ajouter ou retirer des points manuellement
105105
export const modifyFactionPoints = async ({
106-
factionId,
107-
points,
108-
adminId,
106+
title,
107+
factionId,
108+
points,
109+
reason,
110+
adminId
109111
}: {
110112
title : string;
111113
factionId: number;
@@ -116,8 +118,10 @@ export const modifyFactionPoints = async ({
116118
}) => {
117119

118120

121+
const newchall = await createChallenge(title, reason, "Free", points, adminId)
122+
119123
const newChallengeValidationPoints = {
120-
challenge_id: 1,//TO CHANGE TO 1 IN PROD
124+
challenge_id: newchall.id,
121125
validated_by_admin_id: adminId,
122126
validated_at: new Date(),
123127
points: points,
@@ -204,7 +208,7 @@ export const getValidatedChallenges = async () => {
204208
challenge_id: challengeValidationSchema.challenge_id,
205209
challenge_name : challengeSchema.title,
206210
challenge_categorie : challengeSchema.category,
207-
challenge_descrpition : challengeSchema.description,
211+
challenge_description : challengeSchema.description,
208212
points: challengeValidationSchema.points,
209213
validated_at: challengeValidationSchema.validated_at,
210214
target_user_id: challengeValidationSchema.target_user_id,
@@ -241,6 +245,7 @@ export const getTotalFactionPoints = async (factionId: number): Promise<number>
241245
.from(challengeValidationSchema).where(eq(challengeValidationSchema.target_faction_id, factionId));
242246

243247
// Récupérer le total des points
248+
244249
const totalPoints = Number(result[0]?.totalPoints) || 0;
245250
return totalPoints;
246251
} catch (error) {

frontend/src/components/Admin/AdminChallenge/adminChalengeList.tsx

Lines changed: 194 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,225 @@
1+
import { useMemo, useState } from "react";
12
import { Card } from "../../ui/card";
23
import { Button } from "../../ui/button";
34
import { Challenge } from "../../../interfaces/challenge.interface";
4-
import { deleteChallenge } from "../../../services/requests/challenge.service";
5+
import { deleteChallenge, validateChallenge } from "../../../services/requests/challenge.service";
6+
import { Trash2, Edit, CheckCircle2, Search } from "lucide-react";
7+
import Select, { SingleValue } from "react-select";
8+
import { Team } from "../../../interfaces/team.interface";
9+
import { Faction } from "../../../interfaces/faction.interface";
10+
import { User } from "../../../interfaces/user.interface";
511
import Swal from "sweetalert2";
6-
import { Trash2, Edit } from "lucide-react";
12+
import { Input } from "../../ui/input";
713

814
interface Props {
915
challenges: Challenge[];
1016
refreshChallenges: () => void;
1117
onEdit: (c: Challenge) => void;
18+
teams: Team[];
19+
factions: Faction[];
20+
users: User[];
1221
}
1322

14-
const AdminChallengeList = ({ challenges, refreshChallenges, onEdit }: Props) => {
23+
type ValidationTarget = "user" | "team" | "faction";
24+
25+
const AdminChallengeList = ({ challenges, refreshChallenges, onEdit, teams, factions, users }: Props) => {
26+
const [showValidationFormForId, setShowValidationFormForId] = useState<number | null>(null);
27+
const [validationType, setValidationType] = useState<ValidationTarget | null>(null);
28+
const [selectedTargetId, setSelectedTargetId] = useState<number | null>(null);
29+
const [searchTerm, setSearchTerm] = useState("");
30+
31+
32+
const filteredChallenges = useMemo(() => {
33+
return challenges.filter(
34+
(c) =>
35+
c.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
36+
c.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
37+
c.category.toLowerCase().includes(searchTerm.toLowerCase())
38+
);
39+
}, [challenges, searchTerm]);
40+
1541
const handleDelete = async (id: number) => {
1642
const confirm = await Swal.fire({
43+
title: "Supprimer ce challenge ?",
44+
text: "Cette action est irréversible 🚨",
1745
icon: "warning",
18-
title: "Supprimer ?",
19-
text: "Cette action est irréversible",
2046
showCancelButton: true,
47+
confirmButtonColor: "#e3342f",
48+
cancelButtonColor: "#6b7280",
2149
confirmButtonText: "Oui, supprimer",
2250
cancelButtonText: "Annuler",
2351
});
2452

2553
if (!confirm.isConfirmed) return;
2654

27-
await deleteChallenge(id);
28-
Swal.fire({ icon: "success", title: "Challenge supprimé !" });
29-
refreshChallenges();
55+
try {
56+
await deleteChallenge(id);
57+
Swal.fire("Supprimé ✅", "Le challenge a bien été supprimé.", "success");
58+
refreshChallenges();
59+
} catch (err) {
60+
Swal.fire("Erreur ❌", "Impossible de supprimer le challenge.", "error");
61+
}
62+
};
63+
64+
const handleValidate = async () => {
65+
if (!showValidationFormForId || !validationType || !selectedTargetId) return;
66+
67+
try {
68+
const res = await validateChallenge({
69+
challengeId: showValidationFormForId,
70+
type: validationType,
71+
targetId: selectedTargetId,
72+
});
73+
74+
Swal.fire({
75+
icon: "success",
76+
title: "Challenge validé ✅",
77+
text: res.message,
78+
timer: 2000,
79+
showConfirmButton: false,
80+
});
81+
82+
setShowValidationFormForId(null);
83+
setValidationType(null);
84+
setSelectedTargetId(null);
85+
refreshChallenges();
86+
} catch (err) {
87+
console.error("Erreur lors de la validation du challenge", err);
88+
Swal.fire({
89+
icon: "error",
90+
title: "Erreur ❌",
91+
text: "Impossible de valider ce challenge. Réessaie plus tard.",
92+
});
93+
}
3094
};
3195

3296
return (
3397
<Card className="p-6 rounded-2xl shadow-lg">
3498
<h3 className="text-2xl font-semibold text-center mb-6">📜 Challenges</h3>
35-
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
36-
{challenges.map((c) => (
37-
<div
38-
key={c.id}
39-
className="bg-gray-100 p-4 rounded-xl border shadow flex flex-col justify-between"
40-
>
41-
<div>
42-
<h4 className="font-bold text-lg">{c.title}</h4>
43-
<p className="text-gray-700">{c.description}</p>
44-
<p className="text-sm text-gray-500 mt-1">Catégorie : {c.category}</p>
45-
<p className="text-sm text-gray-500">Points : {c.points}</p>
46-
</div>
4799

48-
<div className="flex gap-2 mt-4">
49-
<Button
50-
onClick={() => onEdit(c)}
51-
className="bg-yellow-600 hover:bg-yellow-700 text-white flex items-center gap-2"
52-
>
53-
<Edit className="w-4 h-4" /> Modifier
54-
</Button>
55-
<Button
56-
onClick={() => handleDelete(c.id)}
57-
className="bg-red-600 hover:bg-red-700 text-white flex items-center gap-2"
58-
>
59-
<Trash2 className="w-4 h-4" /> Supprimer
60-
</Button>
100+
{/* 🔎 Barre de recherche */}
101+
<div className="flex items-center gap-3 mb-6">
102+
<Search className="w-5 h-5 text-gray-500" />
103+
<Input
104+
type="text"
105+
placeholder="Rechercher un challenge..."
106+
value={searchTerm}
107+
onChange={(e) => setSearchTerm(e.target.value)}
108+
className="flex-1"
109+
/>
110+
</div>
111+
112+
{/* Liste filtrée */}
113+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
114+
{filteredChallenges.length > 0 ? (
115+
filteredChallenges.map((c) => (
116+
<div
117+
key={c.id}
118+
className="bg-gray-100 p-4 rounded-xl border shadow flex flex-col justify-between"
119+
>
120+
<div>
121+
<h4 className="font-bold text-lg">{c.title}</h4>
122+
<p className="text-gray-700">{c.description}</p>
123+
<p className="text-sm text-gray-500 mt-1">Catégorie : {c.category}</p>
124+
<p className="text-sm text-gray-500">Points : {c.points}</p>
125+
</div>
126+
127+
<div className="flex flex-wrap gap-2 mt-4">
128+
<Button
129+
onClick={() => onEdit(c)}
130+
className="bg-yellow-600 hover:bg-yellow-700 text-white flex items-center gap-2"
131+
>
132+
<Edit className="w-4 h-4" /> Modifier
133+
</Button>
134+
<Button
135+
onClick={() => handleDelete(c.id)}
136+
className="bg-red-600 hover:bg-red-700 text-white flex items-center gap-2"
137+
>
138+
<Trash2 className="w-4 h-4" /> Supprimer
139+
</Button>
140+
<Button
141+
onClick={() => setShowValidationFormForId(c.id)}
142+
className="bg-blue-600 hover:bg-blue-700 text-white flex items-center gap-2"
143+
>
144+
<CheckCircle2 className="w-4 h-4" /> Valider
145+
</Button>
146+
</div>
147+
148+
{showValidationFormForId === c.id && (
149+
<div className="mt-6 bg-white p-4 border rounded-xl shadow-inner space-y-4">
150+
<h4 className="font-bold text-lg">✅ Valider le challenge</h4>
151+
152+
<Select
153+
placeholder="Choisir le type de cible"
154+
onChange={(option: SingleValue<{ value: ValidationTarget; label: string }>) => {
155+
setValidationType(option?.value ?? null);
156+
setSelectedTargetId(null);
157+
}}
158+
options={[
159+
{ value: "user", label: "Utilisateur" },
160+
{ value: "team", label: "Équipe" },
161+
{ value: "faction", label: "Faction" },
162+
]}
163+
/>
164+
165+
{validationType === "user" && (
166+
<Select
167+
placeholder="Sélectionner un utilisateur"
168+
onChange={(option) => setSelectedTargetId(Number(option?.value))}
169+
options={users.map((u: User) => ({
170+
value: u.userId,
171+
label: `${u.firstName} ${u.lastName}`,
172+
}))}
173+
/>
174+
)}
175+
176+
{validationType === "team" && (
177+
<Select
178+
placeholder="Sélectionner une équipe"
179+
onChange={(option) => setSelectedTargetId(Number(option?.value))}
180+
options={teams.map((t: Team) => ({
181+
value: t.teamId,
182+
label: t.name,
183+
}))}
184+
/>
185+
)}
186+
187+
{validationType === "faction" && (
188+
<Select
189+
placeholder="Sélectionner une faction"
190+
onChange={(option) => setSelectedTargetId(Number(option?.value))}
191+
options={factions.map((f: Faction) => ({
192+
value: f.factionId,
193+
label: f.name,
194+
}))}
195+
/>
196+
)}
197+
198+
<div className="flex gap-4">
199+
<Button
200+
onClick={handleValidate}
201+
className="bg-blue-600 hover:bg-blue-700 text-white"
202+
>
203+
✅ Valider
204+
</Button>
205+
<Button
206+
onClick={() => {
207+
setShowValidationFormForId(null);
208+
setValidationType(null);
209+
setSelectedTargetId(null);
210+
}}
211+
className="bg-gray-400 hover:bg-gray-500 text-white"
212+
>
213+
❌ Annuler
214+
</Button>
215+
</div>
216+
</div>
217+
)}
61218
</div>
62-
</div>
63-
))}
219+
))
220+
) : (
221+
<p className="text-center text-gray-500 col-span-full">Aucun challenge trouvé.</p>
222+
)}
64223
</div>
65224
</Card>
66225
);

0 commit comments

Comments
 (0)