From 8a15139efb01446636861a51b2202b2b1afb292c Mon Sep 17 00:00:00 2001 From: Youssef Date: Sat, 30 May 2026 23:11:12 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20gestion=20des=20utilisateurs,=20r=C3=B4?= =?UTF-8?q?les=20et=20=C3=A9quipes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- .../com/astalange/core/entity/AppUser.java | 3 + .../core/entity/CreneauEntrainement.java | 12 +++ .../com/astalange/core/entity/Equipe.java | 59 ++++++++++++ .../com/astalange/core/entity/Licence.java | 7 ++ .../core/repository/EquipeRepository.java | 13 +++ .../core/repository/RoleRepository.java | 9 ++ .../core/security/CustomUserDetails.java | 25 +++++ .../core/security/UserDetailsServiceImpl.java | 5 +- .../db/migration/V11__add_equipes.sql | 19 ++++ ...12__add_roles_and_must_change_password.sql | 2 + .../java/com/astalange/core/EquipeTest.java | 34 +++++++ .../web/config/PasswordChangeInterceptor.java | 36 +++++++ .../astalange/web/config/SecurityConfig.java | 5 + .../com/astalange/web/config/WebConfig.java | 20 ++++ .../web/controller/AdherentController.java | 5 + .../controller/ChangePasswordController.java | 83 +++++++++++++++++ .../web/controller/EquipeController.java | 73 +++++++++++++++ .../web/controller/LicenceController.java | 39 +++++--- .../web/controller/PlanningController.java | 22 ++++- .../web/controller/UtilisateurController.java | 92 ++++++++++++++++++ .../resources/templates/adherents/form.html | 61 +++++++++++- .../resources/templates/adherents/list.html | 3 +- .../resources/templates/change-password.html | 43 +++++++++ .../templates/fragments/sidebar.html | 29 ++++-- .../templates/parametrage/equipes_list.html | 92 ++++++++++++++++++ .../resources/templates/planning/form.html | 28 ++++++ .../templates/planning/planning.html | 4 +- .../templates/utilisateurs/form.html | 67 +++++++++++++ .../templates/utilisateurs/list.html | 93 +++++++++++++++++++ 29 files changed, 948 insertions(+), 35 deletions(-) create mode 100644 as-talange-core/src/main/java/com/astalange/core/entity/Equipe.java create mode 100644 as-talange-core/src/main/java/com/astalange/core/repository/EquipeRepository.java create mode 100644 as-talange-core/src/main/java/com/astalange/core/repository/RoleRepository.java create mode 100644 as-talange-core/src/main/java/com/astalange/core/security/CustomUserDetails.java create mode 100644 as-talange-core/src/main/resources/db/migration/V11__add_equipes.sql create mode 100644 as-talange-core/src/main/resources/db/migration/V12__add_roles_and_must_change_password.sql create mode 100644 as-talange-core/src/test/java/com/astalange/core/EquipeTest.java create mode 100644 as-talange-web/src/main/java/com/astalange/web/config/PasswordChangeInterceptor.java create mode 100644 as-talange-web/src/main/java/com/astalange/web/config/WebConfig.java create mode 100644 as-talange-web/src/main/java/com/astalange/web/controller/ChangePasswordController.java create mode 100644 as-talange-web/src/main/java/com/astalange/web/controller/EquipeController.java create mode 100644 as-talange-web/src/main/java/com/astalange/web/controller/UtilisateurController.java create mode 100644 as-talange-web/src/main/resources/templates/change-password.html create mode 100644 as-talange-web/src/main/resources/templates/parametrage/equipes_list.html create mode 100644 as-talange-web/src/main/resources/templates/utilisateurs/form.html create mode 100644 as-talange-web/src/main/resources/templates/utilisateurs/list.html diff --git a/as-talange-core/src/main/java/com/astalange/core/entity/AppUser.java b/as-talange-core/src/main/java/com/astalange/core/entity/AppUser.java index ea06dba..aeee454 100644 --- a/as-talange-core/src/main/java/com/astalange/core/entity/AppUser.java +++ b/as-talange-core/src/main/java/com/astalange/core/entity/AppUser.java @@ -26,6 +26,9 @@ public class AppUser { @Column(nullable = false) private boolean enabled = true; + @Column(name = "must_change_password", nullable = false) + private boolean mustChangePassword = false; + @ManyToMany(fetch = FetchType.EAGER) @JoinTable( name = "user_role", diff --git a/as-talange-core/src/main/java/com/astalange/core/entity/CreneauEntrainement.java b/as-talange-core/src/main/java/com/astalange/core/entity/CreneauEntrainement.java index bc029fc..e958446 100644 --- a/as-talange-core/src/main/java/com/astalange/core/entity/CreneauEntrainement.java +++ b/as-talange-core/src/main/java/com/astalange/core/entity/CreneauEntrainement.java @@ -42,6 +42,10 @@ public class CreneauEntrainement { @JoinColumn(name = "saison_id", nullable = false) private Saison saison; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "equipe_id") + private Equipe equipe; + @Transient private boolean enConflit = false; @@ -187,6 +191,14 @@ public class CreneauEntrainement { this.saison = saison; } + public Equipe getEquipe() { + return equipe; + } + + public void setEquipe(Equipe equipe) { + this.equipe = equipe; + } + public boolean isEnConflit() { return enConflit; } diff --git a/as-talange-core/src/main/java/com/astalange/core/entity/Equipe.java b/as-talange-core/src/main/java/com/astalange/core/entity/Equipe.java new file mode 100644 index 0000000..f54ab31 --- /dev/null +++ b/as-talange-core/src/main/java/com/astalange/core/entity/Equipe.java @@ -0,0 +1,59 @@ +package com.astalange.core.entity; + +import jakarta.persistence.*; +import java.util.Objects; + +@Entity +@Table(name = "equipe") +public class Equipe { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String nom; + + @ManyToOne(optional = false, fetch = FetchType.LAZY) + @JoinColumn(name = "categorie_id", nullable = false) + private Categorie categorie; + + @ManyToOne(optional = false, fetch = FetchType.LAZY) + @JoinColumn(name = "saison_id", nullable = false) + private Saison saison; + + // Constructors + public Equipe() {} + + public Equipe(String nom, Categorie categorie, Saison saison) { + this.nom = nom; + this.categorie = categorie; + this.saison = saison; + } + + // Getters and Setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getNom() { return nom; } + public void setNom(String nom) { this.nom = nom; } + + public Categorie getCategorie() { return categorie; } + public void setCategorie(Categorie categorie) { this.categorie = categorie; } + + public Saison getSaison() { return saison; } + public void setSaison(Saison saison) { this.saison = saison; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Equipe equipe = (Equipe) o; + return Objects.equals(id, equipe.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/as-talange-core/src/main/java/com/astalange/core/entity/Licence.java b/as-talange-core/src/main/java/com/astalange/core/entity/Licence.java index 42f4f47..d86bf03 100644 --- a/as-talange-core/src/main/java/com/astalange/core/entity/Licence.java +++ b/as-talange-core/src/main/java/com/astalange/core/entity/Licence.java @@ -25,6 +25,10 @@ public class Licence { @JoinColumn(name = "saison_id", nullable = false) private Saison saison; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "equipe_id") + private Equipe equipe; + @Column(nullable = false, length = 50) private String etat; @@ -93,6 +97,9 @@ public class Licence { public Saison getSaison() { return saison; } public void setSaison(Saison saison) { this.saison = saison; } + public Equipe getEquipe() { return equipe; } + public void setEquipe(Equipe equipe) { this.equipe = equipe; } + public String getEtat() { return etat; } public void setEtat(String etat) { this.etat = etat; } diff --git a/as-talange-core/src/main/java/com/astalange/core/repository/EquipeRepository.java b/as-talange-core/src/main/java/com/astalange/core/repository/EquipeRepository.java new file mode 100644 index 0000000..39c5435 --- /dev/null +++ b/as-talange-core/src/main/java/com/astalange/core/repository/EquipeRepository.java @@ -0,0 +1,13 @@ +package com.astalange.core.repository; + +import com.astalange.core.entity.Equipe; +import com.astalange.core.entity.Saison; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.List; + +@Repository +public interface EquipeRepository extends JpaRepository { + List findByCategorieId(Long categorieId); + List findBySaison(Saison saison); +} diff --git a/as-talange-core/src/main/java/com/astalange/core/repository/RoleRepository.java b/as-talange-core/src/main/java/com/astalange/core/repository/RoleRepository.java new file mode 100644 index 0000000..dc2d58a --- /dev/null +++ b/as-talange-core/src/main/java/com/astalange/core/repository/RoleRepository.java @@ -0,0 +1,9 @@ +package com.astalange.core.repository; + +import com.astalange.core.entity.Role; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + +public interface RoleRepository extends JpaRepository { + Optional findByName(String name); +} diff --git a/as-talange-core/src/main/java/com/astalange/core/security/CustomUserDetails.java b/as-talange-core/src/main/java/com/astalange/core/security/CustomUserDetails.java new file mode 100644 index 0000000..ddbdb18 --- /dev/null +++ b/as-talange-core/src/main/java/com/astalange/core/security/CustomUserDetails.java @@ -0,0 +1,25 @@ +package com.astalange.core.security; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.User; +import java.util.Collection; + +public class CustomUserDetails extends User { + private final Long id; + private final boolean mustChangePassword; + + public CustomUserDetails(Long id, String username, String password, boolean enabled, boolean mustChangePassword, + Collection authorities) { + super(username, password, enabled, true, true, true, authorities); + this.id = id; + this.mustChangePassword = mustChangePassword; + } + + public Long getId() { + return id; + } + + public boolean isMustChangePassword() { + return mustChangePassword; + } +} diff --git a/as-talange-core/src/main/java/com/astalange/core/security/UserDetailsServiceImpl.java b/as-talange-core/src/main/java/com/astalange/core/security/UserDetailsServiceImpl.java index f6be8a5..6f20d11 100644 --- a/as-talange-core/src/main/java/com/astalange/core/security/UserDetailsServiceImpl.java +++ b/as-talange-core/src/main/java/com/astalange/core/security/UserDetailsServiceImpl.java @@ -25,11 +25,12 @@ public class UserDetailsServiceImpl implements UserDetailsService { AppUser appUser = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("User not found")); - return new User( + return new CustomUserDetails( + appUser.getId(), appUser.getUsername(), appUser.getPassword(), appUser.isEnabled(), - true, true, true, + appUser.isMustChangePassword(), appUser.getRoles().stream() .map(role -> new SimpleGrantedAuthority(role.getName())) .collect(Collectors.toList()) diff --git a/as-talange-core/src/main/resources/db/migration/V11__add_equipes.sql b/as-talange-core/src/main/resources/db/migration/V11__add_equipes.sql new file mode 100644 index 0000000..776f2e3 --- /dev/null +++ b/as-talange-core/src/main/resources/db/migration/V11__add_equipes.sql @@ -0,0 +1,19 @@ +-- V11__add_equipes.sql + +-- 1. Table des équipes +CREATE TABLE equipe ( + id BIGSERIAL PRIMARY KEY, + nom VARCHAR(255) NOT NULL, + categorie_id BIGINT NOT NULL, + saison_id BIGINT NOT NULL, + CONSTRAINT fk_equipe_categorie FOREIGN KEY (categorie_id) REFERENCES categorie(id) ON DELETE CASCADE, + CONSTRAINT fk_equipe_saison FOREIGN KEY (saison_id) REFERENCES saison(id) ON DELETE CASCADE +); + +-- 2. Ajouter equipe_id à la table licence (nullable, facultative) +ALTER TABLE licence ADD COLUMN equipe_id BIGINT; +ALTER TABLE licence ADD CONSTRAINT fk_licence_equipe FOREIGN KEY (equipe_id) REFERENCES equipe(id) ON DELETE SET NULL; + +-- 3. Ajouter equipe_id à la table creneau_entrainement (nullable, facultative) +ALTER TABLE creneau_entrainement ADD COLUMN equipe_id BIGINT; +ALTER TABLE creneau_entrainement ADD CONSTRAINT fk_creneau_equipe FOREIGN KEY (equipe_id) REFERENCES equipe(id) ON DELETE SET NULL; diff --git a/as-talange-core/src/main/resources/db/migration/V12__add_roles_and_must_change_password.sql b/as-talange-core/src/main/resources/db/migration/V12__add_roles_and_must_change_password.sql new file mode 100644 index 0000000..940cc38 --- /dev/null +++ b/as-talange-core/src/main/resources/db/migration/V12__add_roles_and_must_change_password.sql @@ -0,0 +1,2 @@ +ALTER TABLE app_user ADD COLUMN must_change_password BOOLEAN NOT NULL DEFAULT FALSE; +INSERT INTO role (name) VALUES ('ROLE_TRESORERIE'), ('ROLE_AGENT_SAISIE') ON CONFLICT (name) DO NOTHING; diff --git a/as-talange-core/src/test/java/com/astalange/core/EquipeTest.java b/as-talange-core/src/test/java/com/astalange/core/EquipeTest.java new file mode 100644 index 0000000..e2930a1 --- /dev/null +++ b/as-talange-core/src/test/java/com/astalange/core/EquipeTest.java @@ -0,0 +1,34 @@ +package com.astalange.core; + +import com.astalange.core.entity.*; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class EquipeTest { + + @Test + public void testEquipeEntityAndAssociations() { + Categorie U15 = new Categorie(); + U15.setNom("U15"); + + Equipe equipeA = new Equipe(); + equipeA.setNom("Équipe 1"); + equipeA.setCategorie(U15); + + assertEquals("Équipe 1", equipeA.getNom()); + assertEquals(U15, equipeA.getCategorie()); + + Licence licence = new Licence(); + licence.setCategorie(U15); + licence.setEquipe(equipeA); + + assertEquals(equipeA, licence.getEquipe()); + + CreneauEntrainement creneau = new CreneauEntrainement(); + creneau.setCategorie(U15); + creneau.setEquipe(equipeA); + + assertEquals(equipeA, creneau.getEquipe()); + } +} diff --git a/as-talange-web/src/main/java/com/astalange/web/config/PasswordChangeInterceptor.java b/as-talange-web/src/main/java/com/astalange/web/config/PasswordChangeInterceptor.java new file mode 100644 index 0000000..d143102 --- /dev/null +++ b/as-talange-web/src/main/java/com/astalange/web/config/PasswordChangeInterceptor.java @@ -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; + } +} diff --git a/as-talange-web/src/main/java/com/astalange/web/config/SecurityConfig.java b/as-talange-web/src/main/java/com/astalange/web/config/SecurityConfig.java index 5cfaa6f..6632cff 100644 --- a/as-talange-web/src/main/java/com/astalange/web/config/SecurityConfig.java +++ b/as-talange-web/src/main/java/com/astalange/web/config/SecurityConfig.java @@ -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 diff --git a/as-talange-web/src/main/java/com/astalange/web/config/WebConfig.java b/as-talange-web/src/main/java/com/astalange/web/config/WebConfig.java new file mode 100644 index 0000000..41d8fb9 --- /dev/null +++ b/as-talange-web/src/main/java/com/astalange/web/config/WebConfig.java @@ -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); + } +} diff --git a/as-talange-web/src/main/java/com/astalange/web/controller/AdherentController.java b/as-talange-web/src/main/java/com/astalange/web/controller/AdherentController.java index 6bdc840..87a5e93 100644 --- a/as-talange-web/src/main/java/com/astalange/web/controller/AdherentController.java +++ b/as-talange-web/src/main/java/com/astalange/web/controller/AdherentController.java @@ -32,6 +32,11 @@ public class AdherentController { this.saisonRepository = saisonRepository; } + @org.springframework.web.bind.annotation.ModelAttribute("equipes") + public List populateEquipes() { + return List.of(); + } + @GetMapping public String listAdherents( @org.springframework.web.bind.annotation.RequestParam(required = false) String query, diff --git a/as-talange-web/src/main/java/com/astalange/web/controller/ChangePasswordController.java b/as-talange-web/src/main/java/com/astalange/web/controller/ChangePasswordController.java new file mode 100644 index 0000000..bdabf98 --- /dev/null +++ b/as-talange-web/src/main/java/com/astalange/web/controller/ChangePasswordController.java @@ -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"; + } +} diff --git a/as-talange-web/src/main/java/com/astalange/web/controller/EquipeController.java b/as-talange-web/src/main/java/com/astalange/web/controller/EquipeController.java new file mode 100644 index 0000000..ea20b4f --- /dev/null +++ b/as-talange-web/src/main/java/com/astalange/web/controller/EquipeController.java @@ -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"; + } +} diff --git a/as-talange-web/src/main/java/com/astalange/web/controller/LicenceController.java b/as-talange-web/src/main/java/com/astalange/web/controller/LicenceController.java index 6765819..13e0fdd 100644 --- a/as-talange-web/src/main/java/com/astalange/web/controller/LicenceController.java +++ b/as-talange-web/src/main/java/com/astalange/web/controller/LicenceController.java @@ -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"; diff --git a/as-talange-web/src/main/java/com/astalange/web/controller/PlanningController.java b/as-talange-web/src/main/java/com/astalange/web/controller/PlanningController.java index e79f172..c3fcbf9 100644 --- a/as-talange-web/src/main/java/com/astalange/web/controller/PlanningController.java +++ b/as-talange-web/src/main/java/com/astalange/web/controller/PlanningController.java @@ -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"; } diff --git a/as-talange-web/src/main/java/com/astalange/web/controller/UtilisateurController.java b/as-talange-web/src/main/java/com/astalange/web/controller/UtilisateurController.java new file mode 100644 index 0000000..b6f9c2e --- /dev/null +++ b/as-talange-web/src/main/java/com/astalange/web/controller/UtilisateurController.java @@ -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 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"; + } +} diff --git a/as-talange-web/src/main/resources/templates/adherents/form.html b/as-talange-web/src/main/resources/templates/adherents/form.html index 6bbd1b4..d2ac4bd 100644 --- a/as-talange-web/src/main/resources/templates/adherents/form.html +++ b/as-talange-web/src/main/resources/templates/adherents/form.html @@ -174,7 +174,7 @@ th:data-total-paye="${lic.getPrixTotal().subtract(lic.getResteAPayer())}"> 2024-2025 - U15 + U15 100.00 € (Catégorie) - @@ -193,7 +193,8 @@ th:data-categorie-id="${lic.categorie.id}" th:data-numero-licence="${lic.numeroLicence}" th:data-etat="${lic.etat}" - onclick="openLicenceModal(this.getAttribute('data-licence-id'), this.getAttribute('data-categorie-id'), this.getAttribute('data-numero-licence'), this.getAttribute('data-etat'))" + th:data-equipe-id="${lic.equipe != null ? lic.equipe.id : ''}" + onclick="openLicenceModal(this.getAttribute('data-licence-id'), this.getAttribute('data-categorie-id'), this.getAttribute('data-numero-licence'), this.getAttribute('data-etat'), this.getAttribute('data-equipe-id'))" class="text-blue-600 hover:text-blue-800 font-medium bg-blue-50 px-3 py-1 rounded-lg">Modifier + + + + + diff --git a/as-talange-web/src/main/resources/templates/fragments/sidebar.html b/as-talange-web/src/main/resources/templates/fragments/sidebar.html index 8d6b15c..3ebc938 100644 --- a/as-talange-web/src/main/resources/templates/fragments/sidebar.html +++ b/as-talange-web/src/main/resources/templates/fragments/sidebar.html @@ -1,5 +1,5 @@ - + @@ -11,14 +11,25 @@
Admin
diff --git a/as-talange-web/src/main/resources/templates/parametrage/equipes_list.html b/as-talange-web/src/main/resources/templates/parametrage/equipes_list.html new file mode 100644 index 0000000..09003af --- /dev/null +++ b/as-talange-web/src/main/resources/templates/parametrage/equipes_list.html @@ -0,0 +1,92 @@ + + + + + Paramétrage - Équipes + + + + + + +
+ +
+
+

Configuration des Équipes

+
+ +
+
+ + +
+

Créer une Équipe

+
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
+
+

Équipes Paramétrées

+ Saison Actuelle +
+
+ + + + + + + + + + + + + + + + + + + + +
Nom de l'équipeCatégorieSaisonActions
Aucune équipe configurée pour le moment. Usez du formulaire à gauche pour en créer une.
Équipe 1 + U15 + 2024-2025 +
+ +
+
+
+
+ +
+
+
+ + diff --git a/as-talange-web/src/main/resources/templates/planning/form.html b/as-talange-web/src/main/resources/templates/planning/form.html index c40c170..49304d4 100644 --- a/as-talange-web/src/main/resources/templates/planning/form.html +++ b/as-talange-web/src/main/resources/templates/planning/form.html @@ -33,6 +33,15 @@
+ +
+ + +
+
@@ -95,5 +104,24 @@
+ diff --git a/as-talange-web/src/main/resources/templates/planning/planning.html b/as-talange-web/src/main/resources/templates/planning/planning.html index a279c08..28e95e8 100644 --- a/as-talange-web/src/main/resources/templates/planning/planning.html +++ b/as-talange-web/src/main/resources/templates/planning/planning.html @@ -147,7 +147,7 @@ class="border-l-4 p-2 rounded shadow-sm flex flex-col justify-between overflow-hidden cursor-pointer hover:shadow transition-all relative z-10 m-0.5">
- U15 + U15
Modifier @@ -192,7 +192,7 @@ Aucun entraînement planifié pour ce jour. - U11 + U11 Terrain Synthétique Demi A diff --git a/as-talange-web/src/main/resources/templates/utilisateurs/form.html b/as-talange-web/src/main/resources/templates/utilisateurs/form.html new file mode 100644 index 0000000..d3ea99c --- /dev/null +++ b/as-talange-web/src/main/resources/templates/utilisateurs/form.html @@ -0,0 +1,67 @@ + + + + + Ajouter un Utilisateur - AS Talange + + + + + + + +
+ + +
+ +
+

Ajouter un Utilisateur

+ + ← Retour à la liste + +
+ + +
+ +
+
+ +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +

Le mot de passe par défaut est AsTalange123. L'utilisateur devra le modifier lors de sa première connexion.

+
+ + +
+
+ +
+
+ + + diff --git a/as-talange-web/src/main/resources/templates/utilisateurs/list.html b/as-talange-web/src/main/resources/templates/utilisateurs/list.html new file mode 100644 index 0000000..ff51508 --- /dev/null +++ b/as-talange-web/src/main/resources/templates/utilisateurs/list.html @@ -0,0 +1,93 @@ + + + + + Gestion des Utilisateurs - AS Talange + + + + + + + +
+ + +
+ +
+

Gestion des Utilisateurs

+ + Ajouter un utilisateur + +
+ + +
+ + +
+ +
+
+ +
+ + +
+ + + + + + + + + + + + + + + + + +
Nom d'utilisateurRôleStatutActions
+ Ucef + + + Admin + + + + Mot de passe temporaire + + + Actif + + + +
+ +
+ + Connecté + +
+
+ +
+
+ + +