Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
25 changes: 24 additions & 1 deletion backend/src/controllers/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -221,4 +222,26 @@ export const resetPasswordUser = async (req: Request, res: Response) => {
Error(res, { msg: 'Token invalid or expire' });
return
}
}
}

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 });
}
};

4 changes: 4 additions & 0 deletions backend/src/routes/auth.routes.ts
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -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;
9 changes: 9 additions & 0 deletions backend/src/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
};
15 changes: 10 additions & 5 deletions backend/src/services/challenge.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -241,6 +245,7 @@ export const getTotalFactionPoints = async (factionId: number): Promise<number>
.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) {
Expand Down
229 changes: 194 additions & 35 deletions frontend/src/components/Admin/AdminChallenge/adminChalengeList.tsx
Original file line number Diff line number Diff line change
@@ -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<number | null>(null);
const [validationType, setValidationType] = useState<ValidationTarget | null>(null);
const [selectedTargetId, setSelectedTargetId] = useState<number | null>(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 (
<Card className="p-6 rounded-2xl shadow-lg">
<h3 className="text-2xl font-semibold text-center mb-6">📜 Challenges</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{challenges.map((c) => (
<div
key={c.id}
className="bg-gray-100 p-4 rounded-xl border shadow flex flex-col justify-between"
>
<div>
<h4 className="font-bold text-lg">{c.title}</h4>
<p className="text-gray-700">{c.description}</p>
<p className="text-sm text-gray-500 mt-1">Catégorie : {c.category}</p>
<p className="text-sm text-gray-500">Points : {c.points}</p>
</div>

<div className="flex gap-2 mt-4">
<Button
onClick={() => onEdit(c)}
className="bg-yellow-600 hover:bg-yellow-700 text-white flex items-center gap-2"
>
<Edit className="w-4 h-4" /> Modifier
</Button>
<Button
onClick={() => handleDelete(c.id)}
className="bg-red-600 hover:bg-red-700 text-white flex items-center gap-2"
>
<Trash2 className="w-4 h-4" /> Supprimer
</Button>
{/* 🔎 Barre de recherche */}
<div className="flex items-center gap-3 mb-6">
<Search className="w-5 h-5 text-gray-500" />
<Input
type="text"
placeholder="Rechercher un challenge..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="flex-1"
/>
</div>

{/* Liste filtrée */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredChallenges.length > 0 ? (
filteredChallenges.map((c) => (
<div
key={c.id}
className="bg-gray-100 p-4 rounded-xl border shadow flex flex-col justify-between"
>
<div>
<h4 className="font-bold text-lg">{c.title}</h4>
<p className="text-gray-700">{c.description}</p>
<p className="text-sm text-gray-500 mt-1">Catégorie : {c.category}</p>
<p className="text-sm text-gray-500">Points : {c.points}</p>
</div>

<div className="flex flex-wrap gap-2 mt-4">
<Button
onClick={() => onEdit(c)}
className="bg-yellow-600 hover:bg-yellow-700 text-white flex items-center gap-2"
>
<Edit className="w-4 h-4" /> Modifier
</Button>
<Button
onClick={() => handleDelete(c.id)}
className="bg-red-600 hover:bg-red-700 text-white flex items-center gap-2"
>
<Trash2 className="w-4 h-4" /> Supprimer
</Button>
<Button
onClick={() => setShowValidationFormForId(c.id)}
className="bg-blue-600 hover:bg-blue-700 text-white flex items-center gap-2"
>
<CheckCircle2 className="w-4 h-4" /> Valider
</Button>
</div>

{showValidationFormForId === c.id && (
<div className="mt-6 bg-white p-4 border rounded-xl shadow-inner space-y-4">
<h4 className="font-bold text-lg">✅ Valider le challenge</h4>

<Select
placeholder="Choisir le type de cible"
onChange={(option: SingleValue<{ value: ValidationTarget; label: string }>) => {
setValidationType(option?.value ?? null);
setSelectedTargetId(null);
}}
options={[
{ value: "user", label: "Utilisateur" },
{ value: "team", label: "Équipe" },
{ value: "faction", label: "Faction" },
]}
/>

{validationType === "user" && (
<Select
placeholder="Sélectionner un utilisateur"
onChange={(option) => setSelectedTargetId(Number(option?.value))}
options={users.map((u: User) => ({
value: u.userId,
label: `${u.firstName} ${u.lastName}`,
}))}
/>
)}

{validationType === "team" && (
<Select
placeholder="Sélectionner une équipe"
onChange={(option) => setSelectedTargetId(Number(option?.value))}
options={teams.map((t: Team) => ({
value: t.teamId,
label: t.name,
}))}
/>
)}

{validationType === "faction" && (
<Select
placeholder="Sélectionner une faction"
onChange={(option) => setSelectedTargetId(Number(option?.value))}
options={factions.map((f: Faction) => ({
value: f.factionId,
label: f.name,
}))}
/>
)}

<div className="flex gap-4">
<Button
onClick={handleValidate}
className="bg-blue-600 hover:bg-blue-700 text-white"
>
✅ Valider
</Button>
<Button
onClick={() => {
setShowValidationFormForId(null);
setValidationType(null);
setSelectedTargetId(null);
}}
className="bg-gray-400 hover:bg-gray-500 text-white"
>
❌ Annuler
</Button>
</div>
</div>
)}
</div>
</div>
))}
))
) : (
<p className="text-center text-gray-500 col-span-full">Aucun challenge trouvé.</p>
)}
</div>
</Card>
);
Expand Down
Loading