feat: gestion des utilisateurs, rôles et équipes
- Ajout de la section de gestion des utilisateurs avec rôles (Admin, Trésorerie, Agent de Saisie). - Restriction des accès par rôle (Admin a tout, Trésorerie a adhérents/paiements, Agent de saisie a adhérents/planning). - Flux de modification obligatoire de mot de passe temporaire au premier login (par défaut AsTalange123). - Intégration de la configuration des équipes du club.
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
package com.astalange.web.config;
|
||||
|
||||
import com.astalange.core.security.CustomUserDetails;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
@Component
|
||||
public class PasswordChangeInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth != null && auth.isAuthenticated() && auth.getPrincipal() instanceof CustomUserDetails) {
|
||||
CustomUserDetails userDetails = (CustomUserDetails) auth.getPrincipal();
|
||||
if (userDetails.isMustChangePassword()) {
|
||||
String uri = request.getRequestURI();
|
||||
// Exclude /change-password, /logout, static assets, error pages
|
||||
if (!uri.equals("/change-password") &&
|
||||
!uri.equals("/logout") &&
|
||||
!uri.startsWith("/css/") &&
|
||||
!uri.startsWith("/js/") &&
|
||||
!uri.startsWith("/images/") &&
|
||||
!uri.equals("/error")) {
|
||||
response.sendRedirect("/change-password");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,11 @@ public class SecurityConfig {
|
||||
http
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
|
||||
.requestMatchers("/change-password").authenticated()
|
||||
.requestMatchers("/utilisateurs/**", "/saisons/**", "/categories/**", "/equipes/**", "/modespaiement/**", "/equipements/**").hasRole("ADMIN")
|
||||
.requestMatchers("/paiements/**", "/paiement/**").hasAnyRole("ADMIN", "TRESORERIE")
|
||||
.requestMatchers("/planning/**").hasAnyRole("ADMIN", "AGENT_SAISIE")
|
||||
.requestMatchers("/adherents/**", "/dotations/**").hasAnyRole("ADMIN", "TRESORERIE", "AGENT_SAISIE")
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin(form -> form
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.astalange.web.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
private final PasswordChangeInterceptor passwordChangeInterceptor;
|
||||
|
||||
public WebConfig(PasswordChangeInterceptor passwordChangeInterceptor) {
|
||||
this.passwordChangeInterceptor = passwordChangeInterceptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(passwordChangeInterceptor);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,11 @@ public class AdherentController {
|
||||
this.saisonRepository = saisonRepository;
|
||||
}
|
||||
|
||||
@org.springframework.web.bind.annotation.ModelAttribute("equipes")
|
||||
public List<com.astalange.core.entity.Equipe> populateEquipes() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String listAdherents(
|
||||
@org.springframework.web.bind.annotation.RequestParam(required = false) String query,
|
||||
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package com.astalange.web.controller;
|
||||
|
||||
import com.astalange.core.entity.AppUser;
|
||||
import com.astalange.core.repository.AppUserRepository;
|
||||
import com.astalange.core.security.CustomUserDetails;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
@Controller
|
||||
public class ChangePasswordController {
|
||||
|
||||
private final AppUserRepository userRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
public ChangePasswordController(AppUserRepository userRepository, PasswordEncoder passwordEncoder) {
|
||||
this.userRepository = userRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
@GetMapping("/change-password")
|
||||
public String showChangePasswordForm() {
|
||||
return "change-password";
|
||||
}
|
||||
|
||||
@PostMapping("/change-password")
|
||||
public String changePassword(
|
||||
@RequestParam String newPassword,
|
||||
@RequestParam String confirmPassword,
|
||||
Model model) {
|
||||
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth == null || !(auth.getPrincipal() instanceof CustomUserDetails)) {
|
||||
return "redirect:/login";
|
||||
}
|
||||
|
||||
CustomUserDetails userDetails = (CustomUserDetails) auth.getPrincipal();
|
||||
|
||||
if (!newPassword.equals(confirmPassword)) {
|
||||
model.addAttribute("error", "Les mots de passe ne correspondent pas.");
|
||||
return "change-password";
|
||||
}
|
||||
|
||||
if (newPassword.equals("AsTalange123")) {
|
||||
model.addAttribute("error", "Le nouveau mot de passe doit être différent de AsTalange123.");
|
||||
return "change-password";
|
||||
}
|
||||
|
||||
if (newPassword.length() < 6) {
|
||||
model.addAttribute("error", "Le mot de passe doit contenir au moins 6 caractères.");
|
||||
return "change-password";
|
||||
}
|
||||
|
||||
AppUser appUser = userRepository.findById(userDetails.getId())
|
||||
.orElseThrow(() -> new IllegalStateException("Utilisateur non trouvé"));
|
||||
|
||||
appUser.setPassword(passwordEncoder.encode(newPassword));
|
||||
appUser.setMustChangePassword(false);
|
||||
userRepository.save(appUser);
|
||||
|
||||
// Update Authentication in Security Context
|
||||
CustomUserDetails newPrincipal = new CustomUserDetails(
|
||||
appUser.getId(),
|
||||
appUser.getUsername(),
|
||||
appUser.getPassword(),
|
||||
appUser.isEnabled(),
|
||||
false,
|
||||
auth.getAuthorities()
|
||||
);
|
||||
UsernamePasswordAuthenticationToken newAuth = new UsernamePasswordAuthenticationToken(
|
||||
newPrincipal, auth.getCredentials(), auth.getAuthorities()
|
||||
);
|
||||
SecurityContextHolder.getContext().setAuthentication(newAuth);
|
||||
|
||||
return "redirect:/?passwordChanged=true";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.astalange.web.controller;
|
||||
|
||||
import com.astalange.core.entity.Categorie;
|
||||
import com.astalange.core.entity.Equipe;
|
||||
import com.astalange.core.entity.Saison;
|
||||
import com.astalange.core.repository.CategorieRepository;
|
||||
import com.astalange.core.repository.EquipeRepository;
|
||||
import com.astalange.core.repository.SaisonRepository;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Controller
|
||||
public class EquipeController {
|
||||
|
||||
private final EquipeRepository equipeRepository;
|
||||
private final CategorieRepository categorieRepository;
|
||||
private final SaisonRepository saisonRepository;
|
||||
|
||||
public EquipeController(EquipeRepository equipeRepository, CategorieRepository categorieRepository, SaisonRepository saisonRepository) {
|
||||
this.equipeRepository = equipeRepository;
|
||||
this.categorieRepository = categorieRepository;
|
||||
this.saisonRepository = saisonRepository;
|
||||
}
|
||||
|
||||
@GetMapping("/categories/equipes")
|
||||
public String getEquipesByCategorie(@RequestParam(required = false) Long categorieId, Model model) {
|
||||
if (categorieId != null) {
|
||||
model.addAttribute("equipes", equipeRepository.findByCategorieId(categorieId));
|
||||
} else {
|
||||
model.addAttribute("equipes", List.of());
|
||||
}
|
||||
return "adherents/form :: equipe-options";
|
||||
}
|
||||
|
||||
@GetMapping("/equipes")
|
||||
public String listEquipes(Model model) {
|
||||
Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null);
|
||||
if (saisonActive != null) {
|
||||
model.addAttribute("categories", categorieRepository.findBySaison(saisonActive));
|
||||
model.addAttribute("equipes", equipeRepository.findBySaison(saisonActive));
|
||||
model.addAttribute("saisonActive", saisonActive);
|
||||
} else {
|
||||
model.addAttribute("categories", categorieRepository.findAll());
|
||||
model.addAttribute("equipes", equipeRepository.findAll());
|
||||
}
|
||||
return "parametrage/equipes_list";
|
||||
}
|
||||
|
||||
@PostMapping("/equipes")
|
||||
public String saveEquipe(@RequestParam String nom, @RequestParam Long categorieId) {
|
||||
Categorie categorie = categorieRepository.findById(categorieId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Catégorie invalide : " + categorieId));
|
||||
Saison saison = categorie.getSaison();
|
||||
if (saison == null) {
|
||||
saison = saisonRepository.findByEstActiveTrue()
|
||||
.orElseThrow(() -> new IllegalStateException("Aucune saison active configurée"));
|
||||
}
|
||||
Equipe equipe = new Equipe(nom, categorie, saison);
|
||||
equipeRepository.save(equipe);
|
||||
return "redirect:/equipes";
|
||||
}
|
||||
|
||||
@PostMapping("/equipes/{id}/delete")
|
||||
public String deleteEquipe(@PathVariable Long id) {
|
||||
Equipe equipe = equipeRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Équipe invalide : " + id));
|
||||
equipeRepository.delete(equipe);
|
||||
return "redirect:/equipes";
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,7 @@
|
||||
package com.astalange.web.controller;
|
||||
|
||||
import com.astalange.core.entity.Adherent;
|
||||
import com.astalange.core.entity.Categorie;
|
||||
import com.astalange.core.entity.Licence;
|
||||
import com.astalange.core.entity.Dotation;
|
||||
import com.astalange.core.entity.Equipement;
|
||||
import com.astalange.core.entity.Saison;
|
||||
import com.astalange.core.repository.AdherentRepository;
|
||||
import com.astalange.core.repository.CategorieRepository;
|
||||
import com.astalange.core.repository.LicenceRepository;
|
||||
import com.astalange.core.repository.DotationRepository;
|
||||
import com.astalange.core.repository.EquipementRepository;
|
||||
import com.astalange.core.repository.SaisonRepository;
|
||||
import com.astalange.core.entity.*;
|
||||
import com.astalange.core.repository.*;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -28,19 +18,22 @@ public class LicenceController {
|
||||
private final EquipementRepository equipementRepository;
|
||||
private final DotationRepository dotationRepository;
|
||||
private final SaisonRepository saisonRepository;
|
||||
private final EquipeRepository equipeRepository;
|
||||
|
||||
public LicenceController(AdherentRepository adherentRepository,
|
||||
CategorieRepository categorieRepository,
|
||||
LicenceRepository licenceRepository,
|
||||
EquipementRepository equipementRepository,
|
||||
DotationRepository dotationRepository,
|
||||
SaisonRepository saisonRepository) {
|
||||
SaisonRepository saisonRepository,
|
||||
EquipeRepository equipeRepository) {
|
||||
this.adherentRepository = adherentRepository;
|
||||
this.categorieRepository = categorieRepository;
|
||||
this.licenceRepository = licenceRepository;
|
||||
this.equipementRepository = equipementRepository;
|
||||
this.dotationRepository = dotationRepository;
|
||||
this.saisonRepository = saisonRepository;
|
||||
this.equipeRepository = equipeRepository;
|
||||
}
|
||||
|
||||
@PostMapping("/adherents/{id}/licences")
|
||||
@@ -48,7 +41,8 @@ public class LicenceController {
|
||||
@PathVariable Long id,
|
||||
@RequestParam Long categorieId,
|
||||
@RequestParam String etat,
|
||||
@RequestParam(required = false) String numeroLicence) {
|
||||
@RequestParam(required = false) String numeroLicence,
|
||||
@RequestParam(required = false) Long equipeId) {
|
||||
|
||||
Adherent adherent = adherentRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid Adherent ID"));
|
||||
@@ -71,6 +65,12 @@ public class LicenceController {
|
||||
licence.setEtat(etat);
|
||||
licence.setNumeroLicence(numeroLicence);
|
||||
|
||||
if (equipeId != null) {
|
||||
Equipe equipe = equipeRepository.findById(equipeId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid Equipe ID"));
|
||||
licence.setEquipe(equipe);
|
||||
}
|
||||
|
||||
licence = licenceRepository.save(licence);
|
||||
|
||||
// Auto-initialize dotations for all configured equipments of the active season
|
||||
@@ -96,7 +96,8 @@ public class LicenceController {
|
||||
@PathVariable Long licenceId,
|
||||
@RequestParam Long categorieId,
|
||||
@RequestParam String etat,
|
||||
@RequestParam(required = false) String numeroLicence) {
|
||||
@RequestParam(required = false) String numeroLicence,
|
||||
@RequestParam(required = false) Long equipeId) {
|
||||
|
||||
Licence licence = licenceRepository.findById(licenceId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid Licence ID"));
|
||||
@@ -108,6 +109,14 @@ public class LicenceController {
|
||||
licence.setEtat(etat);
|
||||
licence.setNumeroLicence(numeroLicence);
|
||||
|
||||
if (equipeId != null) {
|
||||
Equipe equipe = equipeRepository.findById(equipeId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid Equipe ID"));
|
||||
licence.setEquipe(equipe);
|
||||
} else {
|
||||
licence.setEquipe(null);
|
||||
}
|
||||
|
||||
licenceRepository.save(licence);
|
||||
|
||||
return "redirect:/adherents/" + adherentId + "/edit";
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.astalange.web.controller;
|
||||
import com.astalange.core.entity.*;
|
||||
import com.astalange.core.repository.CategorieRepository;
|
||||
import com.astalange.core.repository.CreneauEntrainementRepository;
|
||||
import com.astalange.core.repository.EquipeRepository;
|
||||
import com.astalange.core.repository.SaisonRepository;
|
||||
import com.astalange.core.repository.TerrainRepository;
|
||||
import org.springframework.stereotype.Controller;
|
||||
@@ -21,15 +22,18 @@ public class PlanningController {
|
||||
private final TerrainRepository terrainRepository;
|
||||
private final CategorieRepository categorieRepository;
|
||||
private final SaisonRepository saisonRepository;
|
||||
private final EquipeRepository equipeRepository;
|
||||
|
||||
public PlanningController(CreneauEntrainementRepository creneauRepository,
|
||||
TerrainRepository terrainRepository,
|
||||
CategorieRepository categorieRepository,
|
||||
SaisonRepository saisonRepository) {
|
||||
SaisonRepository saisonRepository,
|
||||
EquipeRepository equipeRepository) {
|
||||
this.creneauRepository = creneauRepository;
|
||||
this.terrainRepository = terrainRepository;
|
||||
this.categorieRepository = categorieRepository;
|
||||
this.saisonRepository = saisonRepository;
|
||||
this.equipeRepository = equipeRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@@ -92,6 +96,7 @@ public class PlanningController {
|
||||
model.addAttribute("terrains", terrainRepository.findAll());
|
||||
model.addAttribute("zones", ZoneTerrain.values());
|
||||
model.addAttribute("jours", JourSemaine.values());
|
||||
model.addAttribute("equipes", List.of());
|
||||
|
||||
return "planning/form";
|
||||
}
|
||||
@@ -107,6 +112,12 @@ public class PlanningController {
|
||||
model.addAttribute("zones", ZoneTerrain.values());
|
||||
model.addAttribute("jours", JourSemaine.values());
|
||||
|
||||
if (creneau.getCategorie() != null) {
|
||||
model.addAttribute("equipes", equipeRepository.findByCategorieId(creneau.getCategorie().getId()));
|
||||
} else {
|
||||
model.addAttribute("equipes", List.of());
|
||||
}
|
||||
|
||||
return "planning/form";
|
||||
}
|
||||
|
||||
@@ -122,6 +133,10 @@ public class PlanningController {
|
||||
}
|
||||
}
|
||||
|
||||
if (creneau.getEquipe() != null && creneau.getEquipe().getId() == null) {
|
||||
creneau.setEquipe(null);
|
||||
}
|
||||
|
||||
if (result.hasErrors()) {
|
||||
Saison saison = creneau.getSaison();
|
||||
if (saison == null) {
|
||||
@@ -132,6 +147,11 @@ public class PlanningController {
|
||||
model.addAttribute("terrains", terrainRepository.findAll());
|
||||
model.addAttribute("zones", ZoneTerrain.values());
|
||||
model.addAttribute("jours", JourSemaine.values());
|
||||
if (creneau.getCategorie() != null && creneau.getCategorie().getId() != null) {
|
||||
model.addAttribute("equipes", equipeRepository.findByCategorieId(creneau.getCategorie().getId()));
|
||||
} else {
|
||||
model.addAttribute("equipes", List.of());
|
||||
}
|
||||
return "planning/form";
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.astalange.web.controller;
|
||||
|
||||
import com.astalange.core.entity.AppUser;
|
||||
import com.astalange.core.entity.Role;
|
||||
import com.astalange.core.repository.AppUserRepository;
|
||||
import com.astalange.core.repository.RoleRepository;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
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.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/utilisateurs")
|
||||
public class UtilisateurController {
|
||||
|
||||
private final AppUserRepository userRepository;
|
||||
private final RoleRepository roleRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
public UtilisateurController(AppUserRepository userRepository, RoleRepository roleRepository, PasswordEncoder passwordEncoder) {
|
||||
this.userRepository = userRepository;
|
||||
this.roleRepository = roleRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String listUsers(Model model) {
|
||||
List<AppUser> users = userRepository.findAll();
|
||||
model.addAttribute("users", users);
|
||||
return "utilisateurs/list";
|
||||
}
|
||||
|
||||
@GetMapping("/ajouter")
|
||||
public String showAddForm(Model model) {
|
||||
model.addAttribute("username", "");
|
||||
model.addAttribute("defaultPassword", "AsTalange123");
|
||||
model.addAttribute("roles", roleRepository.findAll());
|
||||
return "utilisateurs/form";
|
||||
}
|
||||
|
||||
@PostMapping("/ajouter")
|
||||
public String addUser(
|
||||
@RequestParam String username,
|
||||
@RequestParam String password,
|
||||
@RequestParam Long roleId,
|
||||
RedirectAttributes redirectAttributes,
|
||||
Model model) {
|
||||
|
||||
if (userRepository.findByUsername(username).isPresent()) {
|
||||
model.addAttribute("error", "Nom d'utilisateur déjà utilisé.");
|
||||
model.addAttribute("username", username);
|
||||
model.addAttribute("defaultPassword", password);
|
||||
model.addAttribute("roles", roleRepository.findAll());
|
||||
return "utilisateurs/form";
|
||||
}
|
||||
|
||||
Role role = roleRepository.findById(roleId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Rôle invalide: " + roleId));
|
||||
|
||||
AppUser appUser = new AppUser();
|
||||
appUser.setUsername(username);
|
||||
appUser.setPassword(passwordEncoder.encode(password));
|
||||
appUser.setEnabled(true);
|
||||
appUser.setMustChangePassword(true);
|
||||
appUser.setRoles(Set.of(role));
|
||||
|
||||
userRepository.save(appUser);
|
||||
|
||||
redirectAttributes.addFlashAttribute("success", "Utilisateur créé avec succès !");
|
||||
return "redirect:/utilisateurs";
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/delete")
|
||||
public String deleteUser(@PathVariable Long id, RedirectAttributes redirectAttributes) {
|
||||
AppUser appUser = userRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Utilisateur non trouvé"));
|
||||
|
||||
org.springframework.security.core.Authentication auth = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth != null && auth.getName().equals(appUser.getUsername())) {
|
||||
redirectAttributes.addFlashAttribute("error", "Vous ne pouvez pas supprimer votre propre compte.");
|
||||
return "redirect:/utilisateurs";
|
||||
}
|
||||
|
||||
userRepository.delete(appUser);
|
||||
redirectAttributes.addFlashAttribute("success", "Utilisateur supprimé avec succès.");
|
||||
return "redirect:/utilisateurs";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user