Feature: Implement secure public pre-registration flow with CAPTCHA and HTMX dashboard
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m12s
AS Talange CI/CD Pipeline / Build & Run in Docker Container (push) Successful in 3m4s

- Created V16 Flyway migration for pre_inscription and token_pre_inscription tables.
- Added PreInscription and TokenPreInscription entities with respective repositories.
- Updated SecurityConfig to allow public access to /inscription-public/**.
- Implemented CaptchaValidationService to interact with Cloudflare Turnstile API.
- Created PublicInscriptionController for managing the QR code entry, CAPTCHA validation, ephemeral tokens, and public form submission.
- Added public Thymeleaf views (demarrer, formulaire, succes, lien-expire) with TailwindCSS.
- Developed AdminPreInscriptionController and PreInscriptionService to handle back-office validation and rejection workflows.
- Built the admin pre-inscriptions dashboard list view.
- Added a dynamic, polling notification badge in the sidebar using HTMX, auto-updating on validation/rejection events.
This commit is contained in:
2026-06-11 23:11:34 +02:00
parent bca84be16a
commit 2e8298b3b6
16 changed files with 669 additions and 1 deletions
@@ -0,0 +1,101 @@
package com.astalange.web.controller;
import com.astalange.core.entity.PreInscription;
import com.astalange.core.entity.TokenPreInscription;
import com.astalange.core.entity.Saison;
import com.astalange.core.repository.PreInscriptionRepository;
import com.astalange.core.repository.TokenPreInscriptionRepository;
import com.astalange.core.repository.SaisonRepository;
import com.astalange.core.service.CaptchaValidationService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.time.LocalDateTime;
import java.util.UUID;
@Controller
@RequestMapping("/inscription-public")
public class PublicInscriptionController {
private final CaptchaValidationService captchaValidationService;
private final TokenPreInscriptionRepository tokenRepository;
private final PreInscriptionRepository preInscriptionRepository;
private final SaisonRepository saisonRepository;
public PublicInscriptionController(CaptchaValidationService captchaValidationService,
TokenPreInscriptionRepository tokenRepository,
PreInscriptionRepository preInscriptionRepository,
SaisonRepository saisonRepository) {
this.captchaValidationService = captchaValidationService;
this.tokenRepository = tokenRepository;
this.preInscriptionRepository = preInscriptionRepository;
this.saisonRepository = saisonRepository;
}
@GetMapping("/demarrer")
public String showDemarrer() {
return "public/demarrer";
}
@PostMapping("/demarrer")
public String processDemarrer(@RequestParam("cf-turnstile-response") String captchaResponse, RedirectAttributes redirectAttributes) {
// En vrai, captchaResponse dépend du fournisseur. Cloudflare Turnstile = cf-turnstile-response
if (!captchaValidationService.validateCaptcha(captchaResponse)) {
redirectAttributes.addFlashAttribute("error", "Validation CAPTCHA échouée. Veuillez réessayer.");
return "redirect:/inscription-public/demarrer";
}
TokenPreInscription token = new TokenPreInscription();
token.setValeurUuid(UUID.randomUUID().toString());
token.setDateExpiration(LocalDateTime.now().plusMinutes(10));
tokenRepository.save(token);
return "redirect:/inscription-public/formulaire?token=" + token.getValeurUuid();
}
@GetMapping("/formulaire")
public String showFormulaire(@RequestParam("token") String tokenUuid, Model model) {
TokenPreInscription token = tokenRepository.findByValeurUuid(tokenUuid).orElse(null);
if (token == null || !token.isValide()) {
return "public/lien-expire";
}
model.addAttribute("preInscription", new PreInscription());
model.addAttribute("tokenUuid", tokenUuid);
return "public/formulaire";
}
@PostMapping("/formulaire")
public String processFormulaire(@RequestParam("token") String tokenUuid,
@ModelAttribute("preInscription") PreInscription preInscription,
RedirectAttributes redirectAttributes) {
TokenPreInscription token = tokenRepository.findByValeurUuid(tokenUuid).orElse(null);
if (token == null || !token.isValide()) {
return "public/lien-expire";
}
Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null);
if (activeSaison == null) {
redirectAttributes.addFlashAttribute("error", "Les inscriptions sont actuellement fermées.");
return "redirect:/inscription-public/demarrer";
}
// Sauvegarde de la pré-inscription
preInscription.setSaison(activeSaison);
preInscription.setStatutTraite(false);
preInscriptionRepository.save(preInscription);
// Invalidation du token
token.setEstUtilise(true);
tokenRepository.save(token);
return "redirect:/inscription-public/succes";
}
@GetMapping("/succes")
public String showSucces() {
return "public/succes";
}
}