Licence EUPL-1.2 + hardening anti-abus
P1 — Licence : - Ajout du fichier LICENSE (EUPL-1.2 complet) - README mis à jour : section licence, table docs, vars d'environnement - En-têtes EUPL ajoutés dans les fichiers sources principaux (Flask, React) P2 — Hardening anti-abus : - Rate limiting Redis-ready (REDIS_URL) avec clé fingerprint + IP - Honeypot anti-bot : champ caché côté client + vérification serveur - Fingerprinting non-PII via FingerprintJS (hash SHA-256, colonne ideas.fingerprint_hash) - Cooldown session : cookie httpOnly signé HMAC-SHA256 (SECRET_KEY requis) - Détection de flood : alerte WARNING si > FLOOD_THRESHOLD soumissions / 5 min - hCaptcha stub : intégré, activable via HCAPTCHA_SECRET_KEY + VITE_HCAPTCHA_SITE_KEY - Nouvelles dépendances : redis (backend), @fingerprintjs/fingerprintjs + @hcaptcha/react-hcaptcha (frontend) - docs/SECURITE_ANTI_ABUS.md : documentation complète des seuils et de la configuration Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
// Copyright (C) 2026 billisdead — Licence EUPL-1.2
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -5,6 +6,7 @@ import { z } from "zod";
|
||||
import { format } from "date-fns";
|
||||
import { fr } from "date-fns/locale";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||
import {
|
||||
useSubmitIdea,
|
||||
useListIdeas,
|
||||
@@ -12,6 +14,8 @@ import {
|
||||
useGetSynthesis,
|
||||
getListIdeasQueryKey,
|
||||
getGetIdeaStatsQueryKey,
|
||||
addExtraHeader,
|
||||
removeExtraHeader,
|
||||
} from "@workspace/api-client-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
@@ -71,6 +75,9 @@ const VALEURS = [
|
||||
},
|
||||
];
|
||||
|
||||
// Clé hCaptcha — activée si la variable d'environnement est définie
|
||||
const HCAPTCHA_SITE_KEY = import.meta.env.VITE_HCAPTCHA_SITE_KEY as string | undefined;
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_URL ?? "";
|
||||
|
||||
export default function Home() {
|
||||
@@ -84,6 +91,13 @@ export default function Home() {
|
||||
const [flaggedIds, setFlaggedIds] = React.useState<Set<number>>(new Set());
|
||||
const [flaggingId, setFlaggingId] = React.useState<number | null>(null);
|
||||
|
||||
// Ref pour le champ leurre honeypot — invisible, non relié à react-hook-form
|
||||
const honeypotRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
// hCaptcha — widget et token
|
||||
const captchaRef = React.useRef<HCaptcha>(null);
|
||||
const [captchaToken, setCaptchaToken] = React.useState<string | null>(null);
|
||||
|
||||
const handleFlag = async (ideaId: number) => {
|
||||
if (flaggedIds.has(ideaId) || flaggingId === ideaId) return;
|
||||
setFlaggingId(ideaId);
|
||||
@@ -173,6 +187,29 @@ export default function Home() {
|
||||
});
|
||||
|
||||
const onSubmit = (data: SubmitIdeaValues) => {
|
||||
// Honeypot — si le champ leurre est rempli, c'est un bot
|
||||
if (honeypotRef.current?.value) {
|
||||
// Simulation silencieuse d'un succès sans appel API
|
||||
setSubmitResult({ success: true, message: "Votre contribution a été ajoutée à la synthèse." });
|
||||
form.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
// hCaptcha — obligatoire si la clé de site est configurée
|
||||
if (HCAPTCHA_SITE_KEY && !captchaToken) {
|
||||
toast({
|
||||
title: "Vérification requise",
|
||||
description: "Veuillez valider le CAPTCHA avant de soumettre.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Transmission du token hCaptcha si disponible
|
||||
if (captchaToken) {
|
||||
addExtraHeader("x-hcaptcha-token", captchaToken);
|
||||
}
|
||||
|
||||
setSubmitResult(null);
|
||||
submitIdea.mutate({ data }, {
|
||||
onSuccess: (result) => {
|
||||
@@ -198,6 +235,12 @@ export default function Home() {
|
||||
message: "Une erreur est survenue lors de l'envoi. Veuillez réessayer.",
|
||||
});
|
||||
},
|
||||
onSettled: () => {
|
||||
// Nettoyage du token hCaptcha après chaque tentative
|
||||
removeExtraHeader("x-hcaptcha-token");
|
||||
captchaRef.current?.resetCaptcha();
|
||||
setCaptchaToken(null);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -227,6 +270,17 @@ export default function Home() {
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
{/* Champ leurre anti-bot (honeypot) — invisible, ne jamais supprimer */}
|
||||
<input
|
||||
ref={honeypotRef}
|
||||
type="text"
|
||||
name="_hp"
|
||||
aria-hidden="true"
|
||||
tabIndex={-1}
|
||||
autoComplete="off"
|
||||
style={{ display: "none", position: "absolute", left: "-9999px" }}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="content"
|
||||
@@ -268,7 +322,7 @@ export default function Home() {
|
||||
<Button
|
||||
type="submit"
|
||||
className="font-bold tracking-wide flex-shrink-0"
|
||||
disabled={submitIdea.isPending}
|
||||
disabled={submitIdea.isPending || (HCAPTCHA_SITE_KEY ? !captchaToken : false)}
|
||||
data-testid="button-submit-idea"
|
||||
>
|
||||
{submitIdea.isPending ? (
|
||||
@@ -278,6 +332,21 @@ export default function Home() {
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* hCaptcha — activé uniquement si VITE_HCAPTCHA_SITE_KEY est défini */}
|
||||
{HCAPTCHA_SITE_KEY && (
|
||||
<div className="flex justify-start">
|
||||
<HCaptcha
|
||||
ref={captchaRef}
|
||||
sitekey={HCAPTCHA_SITE_KEY}
|
||||
onVerify={(token) => setCaptchaToken(token)}
|
||||
onExpire={() => setCaptchaToken(null)}
|
||||
onError={() => setCaptchaToken(null)}
|
||||
size="compact"
|
||||
languageOverride="fr"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user