From f297ae557b42179ea30850e2c26a11c6ff7e25df Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Tue, 16 Jul 2024 22:08:02 +0200 Subject: [PATCH] feat: club remove --- .../ffsaf/data/model/ClubModel.java | 2 +- .../domain/service/AffiliationService.java | 25 +++++- .../ffsaf/domain/service/ClubService.java | 32 ++++++- .../ffsaf/domain/service/KeycloakService.java | 84 ++++++++++++++----- .../ffsaf/domain/service/MembreService.java | 2 +- .../titionfire/ffsaf/rest/ClubEndpoints.java | 11 ++- .../ffsaf/rest/data/SimpleClub.java | 2 + .../rest/from/AffiliationRequestForm.java | 6 +- .../ffsaf/rest/from/FullClubForm.java | 3 + src/main/webapp/src/pages/DemandeAff.jsx | 26 +++++- .../webapp/src/pages/admin/club/ClubPage.jsx | 10 ++- 11 files changed, 165 insertions(+), 38 deletions(-) diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/ClubModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/ClubModel.java index d65b176..72a30de 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/ClubModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/ClubModel.java @@ -55,6 +55,6 @@ public class ClubModel { boolean international; - @OneToMany(mappedBy = "club", fetch = FetchType.LAZY) + @OneToMany(mappedBy = "club", fetch = FetchType.LAZY, cascade = CascadeType.ALL) List affiliations; } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java index 31cf2bb..b55c796 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java @@ -55,15 +55,29 @@ public class AffiliationService { public Uni save(AffiliationRequestForm form) { AffiliationRequestModel affModel = form.toModel(); - affModel.setSaison(Utils.getSaison()); + int currentSaison = Utils.getSaison(); // noinspection ResultOfMethodCallIgnored - return repositoryRequest.count("siret = ?1 and saison = ?2", affModel.getSiret(), affModel.getSaison()) + return Uni.createFrom().item(affModel) + .invoke(Unchecked.consumer(model -> { + if (model.getSaison() != currentSaison && model.getSaison() != currentSaison + 1) { + throw new IllegalArgumentException("Saison not valid"); + } + })) + .chain(() -> repositoryRequest.count("siret = ?1 and saison = ?2", affModel.getSiret(), + affModel.getSaison())) .onItem().invoke(Unchecked.consumer(count -> { if (count != 0) { throw new IllegalArgumentException("Affiliation request already exists"); } })) + .chain(() -> clubRepository.find("SIRET = ?1", affModel.getSiret()).firstResult().chain(club -> + repository.count("club = ?1 and saison = ?2", club, affModel.getSaison()))) + .onItem().invoke(Unchecked.consumer(count -> { + if (count != 0) { + throw new IllegalArgumentException("Affiliation already exists"); + } + })) .map(o -> affModel) .call(model -> ((model.getM1_lincence() != -1) ? combRepository.find("licence", model.getM1_lincence()).count().invoke(count -> { @@ -289,7 +303,12 @@ public class AffiliationService { public Uni setAffiliation(long id, int saison) { return clubRepository.findById(id) - .onItem().ifNull().failWith(new NotFoundException("Affiliation request not found")) + .onItem().ifNull().failWith(new NotFoundException("Club non trouver")) + .invoke(Unchecked.consumer(club -> { + if (club.getAffiliations().stream().anyMatch(affiliation -> affiliation.getSaison() == saison)) { + throw new IllegalArgumentException("Affiliation deja existante"); + } + })) .chain(club -> Panache.withTransaction(() -> repository.persist(new AffiliationModel(null, club, saison)))) .map(SimpleAffiliation::fromModel); diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/ClubService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/ClubService.java index 1e3d4e4..15d387a 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/ClubService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/ClubService.java @@ -4,12 +4,15 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import fr.titionfire.ffsaf.data.model.ClubModel; import fr.titionfire.ffsaf.data.repository.ClubRepository; +import fr.titionfire.ffsaf.data.repository.CombRepository; import fr.titionfire.ffsaf.net2.ServerCustom; import fr.titionfire.ffsaf.net2.data.SimpleClubModel; import fr.titionfire.ffsaf.net2.request.SReqClub; import fr.titionfire.ffsaf.rest.from.FullClubForm; import fr.titionfire.ffsaf.utils.Contact; import fr.titionfire.ffsaf.utils.PageResult; +import fr.titionfire.ffsaf.utils.RoleAsso; +import fr.titionfire.ffsaf.utils.Utils; import io.quarkus.hibernate.reactive.panache.Panache; import io.quarkus.hibernate.reactive.panache.PanacheQuery; import io.quarkus.hibernate.reactive.panache.common.WithSession; @@ -21,6 +24,7 @@ import io.smallrye.mutiny.unchecked.Unchecked; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.ws.rs.BadRequestException; +import org.eclipse.microprofile.config.inject.ConfigProperty; import org.hibernate.reactive.mutiny.Mutiny; import java.util.Collection; @@ -39,6 +43,15 @@ public class ClubService { @Inject ServerCustom serverCustom; + @Inject + CombRepository combRepository; + + @Inject + KeycloakService keycloakService; + + @ConfigProperty(name = "upload_dir") + String media; + public SimpleClubModel findByIdOptionalClub(long id) throws Throwable { return VertxContextSupport.subscribeAndAwait( () -> Panache.withTransaction(() -> repository.findById(id).map(SimpleClubModel::fromModel))); @@ -115,6 +128,7 @@ public class ClubService { m.setContact_intern(input.getContact_intern()); m.setRNA(input.getRna()); m.setSIRET(input.getSiret()); + m.setAddress(input.getAddress()); try { m.setContact(MAPPER.readValue(input.getContact(), typeRef)); @@ -134,6 +148,22 @@ public class ClubService { } public Uni delete(long id) { - return Uni.createFrom().nullItem(); + return repository.findById(id) + .chain(club -> combRepository.list("club = ?1", club) + .map(combModels -> combModels.stream().peek(combModel -> { + combModel.setClub(null); + combModel.setRole(RoleAsso.MEMBRE); + }).toList()) + .call(list -> Uni.join().all(list.stream().filter(m -> m.getUserId() != null) + .map(m -> keycloakService.clearUser(m.getUserId())).toList()).andCollectFailures()) + .chain(list -> Panache.withTransaction(() -> combRepository.persist(list))) + .map(o -> club) + ) + .call(clubModel -> (clubModel.getClubId() == null) ? Uni.createFrom() + .voidItem() : keycloakService.removeClubGroup(clubModel.getClubId())) + .invoke(membreModel -> SReqClub.sendRmIfNeed(serverCustom.clients, id)) + .chain(clubModel -> Panache.withTransaction(() -> repository.delete(clubModel))) + .call(__ -> Utils.deleteMedia(id, media, "ppClub")) + .call(__ -> Utils.deleteMedia(id, media, "clubStatus")); } } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/KeycloakService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/KeycloakService.java index 7a3c49d..cb83f09 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/KeycloakService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/KeycloakService.java @@ -49,21 +49,26 @@ public class KeycloakService { return vertx.getOrCreateContext().executeBlocking(() -> { GroupRepresentation clubGroup = keycloak.realm(realm).groups().groups().stream().filter(g -> g.getName().equals("club")) - .findAny().orElseThrow(() -> new KeycloakException("Fail to fetch group %s".formatted("club"))); + .findAny() + .orElseThrow(() -> new KeycloakException("Fail to fetch group %s".formatted("club"))); GroupRepresentation groupRepresentation = new GroupRepresentation(); groupRepresentation.setName(club.getId() + "-" + club.getName()); try (Response response = keycloak.realm(realm).groups().group(clubGroup.getId()).subGroup(groupRepresentation)) { - if (!response.getStatusInfo().equals(Response.Status.CREATED) && !response.getStatusInfo().equals(Response.Status.CONFLICT)) - throw new KeycloakException("Fail to set group parent for club: %s (reason=%s)".formatted(club.getName(), - response.getStatusInfo().getReasonPhrase())); + if (!response.getStatusInfo().equals(Response.Status.CREATED) && !response.getStatusInfo() + .equals(Response.Status.CONFLICT)) + throw new KeycloakException( + "Fail to set group parent for club: %s (reason=%s)".formatted(club.getName(), + response.getStatusInfo().getReasonPhrase())); } return keycloak.realm(realm).groups().group(clubGroup.getId()).getSubGroups(0, 1000, true).stream() - .filter(g -> g.getName().startsWith(club.getId() + "-")).findAny().map(GroupRepresentation::getId) - .orElseThrow(() -> new KeycloakException("Fail to fetch group %s*".formatted(club.getId() + "-"))); + .filter(g -> g.getName().startsWith(club.getId() + "-")).findAny() + .map(GroupRepresentation::getId) + .orElseThrow( + () -> new KeycloakException("Fail to fetch group %s*".formatted(club.getId() + "-"))); } ).call(id -> clubService.setClubId(club.getId(), id)); } @@ -72,21 +77,24 @@ public class KeycloakService { public Uni getUserFromMember(MembreModel membreModel) { if (membreModel.getUserId() == null) { - return Uni.createFrom().failure(new NullPointerException("No keycloak user linked to the user id=" + membreModel.getId())); + return Uni.createFrom() + .failure(new NullPointerException("No keycloak user linked to the user id=" + membreModel.getId())); } return Uni.createFrom().item(membreModel::getUserId); } public Uni setClubGroupMembre(MembreModel membreModel, ClubModel club) { return getGroupFromClub(club).chain( - clubId -> getUserFromMember(membreModel).chain(userId -> vertx.getOrCreateContext().executeBlocking(() -> { - UserResource user = keycloak.realm(realm).users().get(userId); - user.groups().stream().filter(g -> g.getPath().startsWith("/club")).forEach(g -> user.leaveGroup(g.getId())); - user.joinGroup(clubId); - LOGGER.infof("Set club \"%s\" to user %s (%s)", club.getName(), userId, - user.toRepresentation().getUsername()); - return "OK"; - }))); + clubId -> getUserFromMember(membreModel).chain( + userId -> vertx.getOrCreateContext().executeBlocking(() -> { + UserResource user = keycloak.realm(realm).users().get(userId); + user.groups().stream().filter(g -> g.getPath().startsWith("/club")) + .forEach(g -> user.leaveGroup(g.getId())); + user.joinGroup(clubId); + LOGGER.infof("Set club \"%s\" to user %s (%s)", club.getName(), userId, + user.toRepresentation().getUsername()); + return "OK"; + }))); } public Uni setEmail(String userId, String email) { @@ -104,13 +112,14 @@ public class KeycloakService { public Uni setAutoRoleMembre(String id, RoleAsso role, GradeArbitrage gradeArbitrage) { List toRemove = new ArrayList<>(List.of("club_president", "club_tresorier", "club_secretaire", - "asseseur", "arbitre")); + "club_respo_intra", "asseseur", "arbitre")); List toAdd = new ArrayList<>(); switch (role) { - case PRESIDENT -> toAdd.add("club_president"); - case TRESORIER -> toAdd.add("club_tresorier"); - case SECRETAIRE -> toAdd.add("club_secretaire"); + case PRESIDENT, VPRESIDENT -> toAdd.add("club_president"); + case TRESORIER, VTRESORIER -> toAdd.add("club_tresorier"); + case SECRETAIRE, VSECRETAIRE -> toAdd.add("club_secretaire"); + case MEMBREBUREAU -> toAdd.add("club_respo_intra"); } switch (gradeArbitrage) { case ARBITRE -> toAdd.addAll(List.of("asseseur", "arbitre")); @@ -132,7 +141,8 @@ public class KeycloakService { public Uni> fetchRole(String id) { return vertx.getOrCreateContext().executeBlocking(() -> - keycloak.realm(realm).users().get(id).roles().realmLevel().listEffective().stream().map(RoleRepresentation::getName).toList()); + keycloak.realm(realm).users().get(id).roles().realmLevel().listEffective().stream() + .map(RoleRepresentation::getName).toList()); } public Uni updateRole(String id, List toAdd, List toRemove) { @@ -184,13 +194,15 @@ public class KeycloakService { RequiredAction.UPDATE_PASSWORD.name())); try (Response response = keycloak.realm(realm).users().create(user)) { - if (!response.getStatusInfo().equals(Response.Status.CREATED) && !response.getStatusInfo().equals(Response.Status.CONFLICT)) + if (!response.getStatusInfo().equals(Response.Status.CREATED) && !response.getStatusInfo() + .equals(Response.Status.CONFLICT)) throw new KeycloakException("Fail to creat user %s (reason=%s)".formatted(login, response.getStatusInfo().getReasonPhrase())); } String finalLogin = login; - return getUser(login).orElseThrow(() -> new KeycloakException("Fail to fetch user %s".formatted(finalLogin))); + return getUser(login).orElseThrow( + () -> new KeycloakException("Fail to fetch user %s".formatted(finalLogin))); }) //.invoke(user -> keycloak.realm(realm).users().get(user.getId()) // TODO enable for production // .executeActionsEmail(List.of(RequiredAction.VERIFY_EMAIL.name(), @@ -216,6 +228,30 @@ public class KeycloakService { }); } + public Uni removeClubGroup(String clubId) { + return vertx.getOrCreateContext().executeBlocking(() -> { + keycloak.realm(realm).groups().group(clubId).remove(); + return null; + }); + } + + public Uni clearUser(String userId) { + List toRemove = new ArrayList<>( + List.of("club_president", "club_tresorier", "club_secretaire", "club_respo_intra")); + + return vertx.getOrCreateContext().executeBlocking(() -> { + UserResource user = keycloak.realm(realm).users().get(userId); + + RoleScopeResource resource = user.roles().realmLevel(); + List roles = keycloak.realm(realm).roles().list(); + resource.remove(roles.stream().filter(r -> toRemove.contains(r.getName())).toList()); + + user.groups().stream().filter(g -> g.getPath().startsWith("/club")) + .forEach(g -> user.leaveGroup(g.getId())); + return "OK"; + }); + } + private Optional getUser(String username) { List users = keycloak.realm(realm).users().searchByUsername(username, true); @@ -226,7 +262,9 @@ public class KeycloakService { } private String makeLogin(MembreModel model) { - return Normalizer.normalize((model.getFname().toLowerCase() + "." + model.getLname().toLowerCase()).replace(' ', '_'), Normalizer.Form.NFD) + return Normalizer.normalize( + (model.getFname().toLowerCase() + "." + model.getLname().toLowerCase()).replace(' ', '_'), + Normalizer.Form.NFD) .replaceAll("\\p{M}", ""); } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java index decc099..dda7cc8 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java @@ -150,7 +150,7 @@ public class MembreService { RoleAsso source = RoleAsso.MEMBRE; if (securityIdentity.getRoles().contains("club_president")) source = RoleAsso.PRESIDENT; else if (securityIdentity.getRoles().contains("club_secretaire")) source = RoleAsso.SECRETAIRE; - else if (securityIdentity.getRoles().contains("club_respo_intra")) source = RoleAsso.SECRETAIRE; + else if (securityIdentity.getRoles().contains("club_respo_intra")) source = RoleAsso.MEMBREBUREAU; if (!membre.getRole().equals(membreModel.getRole()) && membre.getRole().level > source.level) throw new ForbiddenException(); })) diff --git a/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java index 45a8199..7210cac 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java @@ -159,8 +159,15 @@ public class ClubEndpoints { @GET @Path("{id}/status") @RolesAllowed({"federation_admin", "club_president", "club_secretaire"}) - public Uni getStatus(@PathParam("id") long id) throws URISyntaxException { - return Utils.getMediaFile(id, media, "clubStatus", clubService.getById(id).onItem().invoke(checkPerm)); + public Uni getStatus(@PathParam("id") long id) { + return clubService.getById(id).onItem().invoke(checkPerm).chain(Unchecked.function(clubModel -> { + try { + return Utils.getMediaFile(clubModel.getId(), media, "clubStatus", + "statue-" + clubModel.getName() + ".pdf", Uni.createFrom().nullItem()); + } catch (URISyntaxException e) { + throw new InternalError(); + } + })); } } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleClub.java b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleClub.java index 44ba58f..d81195c 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleClub.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleClub.java @@ -25,6 +25,7 @@ public class SimpleClub { private String training_location; private String training_day_time; private String contact_intern; + private String address; private String RNA; private Long SIRET; private Long no_affiliation; @@ -48,6 +49,7 @@ public class SimpleClub { .SIRET(model.getSIRET()) .no_affiliation(model.getNo_affiliation()) .international(model.isInternational()) + .address(model.getAddress()) .build(); } } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestForm.java b/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestForm.java index 4b61818..7e83750 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestForm.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestForm.java @@ -13,15 +13,14 @@ import org.jboss.resteasy.reactive.PartType; public class AffiliationRequestForm { @FormParam("name") private String name = null; - @FormParam("siret") private Long siret = null; - @FormParam("rna") private String rna = null; - @FormParam("adresse") private String adresse = null; + @FormParam("saison") + private int saison = -1; @FormParam("status") @PartType(MediaType.APPLICATION_OCTET_STREAM) @@ -70,6 +69,7 @@ public class AffiliationRequestForm { model.setSiret(this.getSiret()); model.setRNA(this.getRna()); model.setAddress(this.getAdresse()); + model.setSaison(this.getSaison()); model.setM1_lname(this.getM1_lname()); model.setM1_fname(this.getM1_fname()); diff --git a/src/main/java/fr/titionfire/ffsaf/rest/from/FullClubForm.java b/src/main/java/fr/titionfire/ffsaf/rest/from/FullClubForm.java index 2ad0d6f..a8a407a 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/from/FullClubForm.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/from/FullClubForm.java @@ -30,6 +30,9 @@ public class FullClubForm { @FormParam("contact_intern") private String contact_intern = null; + @FormParam("address") + private String address = null; + @FormParam("rna") private String rna = null; diff --git a/src/main/webapp/src/pages/DemandeAff.jsx b/src/main/webapp/src/pages/DemandeAff.jsx index 2290d55..722c917 100644 --- a/src/main/webapp/src/pages/DemandeAff.jsx +++ b/src/main/webapp/src/pages/DemandeAff.jsx @@ -1,5 +1,5 @@ import {useState} from "react"; -import {apiAxios} from "../utils/Tools.js"; +import {apiAxios, getSaison} from "../utils/Tools.js"; import {toast} from "react-toastify"; import {useNavigate} from "react-router-dom"; import {RoleList} from "../components/MemberCustomFiels.jsx"; @@ -130,6 +130,7 @@ function AssoInfo() { const [rna, setRna] = useState("") const [rnaEnable, setRnaEnable] = useState(false) const [adresse, setAdresse] = useState("") + const [saison, setSaison] = useState(getSaison()) const fetchSiret = () => { if (siret.length < 14) { @@ -153,7 +154,26 @@ function AssoInfo() { }) } + const currentSaison = getSaison(); + return <> +
+
+ setSaison(Number(e.target.value))}/> + {currentSaison + "-" + (currentSaison + 1)} +
+ OU +
+ setSaison(Number(e.target.value))}/> + {(currentSaison + 1) + "-" + (currentSaison + 2)} +
+
+
Nom de l'association*
- Adresse de contact* - Adresse administrative* + setAdresse(e.target.value)}/>
diff --git a/src/main/webapp/src/pages/admin/club/ClubPage.jsx b/src/main/webapp/src/pages/admin/club/ClubPage.jsx index feb7ceb..f317ddf 100644 --- a/src/main/webapp/src/pages/admin/club/ClubPage.jsx +++ b/src/main/webapp/src/pages/admin/club/ClubPage.jsx @@ -12,6 +12,8 @@ import {useRef, useState} from "react"; import {LocationEditor, LocationEditorModal} from "../../../components/Club/LocationEditor.jsx"; import {ContactEditor} from "../../../components/Club/ContactEditor.jsx"; import {HoraireEditor} from "../../../components/Club/HoraireEditor.jsx"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {faFilePdf} from "@fortawesome/free-solid-svg-icons"; const vite_url = import.meta.env.VITE_URL; @@ -122,11 +124,17 @@ function InformationForm({data}) { +
- + + + +
Laissez vide pour ne rien changer.