diff --git a/src/main/java/fr/titionfire/ExampleResource.java b/src/main/java/fr/titionfire/ExampleResource.java index c7aee61..617736e 100644 --- a/src/main/java/fr/titionfire/ExampleResource.java +++ b/src/main/java/fr/titionfire/ExampleResource.java @@ -79,6 +79,11 @@ public class ExampleResource { response.append("
  • scopes: ").append(this.accessToken.toString()).append("
  • "); } + + if (scopes != null) { + response.append("
  • scopes: ").append(this.accessToken.getClaim("user_groups").toString()).append("
  • "); + } + if (scopes != null) { response.append("
  • getRoles: ").append(this.securityIdentity.getRoles()).append("
  • "); } diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/LicenceModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/LicenceModel.java index 989c25e..ae5d0e7 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/LicenceModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/LicenceModel.java @@ -18,7 +18,7 @@ public class LicenceModel { @GeneratedValue(strategy = GenerationType.IDENTITY) Long id; - @ManyToOne(fetch = FetchType.EAGER) + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "membre", referencedColumnName = "id") MembreModel membre; 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 bc8a9a4..df578b9 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/KeycloakService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/KeycloakService.java @@ -2,8 +2,7 @@ package fr.titionfire.ffsaf.domain.service; import fr.titionfire.ffsaf.data.model.ClubModel; import fr.titionfire.ffsaf.data.model.MembreModel; -import fr.titionfire.ffsaf.utils.KeycloakException; -import fr.titionfire.ffsaf.utils.RequiredAction; +import fr.titionfire.ffsaf.utils.*; import io.quarkus.runtime.annotations.RegisterForReflection; import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.unchecked.Unchecked; @@ -21,6 +20,7 @@ import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.UserRepresentation; import java.text.Normalizer; +import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -86,13 +86,42 @@ public class KeycloakService { }))); } - public Uni fetchCompte(String id) { + public Uni setEmail(String userId, String email) { + return vertx.getOrCreateContext().executeBlocking(() -> { + UserResource user = keycloak.realm(realm).users().get(userId); + UserRepresentation user2 = user.toRepresentation(); + if (email.equals(user2.getEmail())) + return ""; + user2.setEmail(email); + user2.setRequiredActions(List.of(RequiredAction.VERIFY_EMAIL.name())); + user.update(user2); + return ""; + }); + } + + public Uni setAutoRoleMembre(String id, RoleAsso role, GradeArbitrage gradeArbitrage) { + List toRemove = new ArrayList<>(List.of("club_president", "club_tresorier", "club_secretaire", "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"); + } + switch (gradeArbitrage) { + case ARBITRE -> toAdd.addAll(List.of("asseseur", "arbitre")); + case ASSESSEUR -> toAdd.add("asseseur"); + } + toRemove.removeAll(toAdd); + + return updateRole(id, toAdd, toRemove); + } + + public Uni> fetchCompte(String id) { return vertx.getOrCreateContext().executeBlocking(() -> { UserResource user = keycloak.realm(realm).users().get(id); UserRepresentation user2 = user.toRepresentation(); - return new UserCompteState(user2.isEnabled(), user2.getUsername(), user2.isEmailVerified(), - user.roles().realmLevel().listEffective().stream().map(RoleRepresentation::getName).toList(), - user.groups().stream().map(GroupRepresentation::getName).toList()); + return new Pair<>(user, new UserCompteState(user2.isEnabled(), user2.getUsername(), user2.isEmailVerified())) ; }); } @@ -182,7 +211,6 @@ public class KeycloakService { } @RegisterForReflection - public record UserCompteState(Boolean enabled, String login, Boolean emailVerified, List realmRoles, - List groups) { + public record UserCompteState(Boolean enabled, String login, Boolean emailVerified) { } } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/LicenceService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/LicenceService.java index b37aa03..15c9291 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/LicenceService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/LicenceService.java @@ -1,17 +1,22 @@ package fr.titionfire.ffsaf.domain.service; import fr.titionfire.ffsaf.data.model.LicenceModel; +import fr.titionfire.ffsaf.data.model.MembreModel; import fr.titionfire.ffsaf.data.repository.CombRepository; import fr.titionfire.ffsaf.data.repository.LicenceRepository; import fr.titionfire.ffsaf.rest.from.LicenceForm; +import fr.titionfire.ffsaf.utils.Utils; import io.quarkus.hibernate.reactive.panache.Panache; import io.quarkus.hibernate.reactive.panache.common.WithSession; import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.unchecked.Unchecked; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import jakarta.ws.rs.BadRequestException; import org.hibernate.reactive.mutiny.Mutiny; import java.util.List; +import java.util.function.Consumer; @WithSession @ApplicationScoped @@ -23,8 +28,8 @@ public class LicenceService { @Inject CombRepository combRepository; - public Uni> getLicence(long id) { - return combRepository.findById(id).chain(combRepository -> Mutiny.fetch(combRepository.getLicences())); + public Uni> getLicence(long id, Consumer checkPerm) { + return combRepository.findById(id).invoke(checkPerm).chain(combRepository -> Mutiny.fetch(combRepository.getLicences())); } public Uni setLicence(long id, LicenceForm form) { @@ -49,4 +54,33 @@ public class LicenceService { public Uni deleteLicence(long id) { return Panache.withTransaction(() -> repository.deleteById(id)); } + + public Uni askLicence(long id, LicenceForm form, Consumer checkPerm) { + return combRepository.findById(id).invoke(checkPerm).chain(membreModel -> { + if (form.getId() == -1) { + return repository.find("saison = ?1", Utils.getSaison()).count().invoke(Unchecked.consumer(count -> { + if (count > 0) + throw new BadRequestException(); + })).chain(__ -> combRepository.findById(id).chain(combRepository -> { + LicenceModel model = new LicenceModel(); + model.setMembre(combRepository); + model.setSaison(Utils.getSaison()); + model.setCertificate(form.isCertificate()); + model.setValidate(false); + return Panache.withTransaction(() -> repository.persist(model)); + })); + } else { + return repository.findById(form.getId()).chain(model -> { + model.setCertificate(form.isCertificate()); + return Panache.withTransaction(() -> repository.persist(model)); + }); + } + }); + } + + public Uni deleteAskLicence(long id, Consumer checkPerm) { + return repository.findById(id) + .call(licenceModel -> Mutiny.fetch(licenceModel.getMembre()).invoke(checkPerm)) + .chain(__ -> Panache.withTransaction(() -> repository.deleteById(id))); + } } 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 15dfb48..0ac75b3 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java @@ -6,15 +6,21 @@ import fr.titionfire.ffsaf.data.repository.CombRepository; import fr.titionfire.ffsaf.net2.ServerCustom; import fr.titionfire.ffsaf.net2.data.SimpleCombModel; import fr.titionfire.ffsaf.net2.request.SReqComb; +import fr.titionfire.ffsaf.rest.from.ClubMemberForm; import fr.titionfire.ffsaf.rest.from.FullMemberForm; +import fr.titionfire.ffsaf.utils.GroupeUtils; import fr.titionfire.ffsaf.utils.Pair; +import fr.titionfire.ffsaf.utils.RoleAsso; import io.quarkus.hibernate.reactive.panache.Panache; import io.quarkus.hibernate.reactive.panache.common.WithSession; import io.quarkus.panache.common.Sort; import io.quarkus.vertx.VertxContextSupport; import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.unchecked.Unchecked; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import jakarta.ws.rs.ForbiddenException; +import org.eclipse.microprofile.jwt.JsonWebToken; import java.util.List; @@ -46,6 +52,11 @@ public class MembreService { return repository.listAll(Sort.ascending("fname", "lname")); } + public Uni> getInClub(String subject) { + return repository.find("userId = ?1", subject).firstResult() + .chain(membreModel -> repository.find("club = ?1", membreModel.getClub()).list()); + } + public Uni getById(long id) { return repository.findById(id); } @@ -70,6 +81,46 @@ public class MembreService { .invoke(membreModel -> SReqComb.sendIfNeed(serverCustom.clients, SimpleCombModel.fromModel(membreModel))) .call(membreModel -> (membreModel.getUserId() != null) ? keycloakService.setClubGroupMembre(membreModel, membreModel.getClub()) : Uni.createFrom().nullItem()) + .call(membreModel -> (membreModel.getUserId() != null) ? + keycloakService.setAutoRoleMembre(membreModel.getUserId(), membreModel.getRole(), + membreModel.getGrade_arbitrage()) : Uni.createFrom().nullItem()) + .call(membreModel -> (membreModel.getUserId() != null) ? + keycloakService.setEmail(membreModel.getUserId(), membreModel.getEmail()) : Uni.createFrom().nullItem()) + .map(__ -> "OK"); + } + + public Uni update(long id, ClubMemberForm membre, JsonWebToken idToken) { + return repository.findById(id) + .invoke(Unchecked.consumer(membreModel -> { + if (!GroupeUtils.isInClubGroup(membreModel.getClub().getId(), idToken)) + throw new ForbiddenException(); + })) + .invoke(Unchecked.consumer(membreModel -> { + RoleAsso source = RoleAsso.MEMBRE; + if (idToken.getGroups().contains("club_president")) source = RoleAsso.PRESIDENT; + else if (idToken.getGroups().contains("club_secretaire")) source = RoleAsso.SECRETAIRE; + else if (idToken.getGroups().contains("club_respo_intra")) source = RoleAsso.SECRETAIRE; + if (!membre.getRole().equals(membreModel.getRole()) && membre.getRole().level > source.level) + throw new ForbiddenException(); + })) + .onItem().transformToUni(target -> { + target.setFname(membre.getFname()); + target.setLname(membre.getLname()); + target.setCountry(membre.getCountry()); + target.setBirth_date(membre.getBirth_date()); + target.setGenre(membre.getGenre()); + target.setCategorie(membre.getCategorie()); + target.setEmail(membre.getEmail()); + if (!idToken.getSubject().equals(target.getUserId())) + target.setRole(membre.getRole()); + return Panache.withTransaction(() -> repository.persist(target)); + }) + .invoke(membreModel -> SReqComb.sendIfNeed(serverCustom.clients, SimpleCombModel.fromModel(membreModel))) + .call(membreModel -> (membreModel.getUserId() != null) ? + keycloakService.setAutoRoleMembre(membreModel.getUserId(), membreModel.getRole(), + membreModel.getGrade_arbitrage()) : Uni.createFrom().nullItem()) + .call(membreModel -> (membreModel.getUserId() != null) ? + keycloakService.setEmail(membreModel.getUserId(), membreModel.getEmail()) : Uni.createFrom().nullItem()) .map(__ -> "OK"); } @@ -79,5 +130,4 @@ public class MembreService { return Panache.withTransaction(() -> repository.persist(membreModel)); }); } - } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java index 966f954..20aee87 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java @@ -2,8 +2,8 @@ package fr.titionfire.ffsaf.rest; import fr.titionfire.ffsaf.domain.service.ClubService; import fr.titionfire.ffsaf.net2.data.SimpleClubModel; +import io.quarkus.security.Authenticated; import io.smallrye.mutiny.Uni; -import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @@ -20,7 +20,7 @@ public class ClubEndpoints { @GET @Path("/no_detail") - @RolesAllowed("federation_admin") + @Authenticated @Produces(MediaType.APPLICATION_JSON) public Uni> getAll() { return clubService.getAll().map(clubModels -> clubModels.stream().map(SimpleClubModel::fromModel).toList()); diff --git a/src/main/java/fr/titionfire/ffsaf/rest/CombEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/CombEndpoints.java index f1be7f8..b27dd28 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/CombEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/CombEndpoints.java @@ -1,9 +1,14 @@ package fr.titionfire.ffsaf.rest; +import fr.titionfire.ffsaf.data.model.MembreModel; import fr.titionfire.ffsaf.domain.service.MembreService; import fr.titionfire.ffsaf.rest.data.SimpleMembre; +import fr.titionfire.ffsaf.rest.from.ClubMemberForm; import fr.titionfire.ffsaf.rest.from.FullMemberForm; +import fr.titionfire.ffsaf.utils.GroupeUtils; import fr.titionfire.ffsaf.utils.Pair; +import io.quarkus.oidc.IdToken; +import io.quarkus.security.Authenticated; import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.unchecked.Unchecked; import jakarta.annotation.security.RolesAllowed; @@ -14,6 +19,7 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import jodd.net.MimeTypes; import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.jwt.JsonWebToken; import java.io.*; import java.net.URI; @@ -23,7 +29,9 @@ import java.nio.file.Files; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; +import java.util.function.Consumer; +@Authenticated @Path("api/member") public class CombEndpoints { @@ -33,6 +41,15 @@ public class CombEndpoints { @ConfigProperty(name = "upload_dir") String media; + @Inject + @IdToken + JsonWebToken idToken; + + Consumer checkPerm = Unchecked.consumer(membreModel -> { + if (!idToken.getGroups().contains("federation_admin") && !GroupeUtils.isInClubGroup(membreModel.getClub().getId(), idToken)) + throw new ForbiddenException(); + }); + @GET @Path("/all") @RolesAllowed("federation_admin") @@ -42,22 +59,62 @@ public class CombEndpoints { } @GET - @Path("{id}") - @RolesAllowed("federation_admin") + @Path("/club") + @RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"}) @Produces(MediaType.APPLICATION_JSON) - public Uni getById(@PathParam("id") long id) { - return membreService.getById(id).map(SimpleMembre::fromModel); + public Uni> getClub() { + return membreService.getInClub(idToken.getSubject()).map(membreModels -> membreModels.stream().map(SimpleMembre::fromModel).toList()); } + @GET + @Path("{id}") + @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) + @Produces(MediaType.APPLICATION_JSON) + public Uni getById(@PathParam("id") long id) { + return membreService.getById(id).onItem().invoke(checkPerm).map(SimpleMembre::fromModel); + } @POST @Path("{id}") - @RolesAllowed("federation_admin") + @RolesAllowed({"federation_admin"}) @Produces(MediaType.TEXT_PLAIN) @Consumes(MediaType.MULTIPART_FORM_DATA) public Uni setAdminMembre(@PathParam("id") long id, FullMemberForm input) { - Future future = CompletableFuture.supplyAsync(() -> { - try (InputStream is = new BufferedInputStream(new ByteArrayInputStream(input.getPhoto_data()))) { + return membreService.update(id, input) + .invoke(Unchecked.consumer(out -> { + if (!out.equals("OK")) throw new InternalError("Fail to update data: " + out); + })).chain(() -> { + if (input.getPhoto_data().length > 0) + return Uni.createFrom().future(replacePhoto(id, input.getPhoto_data())).invoke(Unchecked.consumer(out -> { + if (!out.equals("OK")) throw new InternalError("Fail to get MimeType " + out); + })); + else + return Uni.createFrom().nullItem(); + }); + } + + @POST + @Path("club/{id}") + @RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"}) + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.MULTIPART_FORM_DATA) + public Uni setMembre(@PathParam("id") long id, ClubMemberForm input) { + return membreService.update(id, input, idToken) + .invoke(Unchecked.consumer(out -> { + if (!out.equals("OK")) throw new InternalError("Fail to update data: " + out); + })).chain(() -> { + if (input.getPhoto_data().length > 0) + return Uni.createFrom().future(replacePhoto(id, input.getPhoto_data())).invoke(Unchecked.consumer(out -> { + if (!out.equals("OK")) throw new InternalError("Fail to get MimeType " + out); + })); + else + return Uni.createFrom().nullItem(); + }); + } + + private Future replacePhoto(long id, byte[] input) { + return CompletableFuture.supplyAsync(() -> { + try (InputStream is = new BufferedInputStream(new ByteArrayInputStream(input))) { String mimeType = URLConnection.guessContentTypeFromStream(is); String[] detectedExtensions = MimeTypes.findExtensionsByMimeTypes(mimeType, false); if (detectedExtensions.length == 0) @@ -73,33 +130,17 @@ public class CombEndpoints { } String extension = "." + detectedExtensions[0]; - Files.write(new File(media, "ppMembre/" + input.getId() + extension).toPath(), input.getPhoto_data()); + Files.write(new File(media, "ppMembre/" + id + extension).toPath(), input); return "OK"; } catch (IOException e) { return e.getMessage(); } }); - - if (input.getPhoto_data().length > 0) { - return membreService.update(id, input) - .invoke(Unchecked.consumer(out -> { - if (!out.equals("OK")) throw new InternalError("Fail to update data: " + out); - })) - .chain(() -> Uni.createFrom().future(future)) - .invoke(Unchecked.consumer(out -> { - if (!out.equals("OK")) throw new InternalError("Fail to get MimeType " + out); - })); - } else { - return membreService.update(id, input) - .invoke(Unchecked.consumer(out -> { - if (!out.equals("OK")) throw new InternalError("Fail to update data: " + out); - })); - } } @GET @Path("{id}/photo") - @RolesAllowed("federation_admin") + @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) public Uni getPhoto(@PathParam("id") long id) throws URISyntaxException { Future> future = CompletableFuture.supplyAsync(() -> { FilenameFilter filter = (directory, filename) -> filename.startsWith(String.valueOf(id)); @@ -117,7 +158,7 @@ public class CombEndpoints { URI uri = new URI("https://mdbcdn.b-cdn.net/img/Photos/new-templates/bootstrap-chat/ava2.webp"); - return Uni.createFrom().future(future) + return membreService.getById(id).onItem().invoke(checkPerm).chain(__ -> Uni.createFrom().future(future) .map(filePair -> { if (filePair == null) return Response.temporaryRedirect(uri).build(); @@ -131,7 +172,7 @@ public class CombEndpoints { resp.header(HttpHeaders.CONTENT_DISPOSITION, "inline; "); return resp.build(); - }); + })); } } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/CompteEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/CompteEndpoints.java index bc7fd12..33dc88a 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/CompteEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/CompteEndpoints.java @@ -2,13 +2,16 @@ package fr.titionfire.ffsaf.rest; import fr.titionfire.ffsaf.domain.service.KeycloakService; import fr.titionfire.ffsaf.rest.from.MemberPermForm; +import fr.titionfire.ffsaf.utils.GroupeUtils; +import fr.titionfire.ffsaf.utils.Pair; +import io.quarkus.oidc.IdToken; import io.smallrye.mutiny.Uni; +import io.vertx.mutiny.core.Vertx; import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.PUT; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.*; +import org.eclipse.microprofile.jwt.JsonWebToken; +import org.keycloak.representations.idm.GroupRepresentation; import java.util.ArrayList; import java.util.List; @@ -19,11 +22,26 @@ public class CompteEndpoints { @Inject KeycloakService service; + @Inject + JsonWebToken accessToken; + + @Inject + @IdToken + JsonWebToken idToken; + + @Inject + Vertx vertx; + @GET @Path("{id}") - @RolesAllowed("federation_admin") - public Uni getCompte(@PathParam("id") String id) { - return service.fetchCompte(id); + @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) + public Uni getCompte(@PathParam("id") String id) { + return service.fetchCompte(id).call(pair -> vertx.getOrCreateContext().executeBlocking(() -> { + if (!idToken.getGroups().contains("federation_admin") && !pair.getKey().groups().stream().map(GroupRepresentation::getPath) + .anyMatch(s -> s.startsWith("/club/") && GroupeUtils.contains(s, accessToken))) + throw new ForbiddenException(); + return pair; + })).map(Pair::getValue); } @PUT diff --git a/src/main/java/fr/titionfire/ffsaf/rest/LicenceEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/LicenceEndpoints.java index 5bfff5c..7583287 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/LicenceEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/LicenceEndpoints.java @@ -1,15 +1,21 @@ package fr.titionfire.ffsaf.rest; +import fr.titionfire.ffsaf.data.model.MembreModel; import fr.titionfire.ffsaf.domain.service.LicenceService; import fr.titionfire.ffsaf.rest.data.SimpleLicence; import fr.titionfire.ffsaf.rest.from.LicenceForm; +import fr.titionfire.ffsaf.utils.GroupeUtils; +import io.quarkus.oidc.IdToken; import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.unchecked.Unchecked; import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.jwt.JsonWebToken; import java.util.List; +import java.util.function.Consumer; @Path("api/licence") public class LicenceEndpoints { @@ -17,12 +23,21 @@ public class LicenceEndpoints { @Inject LicenceService licenceService; + @Inject + @IdToken + JsonWebToken idToken; + + Consumer checkPerm = Unchecked.consumer(membreModel -> { + if (!idToken.getGroups().contains("federation_admin") && !GroupeUtils.isInClubGroup(membreModel.getClub().getId(), idToken)) + throw new ForbiddenException(); + }); + @GET @Path("{id}") - @RolesAllowed("federation_admin") + @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) @Produces(MediaType.APPLICATION_JSON) public Uni> getLicence(@PathParam("id") long id) { - return licenceService.getLicence(id).map(licenceModels -> licenceModels.stream().map(SimpleLicence::fromModel).toList()); + return licenceService.getLicence(id, checkPerm).map(licenceModels -> licenceModels.stream().map(SimpleLicence::fromModel).toList()); } @POST @@ -41,4 +56,21 @@ public class LicenceEndpoints { public Uni deleteLicence(@PathParam("id") long id) { return licenceService.deleteLicence(id); } + + @POST + @Path("club/{id}") + @RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"}) + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.MULTIPART_FORM_DATA) + public Uni askLicence(@PathParam("id") long id, LicenceForm form) { + return licenceService.askLicence(id, form, checkPerm).map(SimpleLicence::fromModel); + } + + @DELETE + @Path("club/{id}") + @RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"}) + @Produces(MediaType.TEXT_PLAIN) + public Uni deleteAskLicence(@PathParam("id") long id) { + return licenceService.deleteAskLicence(id, checkPerm); + } } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/UserInfo.java b/src/main/java/fr/titionfire/ffsaf/rest/data/UserInfo.java index 0c102f2..cc8dc72 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/UserInfo.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/UserInfo.java @@ -6,6 +6,8 @@ import lombok.Builder; import lombok.Data; import org.eclipse.microprofile.jwt.JsonWebToken; +import java.util.ArrayList; +import java.util.List; import java.util.Set; @Data @@ -19,7 +21,7 @@ public class UserInfo { String email; boolean emailVerified; long expiration; - Set groups; + List groups; Set roles; public static UserInfo makeUserInfo(JsonWebToken accessToken, SecurityIdentity securityIdentity) { @@ -31,7 +33,13 @@ public class UserInfo { builder.email(accessToken.getClaim("email")); builder.emailVerified(accessToken.getClaim("email_verified")); builder.expiration(accessToken.getExpirationTime()); - builder.groups(accessToken.getGroups()); + List groups = new ArrayList<>(); + if (accessToken.getClaim("user_groups") instanceof Iterable) { + for (Object str : (Iterable) accessToken.getClaim("user_groups")) { + groups.add(str.toString().substring(1, str.toString().length() - 1)); + } + } + builder.groups(groups); builder.roles(securityIdentity.getRoles()); return builder.build(); } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/from/ClubMemberForm.java b/src/main/java/fr/titionfire/ffsaf/rest/from/ClubMemberForm.java new file mode 100644 index 0000000..ac752d3 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/from/ClubMemberForm.java @@ -0,0 +1,61 @@ +package fr.titionfire.ffsaf.rest.from; + +import fr.titionfire.ffsaf.utils.Categorie; +import fr.titionfire.ffsaf.utils.Genre; +import fr.titionfire.ffsaf.utils.RoleAsso; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.core.MediaType; +import lombok.Getter; +import org.jboss.resteasy.reactive.PartType; + +import java.util.Date; + +@Getter +public class ClubMemberForm { + @FormParam("id") + private String id = null; + + @FormParam("lname") + private String lname = null; + + @FormParam("fname") + private String fname = null; + + @FormParam("categorie") + private Categorie categorie = null; + + @FormParam("genre") + private Genre genre; + + @FormParam("country") + private String country; + + @FormParam("birth_date") + private Date birth_date; + + @FormParam("email") + private String email; + + @FormParam("role") + private RoleAsso role; + + @FormParam("photo_data") + @PartType(MediaType.APPLICATION_OCTET_STREAM) + private byte[] photo_data = new byte[0]; + + @Override + public String toString() { + return "ClubMemberForm{" + + "id='" + id + '\'' + + ", lname='" + lname + '\'' + + ", fname='" + fname + '\'' + + ", categorie=" + categorie + + ", genre=" + genre + + ", country='" + country + '\'' + + ", birth_date=" + birth_date + + ", email='" + email + '\'' + + ", role=" + role + + ", url_photo=" + photo_data.length + + '}'; + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/utils/GroupeUtils.java b/src/main/java/fr/titionfire/ffsaf/utils/GroupeUtils.java new file mode 100644 index 0000000..3910ea9 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/utils/GroupeUtils.java @@ -0,0 +1,25 @@ +package fr.titionfire.ffsaf.utils; + +import org.eclipse.microprofile.jwt.JsonWebToken; + +public class GroupeUtils { + public static boolean isInClubGroup(long id, JsonWebToken accessToken) { + if (accessToken.getClaim("user_groups") instanceof Iterable) { + for (Object str : (Iterable) accessToken.getClaim("user_groups")) { + if (str.toString().substring(1, str.toString().length() - 1).startsWith("/club/" + id + "-")) + return true; + } + } + return false; + } + + public static boolean contains(String string, JsonWebToken accessToken) { + if (accessToken.getClaim("user_groups") instanceof Iterable) { + for (Object str : (Iterable) accessToken.getClaim("user_groups")) { + if (str.toString().substring(1, str.toString().length() - 1).contains(string)) + return true; + } + } + return false; + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/utils/RoleAsso.java b/src/main/java/fr/titionfire/ffsaf/utils/RoleAsso.java index 1051a16..84991b1 100644 --- a/src/main/java/fr/titionfire/ffsaf/utils/RoleAsso.java +++ b/src/main/java/fr/titionfire/ffsaf/utils/RoleAsso.java @@ -4,19 +4,17 @@ import io.quarkus.runtime.annotations.RegisterForReflection; @RegisterForReflection public enum RoleAsso { - MEMBRE("Membre"), - PRESIDENT("Président"), - TRESORIER("Trésorier"), - SECRETAIRE("Secrétaire"); + MEMBRE("Membre", 0), + PRESIDENT("Président", 3), + TRESORIER("Trésorier", 1), + SECRETAIRE("Secrétaire", 2); - public String name; + public final String name; + public final int level; - RoleAsso(String name) { - this.name = name; - } - - public void setName(String name) { + RoleAsso(String name, int level) { this.name = name; + this.level = level; } @Override diff --git a/src/main/java/fr/titionfire/ffsaf/utils/Utils.java b/src/main/java/fr/titionfire/ffsaf/utils/Utils.java new file mode 100644 index 0000000..1a33bb2 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/utils/Utils.java @@ -0,0 +1,22 @@ +package fr.titionfire.ffsaf.utils; + +import java.util.Calendar; +import java.util.Date; + +public class Utils { + + public static int getSaison() { + return getSaison(new Date()); + } + + public static int getSaison(Date date) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + + if (calendar.get(Calendar.MONTH) >= Calendar.SEPTEMBER) { + return calendar.get(Calendar.YEAR); + } else { + return calendar.get(Calendar.YEAR) - 1; + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 053cf03..69feeed 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -40,7 +40,7 @@ quarkus.oidc.token.refresh-expired=true quarkus.oidc.authentication.redirect-path=/api/auth/login quarkus.oidc.logout.path=/api/logout -quarkus.oidc.logout.post-logout-path=/index.html +quarkus.oidc.logout.post-logout-path=/ # Only the authenticated users can initiate a logout: quarkus.http.auth.permission.authenticated.paths=api/logout,api/auth/login diff --git a/src/main/webapp/src/App.jsx b/src/main/webapp/src/App.jsx index 04b7a97..9c0ff8d 100644 --- a/src/main/webapp/src/App.jsx +++ b/src/main/webapp/src/App.jsx @@ -10,6 +10,7 @@ import {ToastContainer} from "react-toastify"; import './App.css' import 'react-toastify/dist/ReactToastify.css'; +import {ClubRoot, getClubChildren} from "./pages/club/ClubRoot.jsx"; const router = createBrowserRouter([ { @@ -25,6 +26,11 @@ const router = createBrowserRouter([ path: 'admin', element: , children: getAdminChildren() + }, + { + path: 'club', + element: , + children: getClubChildren() } ] }, diff --git a/src/main/webapp/src/components/ColoredCircle.jsx b/src/main/webapp/src/components/ColoredCircle.jsx index 2f9d574..55fab61 100644 --- a/src/main/webapp/src/components/ColoredCircle.jsx +++ b/src/main/webapp/src/components/ColoredCircle.jsx @@ -13,4 +13,16 @@ export const ColoredCircle = ({color, boolean}) => { return +}; + +export const ColoredText = ({boolean, text={true: "Oui", false: "Non"}}) => { + const styles = {color: '#F00'}; + + if (boolean !== undefined) { + styles.color = (boolean) ? '#00c700' : '#e50000'; + } + + return + {text[boolean]} + }; \ No newline at end of file diff --git a/src/main/webapp/src/pages/admin/member/MemberCustomFiels.jsx b/src/main/webapp/src/components/MemberCustomFiels.jsx similarity index 94% rename from src/main/webapp/src/pages/admin/member/MemberCustomFiels.jsx rename to src/main/webapp/src/components/MemberCustomFiels.jsx index 05490bf..1b7eb64 100644 --- a/src/main/webapp/src/pages/admin/member/MemberCustomFiels.jsx +++ b/src/main/webapp/src/components/MemberCustomFiels.jsx @@ -1,5 +1,5 @@ import {useEffect, useState} from "react"; -import {getCategoryFormBirthDate} from "../../../utils/Tools.js"; +import {getCategoryFormBirthDate} from "../utils/Tools.js"; export function BirthDayField({inti_date, inti_category}) { const [date, setDate] = useState(inti_date) @@ -36,11 +36,11 @@ export function BirthDayField({inti_date, inti_category}) { } -export function OptionField({name, text, values, value}) { +export function OptionField({name, text, values, value, disabled=false}) { return
    - {Object.keys(values).map((key, _) => { return () })} diff --git a/src/main/webapp/src/components/Nav.jsx b/src/main/webapp/src/components/Nav.jsx index 3b95604..886644d 100644 --- a/src/main/webapp/src/components/Nav.jsx +++ b/src/main/webapp/src/components/Nav.jsx @@ -22,6 +22,7 @@ export function Nav() {
    • Accueil
    • +
    @@ -31,6 +32,24 @@ export function Nav() { } +function ClubMenu() { + const {is_authenticated, userinfo} = useAuth() + + if (!is_authenticated || !(userinfo?.roles?.includes("club_president") + || userinfo?.roles?.includes("club_secretaire") || userinfo?.roles?.includes("club_tresorier"))) + return <> + + return
  • + +
      +
    • Member
    • +
    • B
    • +
    +
  • +} + function AdminMenu() { const {is_authenticated, userinfo} = useAuth() diff --git a/src/main/webapp/src/pages/admin/member/InformationForm.jsx b/src/main/webapp/src/pages/admin/member/InformationForm.jsx index e88630a..265decc 100644 --- a/src/main/webapp/src/pages/admin/member/InformationForm.jsx +++ b/src/main/webapp/src/pages/admin/member/InformationForm.jsx @@ -2,7 +2,7 @@ import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; import {apiAxios} from "../../../utils/Tools.js"; import {toast} from "react-toastify"; import imageCompression from "browser-image-compression"; -import {BirthDayField, OptionField, TextField} from "./MemberCustomFiels.jsx"; +import {BirthDayField, OptionField, TextField} from "../../../components/MemberCustomFiels.jsx"; import {ClubSelect} from "../../../components/ClubSelect.jsx"; export function InformationForm({data}) { diff --git a/src/main/webapp/src/pages/admin/member/LicenceCard.jsx b/src/main/webapp/src/pages/admin/member/LicenceCard.jsx index 1346079..fd11ec3 100644 --- a/src/main/webapp/src/pages/admin/member/LicenceCard.jsx +++ b/src/main/webapp/src/pages/admin/member/LicenceCard.jsx @@ -4,7 +4,7 @@ import {useEffect, useReducer, useState} from "react"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faPen} from "@fortawesome/free-solid-svg-icons"; import {AxiosError} from "../../../components/AxiosError.jsx"; -import {CheckField, TextField} from "./MemberCustomFiels.jsx"; +import {CheckField, TextField} from "../../../components/MemberCustomFiels.jsx"; import {apiAxios, getSaison} from "../../../utils/Tools.js"; import {Input} from "../../../components/Input.jsx"; import {toast} from "react-toastify"; @@ -115,10 +115,9 @@ function removeLicence(id, dispatch) { success: "Licence supprimée avec succès 🎉", error: "Échec de la suppression de la licence 😕" } - ).then(data => { + ).then(_ => { dispatch({type: 'REMOVE', payload: id}) }) - console.log(id) } function ModalContent({licence, dispatch}) { diff --git a/src/main/webapp/src/pages/admin/member/PremForm.jsx b/src/main/webapp/src/pages/admin/member/PremForm.jsx index 8f5ab0d..59525e9 100644 --- a/src/main/webapp/src/pages/admin/member/PremForm.jsx +++ b/src/main/webapp/src/pages/admin/member/PremForm.jsx @@ -2,7 +2,7 @@ import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; import {apiAxios} from "../../../utils/Tools.js"; import {toast} from "react-toastify"; import {useFetch} from "../../../hooks/useFetch.js"; -import {CheckField} from "./MemberCustomFiels.jsx"; +import {CheckField} from "../../../components/MemberCustomFiels.jsx"; import {AxiosError} from "../../../components/AxiosError.jsx"; export function PremForm({userData}) { @@ -66,7 +66,7 @@ function PremFormContent({userData}) {
    FFSAF intra
    {data ? <> - : error && } diff --git a/src/main/webapp/src/pages/club/ClubRoot.jsx b/src/main/webapp/src/pages/club/ClubRoot.jsx new file mode 100644 index 0000000..5c92a4a --- /dev/null +++ b/src/main/webapp/src/pages/club/ClubRoot.jsx @@ -0,0 +1,43 @@ +import {Outlet} from "react-router-dom"; +import {LoadingProvider} from "../../hooks/useLoading.jsx"; +import {MemberList} from "./MemberList.jsx"; +import {MemberPage} from "./member/MemberPage.jsx"; +import {useAuth} from "../../hooks/useAuth.jsx"; + +export function ClubRoot() { + const {userinfo} = useAuth() + let club = "" + if (userinfo?.groups) { + for (let group of userinfo.groups) { + if (group.startsWith("/club/")) { + club = group.slice(group.indexOf("-") + 1) + break + } + } + } + + return <> +
    +

    Espace club

    {club}

    + + + + +} + +export function getClubChildren() { + return [ + { + path: 'member', + element: + }, + { + path: 'member/:id', + element: + }, + { + path: 'b', + element:
    Club B
    + } + ] +} \ No newline at end of file diff --git a/src/main/webapp/src/pages/club/MemberList.jsx b/src/main/webapp/src/pages/club/MemberList.jsx new file mode 100644 index 0000000..a486f6b --- /dev/null +++ b/src/main/webapp/src/pages/club/MemberList.jsx @@ -0,0 +1,60 @@ +import {useLoadingSwitcher} from "../../hooks/useLoading.jsx"; +import {useFetch} from "../../hooks/useFetch.js"; +import {AxiosError} from "../../components/AxiosError.jsx"; +import {ThreeDots} from "react-loader-spinner"; +import {useState} from "react"; +import {Input} from "../../components/Input.jsx"; +import {useNavigate} from "react-router-dom"; + +const removeDiacritics = str => { + return str + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') +} + +export function MemberList() { + const setLoading = useLoadingSwitcher() + const {data, error} = useFetch(`/member/club`, setLoading, 1) + const [searchInput, setSearchInput] = useState(""); + const navigate = useNavigate(); + + const visibleMember = data ? data.filter(member => { + const lo = removeDiacritics(searchInput).toLowerCase() + return !searchInput + || (removeDiacritics(member.fname).toLowerCase().startsWith(lo) + || removeDiacritics(member.lname).toLowerCase().startsWith(lo)); + }) : []; + + return <> + + {data + ?
    + {visibleMember.map(member => ( + navigate("/club/member/" + member.id)} + className="list-group-item list-group-item-action">{member.fname} {member.lname}))} +
    + : error + ? + : + } + +} + +function SearchBar({searchInput, onSearchInputChange}) { + return
    +
    + +
    +
    +} + +function Def() { + return
    +
  • +
  • +
  • +
  • +
  • +
    +} \ No newline at end of file diff --git a/src/main/webapp/src/pages/club/member/CompteInfo.jsx b/src/main/webapp/src/pages/club/member/CompteInfo.jsx new file mode 100644 index 0000000..e9128c9 --- /dev/null +++ b/src/main/webapp/src/pages/club/member/CompteInfo.jsx @@ -0,0 +1,55 @@ +import {toast} from "react-toastify"; +import {apiAxios} from "../../../utils/Tools.js"; +import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; +import {useFetch} from "../../../hooks/useFetch.js"; +import {ColoredCircle} from "../../../components/ColoredCircle.jsx"; +import {AxiosError} from "../../../components/AxiosError.jsx"; + +export function CompteInfo({userData}) { + + return
    +
    Compte
    +
    + {userData.userId + ? + : + <> +
    +
    +
    Ce membre ne dispose pas de compte...
    + Un compte sera créé par la fédération lors de la validation de sa première licence +
    +
    +
    + + } +
    +
    +} + +function CompteInfoContent({userData}) { + const setLoading = useLoadingSwitcher() + const {data, error} = useFetch(`/compte/${userData.userId}`, setLoading, 1) + + return <> + {data + ? <> +
    +
    +
    Identifiant: {data.login}
    +
    +
    +
    +
    +
    Activer:
    +
    +
    +
    +
    +
    Email vérifié:
    +
    +
    + + : error && + } +} diff --git a/src/main/webapp/src/pages/club/member/InformationForm.jsx b/src/main/webapp/src/pages/club/member/InformationForm.jsx new file mode 100644 index 0000000..4fd541c --- /dev/null +++ b/src/main/webapp/src/pages/club/member/InformationForm.jsx @@ -0,0 +1,101 @@ +// noinspection DuplicatedCode + +import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; +import {apiAxios} from "../../../utils/Tools.js"; +import {toast} from "react-toastify"; +import imageCompression from "browser-image-compression"; +import {BirthDayField, OptionField, TextField} from "../../../components/MemberCustomFiels.jsx"; +import {ClubSelect} from "../../../components/ClubSelect.jsx"; + +export function InformationForm({data}) { + const setLoading = useLoadingSwitcher() + const handleSubmit = (event) => { + event.preventDefault(); + setLoading(1) + + const formData = new FormData(); + formData.append("id", data.id); + formData.append("lname", event.target.lname?.value); + formData.append("fname", event.target.fname?.value); + formData.append("categorie", event.target.category?.value); + formData.append("genre", event.target.genre?.value); + formData.append("country", event.target.country?.value); + formData.append("birth_date", new Date(event.target.birth_date?.value).toUTCString()); + formData.append("email", event.target.email?.value); + formData.append("role", event.target.role?.value); + + const send = (formData_) => { + apiAxios.post(`/member/club/${data.id}`, formData_, { + headers: { + 'Accept': '*/*', + 'Content-Type': 'multipart/form-data', + } + }).then(_ => { + toast.success('Profile mis à jours avec succès 🎉'); + }).catch(e => { + console.log(e.response) + toast.error('Échec de la mise à jours du profile 😕 (code: ' + e.response.status + ')'); + }).finally(() => { + if (setLoading) + setLoading(0) + }) + } + + const imageFile = event.target.url_photo.files[0]; + if (imageFile) { + console.log(`originalFile size ${imageFile.size / 1024 / 1024} MB`); + const options = { + maxSizeMB: 1, + maxWidthOrHeight: 1920, + useWebWorker: true, + } + imageCompression(imageFile, options).then(compressedFile => { + console.log(`compressedFile size ${compressedFile.size / 1024 / 1024} MB`); // smaller than maxSizeMB + formData.append("photo_data", compressedFile) + send(formData) + }); + } else { + send(formData) + } + } + + return
    +
    +
    Information
    +
    + + + + + + + + +
    +
    + + +
    +
    +
    +
    + +
    +
    +
    +
    +
    ; +} \ No newline at end of file diff --git a/src/main/webapp/src/pages/club/member/LicenceCard.jsx b/src/main/webapp/src/pages/club/member/LicenceCard.jsx new file mode 100644 index 0000000..48023ea --- /dev/null +++ b/src/main/webapp/src/pages/club/member/LicenceCard.jsx @@ -0,0 +1,176 @@ +import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; +import {useFetch} from "../../../hooks/useFetch.js"; +import {useEffect, useReducer, useState} from "react"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {faInfo, faPen} from "@fortawesome/free-solid-svg-icons"; +import {AxiosError} from "../../../components/AxiosError.jsx"; +import {apiAxios, getSaison} from "../../../utils/Tools.js"; +import {toast} from "react-toastify"; +import {ColoredText} from "../../../components/ColoredCircle.jsx"; + +function licenceReducer(licences, action) { + switch (action.type) { + case 'REMOVE': + return licences.filter(licence => licence.id !== action.payload) + case 'UPDATE_OR_ADD': + const index = licences.findIndex(licence => licence.id === action.payload.id) + if (index === -1) { + return [ + ...licences, + action.payload + ] + } else { + licences[index] = action.payload + return [...licences] + } + case 'SORT': + return licences.sort((a, b) => b.saison - a.saison) + default: + throw new Error() + } +} + +export function LicenceCard({userData}) { + const defaultLicence = {id: -1, membre: userData.id, validate: false, saison: getSaison(), certificate: false} + + const setLoading = useLoadingSwitcher() + const {data, error} = useFetch(`/licence/${userData.id}`, setLoading, 1) + const [modalLicence, setModal] = useState(defaultLicence) + const [licences, dispatch] = useReducer(licenceReducer, []) + + useEffect(() => { + if (!data) return + for (const dataKey of data) { + dispatch({type: 'UPDATE_OR_ADD', payload: dataKey}) + } + dispatch({type: 'SORT'}) + }, [data]); + + return
    +
    +
    +
    Licence
    +
    + +
    +
    +
    +
    +
      + {licences.map((licence, index) => { + return
      +
      {licence?.saison}-{licence?.saison + 1}
      + +
      + })} + {error && } +
    +
    + + +
    ; +} + +function sendLicence(event, dispatch) { + event.preventDefault(); + + const formData = new FormData(event.target); + toast.promise( + apiAxios.post(`/licence/club/${formData.get('membre')}`, formData), + { + pending: "Enregistrement de la demande de licence en cours", + success: "Demande de licence enregistrée avec succès 🎉", + error: "Échec de la demande de licence 😕" + } + ).then(data => { + dispatch({type: 'UPDATE_OR_ADD', payload: data.data}) + dispatch({type: 'SORT'}) + }) + +} + +function removeLicence(id, dispatch) { + toast.promise( + apiAxios.delete(`/licence/club/${id}`), + { + pending: "Suppression de la demande en cours", + success: "Demande supprimée avec succès 🎉", + error: "Échec de la suppression de la demande de licence 😕" + } + ).then(_ => { + dispatch({type: 'REMOVE', payload: id}) + }) +} + +function ModalContent({licence, dispatch}) { + const [certificate, setCertificate] = useState(false) + const [isNew, setNew] = useState(true) + + const handleCertificateChange = (event) => { + setCertificate(event.target.value === 'true'); + } + + useEffect(() => { + if (licence.id !== -1) { + setNew(false) + setCertificate(licence.certificate) + } else { + setNew(true) + setCertificate(false) + } + }, [licence]); + + const currentSaison = licence.saison === getSaison(); + + return
    sendLicence(e, dispatch)}> + + +
    +

    + {isNew ? "Demande de licence " : "Edition de la demande "} + (saison {licence.saison}-{licence.saison + 1})

    + +
    +
    +
    + Certificat médical + + + + +
    + +
    +
    Validation de la licence:
    +
    +
    +
    + {currentSaison && + } + + {currentSaison && licence.validate === false && + } +
    +
    +} diff --git a/src/main/webapp/src/pages/club/member/MemberPage.jsx b/src/main/webapp/src/pages/club/member/MemberPage.jsx new file mode 100644 index 0000000..3f34d2f --- /dev/null +++ b/src/main/webapp/src/pages/club/member/MemberPage.jsx @@ -0,0 +1,70 @@ +import {useNavigate, useParams} from "react-router-dom"; +import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; +import {useFetch} from "../../../hooks/useFetch.js"; +import {AxiosError} from "../../../components/AxiosError.jsx"; +import {CompteInfo} from "./CompteInfo.jsx"; +import {InformationForm} from "./InformationForm.jsx"; +import {LicenceCard} from "./LicenceCard.jsx"; + +const vite_url = import.meta.env.VITE_URL; + +export function MemberPage() { + const {id} = useParams() + const navigate = useNavigate(); + + const setLoading = useLoadingSwitcher() + const {data, error} = useFetch(`/member/${id}`, setLoading, 1) + + return <> +

    Page membre

    + + {data + ?
    +
    +
    + + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    +
    + : error && + } + +} + +function PhotoCard({data}) { + return
    +
    Licence n°{data.licence}
    +
    +
    + avatar +
    +
    +
    ; +} + +function SelectCard() { + return
    +
    Sélection en équipe de France
    +
    +

    Soon

    + +
    +
    ; +}