feat: add club member's gestion page
This commit is contained in:
parent
0c9020890a
commit
2a59c22db6
@ -79,6 +79,11 @@ public class ExampleResource {
|
|||||||
response.append("<li>scopes: ").append(this.accessToken.toString()).append("</li>");
|
response.append("<li>scopes: ").append(this.accessToken.toString()).append("</li>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (scopes != null) {
|
||||||
|
response.append("<li>scopes: ").append(this.accessToken.getClaim("user_groups").toString()).append("</li>");
|
||||||
|
}
|
||||||
|
|
||||||
if (scopes != null) {
|
if (scopes != null) {
|
||||||
response.append("<li>getRoles: ").append(this.securityIdentity.getRoles()).append("</li>");
|
response.append("<li>getRoles: ").append(this.securityIdentity.getRoles()).append("</li>");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,7 @@ public class LicenceModel {
|
|||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
Long id;
|
Long id;
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.EAGER)
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "membre", referencedColumnName = "id")
|
@JoinColumn(name = "membre", referencedColumnName = "id")
|
||||||
MembreModel membre;
|
MembreModel membre;
|
||||||
|
|
||||||
|
|||||||
@ -2,8 +2,7 @@ package fr.titionfire.ffsaf.domain.service;
|
|||||||
|
|
||||||
import fr.titionfire.ffsaf.data.model.ClubModel;
|
import fr.titionfire.ffsaf.data.model.ClubModel;
|
||||||
import fr.titionfire.ffsaf.data.model.MembreModel;
|
import fr.titionfire.ffsaf.data.model.MembreModel;
|
||||||
import fr.titionfire.ffsaf.utils.KeycloakException;
|
import fr.titionfire.ffsaf.utils.*;
|
||||||
import fr.titionfire.ffsaf.utils.RequiredAction;
|
|
||||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||||
import io.smallrye.mutiny.Uni;
|
import io.smallrye.mutiny.Uni;
|
||||||
import io.smallrye.mutiny.unchecked.Unchecked;
|
import io.smallrye.mutiny.unchecked.Unchecked;
|
||||||
@ -21,6 +20,7 @@ import org.keycloak.representations.idm.RoleRepresentation;
|
|||||||
import org.keycloak.representations.idm.UserRepresentation;
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
|
|
||||||
import java.text.Normalizer;
|
import java.text.Normalizer;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@ -86,13 +86,42 @@ public class KeycloakService {
|
|||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Uni<UserCompteState> 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<String> toRemove = new ArrayList<>(List.of("club_president", "club_tresorier", "club_secretaire", "asseseur", "arbitre"));
|
||||||
|
List<String> 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<Pair<UserResource, UserCompteState>> fetchCompte(String id) {
|
||||||
return vertx.getOrCreateContext().executeBlocking(() -> {
|
return vertx.getOrCreateContext().executeBlocking(() -> {
|
||||||
UserResource user = keycloak.realm(realm).users().get(id);
|
UserResource user = keycloak.realm(realm).users().get(id);
|
||||||
UserRepresentation user2 = user.toRepresentation();
|
UserRepresentation user2 = user.toRepresentation();
|
||||||
return new UserCompteState(user2.isEnabled(), user2.getUsername(), user2.isEmailVerified(),
|
return new Pair<>(user, new UserCompteState(user2.isEnabled(), user2.getUsername(), user2.isEmailVerified())) ;
|
||||||
user.roles().realmLevel().listEffective().stream().map(RoleRepresentation::getName).toList(),
|
|
||||||
user.groups().stream().map(GroupRepresentation::getName).toList());
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,7 +211,6 @@ public class KeycloakService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RegisterForReflection
|
@RegisterForReflection
|
||||||
public record UserCompteState(Boolean enabled, String login, Boolean emailVerified, List<String> realmRoles,
|
public record UserCompteState(Boolean enabled, String login, Boolean emailVerified) {
|
||||||
List<String> groups) {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,22 @@
|
|||||||
package fr.titionfire.ffsaf.domain.service;
|
package fr.titionfire.ffsaf.domain.service;
|
||||||
|
|
||||||
import fr.titionfire.ffsaf.data.model.LicenceModel;
|
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.CombRepository;
|
||||||
import fr.titionfire.ffsaf.data.repository.LicenceRepository;
|
import fr.titionfire.ffsaf.data.repository.LicenceRepository;
|
||||||
import fr.titionfire.ffsaf.rest.from.LicenceForm;
|
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.Panache;
|
||||||
import io.quarkus.hibernate.reactive.panache.common.WithSession;
|
import io.quarkus.hibernate.reactive.panache.common.WithSession;
|
||||||
import io.smallrye.mutiny.Uni;
|
import io.smallrye.mutiny.Uni;
|
||||||
|
import io.smallrye.mutiny.unchecked.Unchecked;
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.ws.rs.BadRequestException;
|
||||||
import org.hibernate.reactive.mutiny.Mutiny;
|
import org.hibernate.reactive.mutiny.Mutiny;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
@WithSession
|
@WithSession
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
@ -23,8 +28,8 @@ public class LicenceService {
|
|||||||
@Inject
|
@Inject
|
||||||
CombRepository combRepository;
|
CombRepository combRepository;
|
||||||
|
|
||||||
public Uni<List<LicenceModel>> getLicence(long id) {
|
public Uni<List<LicenceModel>> getLicence(long id, Consumer<MembreModel> checkPerm) {
|
||||||
return combRepository.findById(id).chain(combRepository -> Mutiny.fetch(combRepository.getLicences()));
|
return combRepository.findById(id).invoke(checkPerm).chain(combRepository -> Mutiny.fetch(combRepository.getLicences()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Uni<LicenceModel> setLicence(long id, LicenceForm form) {
|
public Uni<LicenceModel> setLicence(long id, LicenceForm form) {
|
||||||
@ -49,4 +54,33 @@ public class LicenceService {
|
|||||||
public Uni<?> deleteLicence(long id) {
|
public Uni<?> deleteLicence(long id) {
|
||||||
return Panache.withTransaction(() -> repository.deleteById(id));
|
return Panache.withTransaction(() -> repository.deleteById(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Uni<LicenceModel> askLicence(long id, LicenceForm form, Consumer<MembreModel> 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<MembreModel> checkPerm) {
|
||||||
|
return repository.findById(id)
|
||||||
|
.call(licenceModel -> Mutiny.fetch(licenceModel.getMembre()).invoke(checkPerm))
|
||||||
|
.chain(__ -> Panache.withTransaction(() -> repository.deleteById(id)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,15 +6,21 @@ import fr.titionfire.ffsaf.data.repository.CombRepository;
|
|||||||
import fr.titionfire.ffsaf.net2.ServerCustom;
|
import fr.titionfire.ffsaf.net2.ServerCustom;
|
||||||
import fr.titionfire.ffsaf.net2.data.SimpleCombModel;
|
import fr.titionfire.ffsaf.net2.data.SimpleCombModel;
|
||||||
import fr.titionfire.ffsaf.net2.request.SReqComb;
|
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.rest.from.FullMemberForm;
|
||||||
|
import fr.titionfire.ffsaf.utils.GroupeUtils;
|
||||||
import fr.titionfire.ffsaf.utils.Pair;
|
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.Panache;
|
||||||
import io.quarkus.hibernate.reactive.panache.common.WithSession;
|
import io.quarkus.hibernate.reactive.panache.common.WithSession;
|
||||||
import io.quarkus.panache.common.Sort;
|
import io.quarkus.panache.common.Sort;
|
||||||
import io.quarkus.vertx.VertxContextSupport;
|
import io.quarkus.vertx.VertxContextSupport;
|
||||||
import io.smallrye.mutiny.Uni;
|
import io.smallrye.mutiny.Uni;
|
||||||
|
import io.smallrye.mutiny.unchecked.Unchecked;
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.ws.rs.ForbiddenException;
|
||||||
|
import org.eclipse.microprofile.jwt.JsonWebToken;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -46,6 +52,11 @@ public class MembreService {
|
|||||||
return repository.listAll(Sort.ascending("fname", "lname"));
|
return repository.listAll(Sort.ascending("fname", "lname"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Uni<List<MembreModel>> getInClub(String subject) {
|
||||||
|
return repository.find("userId = ?1", subject).firstResult()
|
||||||
|
.chain(membreModel -> repository.find("club = ?1", membreModel.getClub()).list());
|
||||||
|
}
|
||||||
|
|
||||||
public Uni<MembreModel> getById(long id) {
|
public Uni<MembreModel> getById(long id) {
|
||||||
return repository.findById(id);
|
return repository.findById(id);
|
||||||
}
|
}
|
||||||
@ -70,6 +81,46 @@ public class MembreService {
|
|||||||
.invoke(membreModel -> SReqComb.sendIfNeed(serverCustom.clients, SimpleCombModel.fromModel(membreModel)))
|
.invoke(membreModel -> SReqComb.sendIfNeed(serverCustom.clients, SimpleCombModel.fromModel(membreModel)))
|
||||||
.call(membreModel -> (membreModel.getUserId() != null) ?
|
.call(membreModel -> (membreModel.getUserId() != null) ?
|
||||||
keycloakService.setClubGroupMembre(membreModel, membreModel.getClub()) : Uni.createFrom().nullItem())
|
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<String> 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");
|
.map(__ -> "OK");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,5 +130,4 @@ public class MembreService {
|
|||||||
return Panache.withTransaction(() -> repository.persist(membreModel));
|
return Panache.withTransaction(() -> repository.persist(membreModel));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,8 +2,8 @@ package fr.titionfire.ffsaf.rest;
|
|||||||
|
|
||||||
import fr.titionfire.ffsaf.domain.service.ClubService;
|
import fr.titionfire.ffsaf.domain.service.ClubService;
|
||||||
import fr.titionfire.ffsaf.net2.data.SimpleClubModel;
|
import fr.titionfire.ffsaf.net2.data.SimpleClubModel;
|
||||||
|
import io.quarkus.security.Authenticated;
|
||||||
import io.smallrye.mutiny.Uni;
|
import io.smallrye.mutiny.Uni;
|
||||||
import jakarta.annotation.security.RolesAllowed;
|
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.ws.rs.GET;
|
import jakarta.ws.rs.GET;
|
||||||
import jakarta.ws.rs.Path;
|
import jakarta.ws.rs.Path;
|
||||||
@ -20,7 +20,7 @@ public class ClubEndpoints {
|
|||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/no_detail")
|
@Path("/no_detail")
|
||||||
@RolesAllowed("federation_admin")
|
@Authenticated
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public Uni<List<SimpleClubModel>> getAll() {
|
public Uni<List<SimpleClubModel>> getAll() {
|
||||||
return clubService.getAll().map(clubModels -> clubModels.stream().map(SimpleClubModel::fromModel).toList());
|
return clubService.getAll().map(clubModels -> clubModels.stream().map(SimpleClubModel::fromModel).toList());
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
package fr.titionfire.ffsaf.rest;
|
package fr.titionfire.ffsaf.rest;
|
||||||
|
|
||||||
|
import fr.titionfire.ffsaf.data.model.MembreModel;
|
||||||
import fr.titionfire.ffsaf.domain.service.MembreService;
|
import fr.titionfire.ffsaf.domain.service.MembreService;
|
||||||
import fr.titionfire.ffsaf.rest.data.SimpleMembre;
|
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.rest.from.FullMemberForm;
|
||||||
|
import fr.titionfire.ffsaf.utils.GroupeUtils;
|
||||||
import fr.titionfire.ffsaf.utils.Pair;
|
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.Uni;
|
||||||
import io.smallrye.mutiny.unchecked.Unchecked;
|
import io.smallrye.mutiny.unchecked.Unchecked;
|
||||||
import jakarta.annotation.security.RolesAllowed;
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
@ -14,6 +19,7 @@ import jakarta.ws.rs.core.MediaType;
|
|||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import jodd.net.MimeTypes;
|
import jodd.net.MimeTypes;
|
||||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||||
|
import org.eclipse.microprofile.jwt.JsonWebToken;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
@ -23,7 +29,9 @@ import java.nio.file.Files;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
@Authenticated
|
||||||
@Path("api/member")
|
@Path("api/member")
|
||||||
public class CombEndpoints {
|
public class CombEndpoints {
|
||||||
|
|
||||||
@ -33,6 +41,15 @@ public class CombEndpoints {
|
|||||||
@ConfigProperty(name = "upload_dir")
|
@ConfigProperty(name = "upload_dir")
|
||||||
String media;
|
String media;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@IdToken
|
||||||
|
JsonWebToken idToken;
|
||||||
|
|
||||||
|
Consumer<MembreModel> checkPerm = Unchecked.consumer(membreModel -> {
|
||||||
|
if (!idToken.getGroups().contains("federation_admin") && !GroupeUtils.isInClubGroup(membreModel.getClub().getId(), idToken))
|
||||||
|
throw new ForbiddenException();
|
||||||
|
});
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/all")
|
@Path("/all")
|
||||||
@RolesAllowed("federation_admin")
|
@RolesAllowed("federation_admin")
|
||||||
@ -42,22 +59,62 @@ public class CombEndpoints {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("{id}")
|
@Path("/club")
|
||||||
@RolesAllowed("federation_admin")
|
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public Uni<SimpleMembre> getById(@PathParam("id") long id) {
|
public Uni<List<SimpleMembre>> getClub() {
|
||||||
return membreService.getById(id).map(SimpleMembre::fromModel);
|
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<SimpleMembre> getById(@PathParam("id") long id) {
|
||||||
|
return membreService.getById(id).onItem().invoke(checkPerm).map(SimpleMembre::fromModel);
|
||||||
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("{id}")
|
@Path("{id}")
|
||||||
@RolesAllowed("federation_admin")
|
@RolesAllowed({"federation_admin"})
|
||||||
@Produces(MediaType.TEXT_PLAIN)
|
@Produces(MediaType.TEXT_PLAIN)
|
||||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||||
public Uni<String> setAdminMembre(@PathParam("id") long id, FullMemberForm input) {
|
public Uni<String> setAdminMembre(@PathParam("id") long id, FullMemberForm input) {
|
||||||
Future<String> future = CompletableFuture.supplyAsync(() -> {
|
return membreService.update(id, input)
|
||||||
try (InputStream is = new BufferedInputStream(new ByteArrayInputStream(input.getPhoto_data()))) {
|
.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<String> 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<String> replacePhoto(long id, byte[] input) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
try (InputStream is = new BufferedInputStream(new ByteArrayInputStream(input))) {
|
||||||
String mimeType = URLConnection.guessContentTypeFromStream(is);
|
String mimeType = URLConnection.guessContentTypeFromStream(is);
|
||||||
String[] detectedExtensions = MimeTypes.findExtensionsByMimeTypes(mimeType, false);
|
String[] detectedExtensions = MimeTypes.findExtensionsByMimeTypes(mimeType, false);
|
||||||
if (detectedExtensions.length == 0)
|
if (detectedExtensions.length == 0)
|
||||||
@ -73,33 +130,17 @@ public class CombEndpoints {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String extension = "." + detectedExtensions[0];
|
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";
|
return "OK";
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
return e.getMessage();
|
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
|
@GET
|
||||||
@Path("{id}/photo")
|
@Path("{id}/photo")
|
||||||
@RolesAllowed("federation_admin")
|
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"})
|
||||||
public Uni<Response> getPhoto(@PathParam("id") long id) throws URISyntaxException {
|
public Uni<Response> getPhoto(@PathParam("id") long id) throws URISyntaxException {
|
||||||
Future<Pair<File, byte[]>> future = CompletableFuture.supplyAsync(() -> {
|
Future<Pair<File, byte[]>> future = CompletableFuture.supplyAsync(() -> {
|
||||||
FilenameFilter filter = (directory, filename) -> filename.startsWith(String.valueOf(id));
|
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");
|
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 -> {
|
.map(filePair -> {
|
||||||
if (filePair == null)
|
if (filePair == null)
|
||||||
return Response.temporaryRedirect(uri).build();
|
return Response.temporaryRedirect(uri).build();
|
||||||
@ -131,7 +172,7 @@ public class CombEndpoints {
|
|||||||
resp.header(HttpHeaders.CONTENT_DISPOSITION, "inline; ");
|
resp.header(HttpHeaders.CONTENT_DISPOSITION, "inline; ");
|
||||||
|
|
||||||
return resp.build();
|
return resp.build();
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,13 +2,16 @@ package fr.titionfire.ffsaf.rest;
|
|||||||
|
|
||||||
import fr.titionfire.ffsaf.domain.service.KeycloakService;
|
import fr.titionfire.ffsaf.domain.service.KeycloakService;
|
||||||
import fr.titionfire.ffsaf.rest.from.MemberPermForm;
|
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.smallrye.mutiny.Uni;
|
||||||
|
import io.vertx.mutiny.core.Vertx;
|
||||||
import jakarta.annotation.security.RolesAllowed;
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.ws.rs.GET;
|
import jakarta.ws.rs.*;
|
||||||
import jakarta.ws.rs.PUT;
|
import org.eclipse.microprofile.jwt.JsonWebToken;
|
||||||
import jakarta.ws.rs.Path;
|
import org.keycloak.representations.idm.GroupRepresentation;
|
||||||
import jakarta.ws.rs.PathParam;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -19,11 +22,26 @@ public class CompteEndpoints {
|
|||||||
@Inject
|
@Inject
|
||||||
KeycloakService service;
|
KeycloakService service;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
JsonWebToken accessToken;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@IdToken
|
||||||
|
JsonWebToken idToken;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
Vertx vertx;
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("{id}")
|
@Path("{id}")
|
||||||
@RolesAllowed("federation_admin")
|
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"})
|
||||||
public Uni<?> getCompte(@PathParam("id") String id) {
|
public Uni<KeycloakService.UserCompteState> getCompte(@PathParam("id") String id) {
|
||||||
return service.fetchCompte(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
|
@PUT
|
||||||
|
|||||||
@ -1,15 +1,21 @@
|
|||||||
package fr.titionfire.ffsaf.rest;
|
package fr.titionfire.ffsaf.rest;
|
||||||
|
|
||||||
|
import fr.titionfire.ffsaf.data.model.MembreModel;
|
||||||
import fr.titionfire.ffsaf.domain.service.LicenceService;
|
import fr.titionfire.ffsaf.domain.service.LicenceService;
|
||||||
import fr.titionfire.ffsaf.rest.data.SimpleLicence;
|
import fr.titionfire.ffsaf.rest.data.SimpleLicence;
|
||||||
import fr.titionfire.ffsaf.rest.from.LicenceForm;
|
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.Uni;
|
||||||
|
import io.smallrye.mutiny.unchecked.Unchecked;
|
||||||
import jakarta.annotation.security.RolesAllowed;
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.ws.rs.*;
|
import jakarta.ws.rs.*;
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import org.eclipse.microprofile.jwt.JsonWebToken;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
@Path("api/licence")
|
@Path("api/licence")
|
||||||
public class LicenceEndpoints {
|
public class LicenceEndpoints {
|
||||||
@ -17,12 +23,21 @@ public class LicenceEndpoints {
|
|||||||
@Inject
|
@Inject
|
||||||
LicenceService licenceService;
|
LicenceService licenceService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@IdToken
|
||||||
|
JsonWebToken idToken;
|
||||||
|
|
||||||
|
Consumer<MembreModel> checkPerm = Unchecked.consumer(membreModel -> {
|
||||||
|
if (!idToken.getGroups().contains("federation_admin") && !GroupeUtils.isInClubGroup(membreModel.getClub().getId(), idToken))
|
||||||
|
throw new ForbiddenException();
|
||||||
|
});
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("{id}")
|
@Path("{id}")
|
||||||
@RolesAllowed("federation_admin")
|
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"})
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public Uni<List<SimpleLicence>> getLicence(@PathParam("id") long id) {
|
public Uni<List<SimpleLicence>> 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
|
@POST
|
||||||
@ -41,4 +56,21 @@ public class LicenceEndpoints {
|
|||||||
public Uni<?> deleteLicence(@PathParam("id") long id) {
|
public Uni<?> deleteLicence(@PathParam("id") long id) {
|
||||||
return licenceService.deleteLicence(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<SimpleLicence> 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import lombok.Builder;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.eclipse.microprofile.jwt.JsonWebToken;
|
import org.eclipse.microprofile.jwt.JsonWebToken;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@ -19,7 +21,7 @@ public class UserInfo {
|
|||||||
String email;
|
String email;
|
||||||
boolean emailVerified;
|
boolean emailVerified;
|
||||||
long expiration;
|
long expiration;
|
||||||
Set<String> groups;
|
List<String> groups;
|
||||||
Set<String> roles;
|
Set<String> roles;
|
||||||
|
|
||||||
public static UserInfo makeUserInfo(JsonWebToken accessToken, SecurityIdentity securityIdentity) {
|
public static UserInfo makeUserInfo(JsonWebToken accessToken, SecurityIdentity securityIdentity) {
|
||||||
@ -31,7 +33,13 @@ public class UserInfo {
|
|||||||
builder.email(accessToken.getClaim("email"));
|
builder.email(accessToken.getClaim("email"));
|
||||||
builder.emailVerified(accessToken.getClaim("email_verified"));
|
builder.emailVerified(accessToken.getClaim("email_verified"));
|
||||||
builder.expiration(accessToken.getExpirationTime());
|
builder.expiration(accessToken.getExpirationTime());
|
||||||
builder.groups(accessToken.getGroups());
|
List<String> 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());
|
builder.roles(securityIdentity.getRoles());
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/main/java/fr/titionfire/ffsaf/utils/GroupeUtils.java
Normal file
25
src/main/java/fr/titionfire/ffsaf/utils/GroupeUtils.java
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,19 +4,17 @@ import io.quarkus.runtime.annotations.RegisterForReflection;
|
|||||||
|
|
||||||
@RegisterForReflection
|
@RegisterForReflection
|
||||||
public enum RoleAsso {
|
public enum RoleAsso {
|
||||||
MEMBRE("Membre"),
|
MEMBRE("Membre", 0),
|
||||||
PRESIDENT("Président"),
|
PRESIDENT("Président", 3),
|
||||||
TRESORIER("Trésorier"),
|
TRESORIER("Trésorier", 1),
|
||||||
SECRETAIRE("Secrétaire");
|
SECRETAIRE("Secrétaire", 2);
|
||||||
|
|
||||||
public String name;
|
public final String name;
|
||||||
|
public final int level;
|
||||||
|
|
||||||
RoleAsso(String name) {
|
RoleAsso(String name, int level) {
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setName(String name) {
|
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
this.level = level;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
22
src/main/java/fr/titionfire/ffsaf/utils/Utils.java
Normal file
22
src/main/java/fr/titionfire/ffsaf/utils/Utils.java
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -40,7 +40,7 @@ quarkus.oidc.token.refresh-expired=true
|
|||||||
|
|
||||||
quarkus.oidc.authentication.redirect-path=/api/auth/login
|
quarkus.oidc.authentication.redirect-path=/api/auth/login
|
||||||
quarkus.oidc.logout.path=/api/logout
|
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:
|
# Only the authenticated users can initiate a logout:
|
||||||
quarkus.http.auth.permission.authenticated.paths=api/logout,api/auth/login
|
quarkus.http.auth.permission.authenticated.paths=api/logout,api/auth/login
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {ToastContainer} from "react-toastify";
|
|||||||
|
|
||||||
import './App.css'
|
import './App.css'
|
||||||
import 'react-toastify/dist/ReactToastify.css';
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
|
import {ClubRoot, getClubChildren} from "./pages/club/ClubRoot.jsx";
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@ -25,6 +26,11 @@ const router = createBrowserRouter([
|
|||||||
path: 'admin',
|
path: 'admin',
|
||||||
element: <AdminRoot/>,
|
element: <AdminRoot/>,
|
||||||
children: getAdminChildren()
|
children: getAdminChildren()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'club',
|
||||||
|
element: <ClubRoot/>,
|
||||||
|
children: getClubChildren()
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@ -13,4 +13,16 @@ export const ColoredCircle = ({color, boolean}) => {
|
|||||||
return <Fragment>
|
return <Fragment>
|
||||||
<span className="colored-circle" style={styles}/>
|
<span className="colored-circle" style={styles}/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ColoredText = ({boolean, text={true: "Oui", false: "Non"}}) => {
|
||||||
|
const styles = {color: '#F00'};
|
||||||
|
|
||||||
|
if (boolean !== undefined) {
|
||||||
|
styles.color = (boolean) ? '#00c700' : '#e50000';
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Fragment>
|
||||||
|
<span className="font-weight-bold" style={styles}>{text[boolean]}</span>
|
||||||
|
</Fragment>
|
||||||
};
|
};
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {getCategoryFormBirthDate} from "../../../utils/Tools.js";
|
import {getCategoryFormBirthDate} from "../utils/Tools.js";
|
||||||
|
|
||||||
export function BirthDayField({inti_date, inti_category}) {
|
export function BirthDayField({inti_date, inti_category}) {
|
||||||
const [date, setDate] = useState(inti_date)
|
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 <div className="row">
|
return <div className="row">
|
||||||
<div className="input-group mb-3">
|
<div className="input-group mb-3">
|
||||||
<label className="input-group-text" id={name}>{text}</label>
|
<label className="input-group-text" id={name}>{text}</label>
|
||||||
<select className="form-select" id={name} name={name} defaultValue={value} required>
|
<select className="form-select" id={name} name={name} defaultValue={value} required disabled={disabled}>
|
||||||
{Object.keys(values).map((key, _) => {
|
{Object.keys(values).map((key, _) => {
|
||||||
return (<option key={key} value={key}>{values[key]}</option>)
|
return (<option key={key} value={key}>{values[key]}</option>)
|
||||||
})}
|
})}
|
||||||
@ -22,6 +22,7 @@ export function Nav() {
|
|||||||
<div className="collapse-item">
|
<div className="collapse-item">
|
||||||
<ul className="navbar-nav">
|
<ul className="navbar-nav">
|
||||||
<li className="nav-item"><NavLink className="nav-link" to="/">Accueil</NavLink></li>
|
<li className="nav-item"><NavLink className="nav-link" to="/">Accueil</NavLink></li>
|
||||||
|
<ClubMenu/>
|
||||||
<AdminMenu/>
|
<AdminMenu/>
|
||||||
<LoginMenu/>
|
<LoginMenu/>
|
||||||
</ul>
|
</ul>
|
||||||
@ -31,6 +32,24 @@ export function Nav() {
|
|||||||
</nav>
|
</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 <li className="nav-item dropdown">
|
||||||
|
<div className="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
Club
|
||||||
|
</div>
|
||||||
|
<ul className="dropdown-menu">
|
||||||
|
<li className="nav-item"><NavLink className="nav-link" to="/club/member">Member</NavLink></li>
|
||||||
|
<li className="nav-item"><NavLink className="nav-link" to="/club/b">B</NavLink></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
|
||||||
function AdminMenu() {
|
function AdminMenu() {
|
||||||
const {is_authenticated, userinfo} = useAuth()
|
const {is_authenticated, userinfo} = useAuth()
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
|||||||
import {apiAxios} from "../../../utils/Tools.js";
|
import {apiAxios} from "../../../utils/Tools.js";
|
||||||
import {toast} from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
import imageCompression from "browser-image-compression";
|
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";
|
import {ClubSelect} from "../../../components/ClubSelect.jsx";
|
||||||
|
|
||||||
export function InformationForm({data}) {
|
export function InformationForm({data}) {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import {useEffect, useReducer, useState} from "react";
|
|||||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||||
import {faPen} from "@fortawesome/free-solid-svg-icons";
|
import {faPen} from "@fortawesome/free-solid-svg-icons";
|
||||||
import {AxiosError} from "../../../components/AxiosError.jsx";
|
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 {apiAxios, getSaison} from "../../../utils/Tools.js";
|
||||||
import {Input} from "../../../components/Input.jsx";
|
import {Input} from "../../../components/Input.jsx";
|
||||||
import {toast} from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
@ -115,10 +115,9 @@ function removeLicence(id, dispatch) {
|
|||||||
success: "Licence supprimée avec succès 🎉",
|
success: "Licence supprimée avec succès 🎉",
|
||||||
error: "Échec de la suppression de la licence 😕"
|
error: "Échec de la suppression de la licence 😕"
|
||||||
}
|
}
|
||||||
).then(data => {
|
).then(_ => {
|
||||||
dispatch({type: 'REMOVE', payload: id})
|
dispatch({type: 'REMOVE', payload: id})
|
||||||
})
|
})
|
||||||
console.log(id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ModalContent({licence, dispatch}) {
|
function ModalContent({licence, dispatch}) {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
|||||||
import {apiAxios} from "../../../utils/Tools.js";
|
import {apiAxios} from "../../../utils/Tools.js";
|
||||||
import {toast} from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
import {useFetch} from "../../../hooks/useFetch.js";
|
import {useFetch} from "../../../hooks/useFetch.js";
|
||||||
import {CheckField} from "./MemberCustomFiels.jsx";
|
import {CheckField} from "../../../components/MemberCustomFiels.jsx";
|
||||||
import {AxiosError} from "../../../components/AxiosError.jsx";
|
import {AxiosError} from "../../../components/AxiosError.jsx";
|
||||||
|
|
||||||
export function PremForm({userData}) {
|
export function PremForm({userData}) {
|
||||||
@ -66,7 +66,7 @@ function PremFormContent({userData}) {
|
|||||||
<h5>FFSAF intra</h5>
|
<h5>FFSAF intra</h5>
|
||||||
{data
|
{data
|
||||||
? <>
|
? <>
|
||||||
<CheckField name="federation_admin" text="Accès à l'intra"
|
<CheckField name="federation_admin" text="Administrateur de la fédération"
|
||||||
value={data.includes("federation_admin")}/>
|
value={data.includes("federation_admin")}/>
|
||||||
</>
|
</>
|
||||||
: error && <AxiosError error={error}/>}
|
: error && <AxiosError error={error}/>}
|
||||||
|
|||||||
43
src/main/webapp/src/pages/club/ClubRoot.jsx
Normal file
43
src/main/webapp/src/pages/club/ClubRoot.jsx
Normal file
@ -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 <>
|
||||||
|
<div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap'}}>
|
||||||
|
<h1>Espace club</h1><h3 style={{marginLeft: '0.75em'}}>{club}</h3></div>
|
||||||
|
<LoadingProvider>
|
||||||
|
<Outlet/>
|
||||||
|
</LoadingProvider>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getClubChildren() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
path: 'member',
|
||||||
|
element: <MemberList/>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'member/:id',
|
||||||
|
element: <MemberPage/>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'b',
|
||||||
|
element: <div>Club B</div>
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
60
src/main/webapp/src/pages/club/MemberList.jsx
Normal file
60
src/main/webapp/src/pages/club/MemberList.jsx
Normal file
@ -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 <>
|
||||||
|
<SearchBar searchInput={searchInput} onSearchInputChange={setSearchInput}/>
|
||||||
|
{data
|
||||||
|
? <div className="list-group">
|
||||||
|
{visibleMember.map(member => (
|
||||||
|
<span key={member.id}
|
||||||
|
onClick={() => navigate("/club/member/" + member.id)}
|
||||||
|
className="list-group-item list-group-item-action">{member.fname} {member.lname}</span>))}
|
||||||
|
</div>
|
||||||
|
: error
|
||||||
|
? <AxiosError error={error}/>
|
||||||
|
: <Def/>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
function SearchBar({searchInput, onSearchInputChange}) {
|
||||||
|
return <div>
|
||||||
|
<div className="mb-3">
|
||||||
|
<Input value={searchInput} onChange={onSearchInputChange} placeholder="Rechercher..."/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
function Def() {
|
||||||
|
return <div className="list-group">
|
||||||
|
<li className="list-group-item"><ThreeDots/></li>
|
||||||
|
<li className="list-group-item"><ThreeDots/></li>
|
||||||
|
<li className="list-group-item"><ThreeDots/></li>
|
||||||
|
<li className="list-group-item"><ThreeDots/></li>
|
||||||
|
<li className="list-group-item"><ThreeDots/></li>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
55
src/main/webapp/src/pages/club/member/CompteInfo.jsx
Normal file
55
src/main/webapp/src/pages/club/member/CompteInfo.jsx
Normal file
@ -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 <div className="card mb-4">
|
||||||
|
<div className="card-header">Compte</div>
|
||||||
|
<div className="card-body text-center">
|
||||||
|
{userData.userId
|
||||||
|
? <CompteInfoContent userData={userData}/>
|
||||||
|
:
|
||||||
|
<>
|
||||||
|
<div className="row">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<div>Ce membre ne dispose pas de compte... <br/>
|
||||||
|
Un compte sera créé par la fédération lors de la validation de sa première licence
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
function CompteInfoContent({userData}) {
|
||||||
|
const setLoading = useLoadingSwitcher()
|
||||||
|
const {data, error} = useFetch(`/compte/${userData.userId}`, setLoading, 1)
|
||||||
|
|
||||||
|
return <>
|
||||||
|
{data
|
||||||
|
? <>
|
||||||
|
<div className="row">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<div>Identifiant: {data.login}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="row">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<div>Activer: <ColoredCircle boolean={data.enabled}/></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="row">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<div>Email vérifié: <ColoredCircle boolean={data.emailVerified}/></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
: error && <AxiosError error={error}/>
|
||||||
|
} </>
|
||||||
|
}
|
||||||
101
src/main/webapp/src/pages/club/member/InformationForm.jsx
Normal file
101
src/main/webapp/src/pages/club/member/InformationForm.jsx
Normal file
@ -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 <form onSubmit={handleSubmit}>
|
||||||
|
<div className="card mb-4">
|
||||||
|
<div className="card-header">Information</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<TextField name="lname" text="Nom" value={data.lname}/>
|
||||||
|
<TextField name="fname" text="Prénom" value={data.fname}/>
|
||||||
|
<TextField name="email" text="Email" value={data.email} placeholder="name@example.com"
|
||||||
|
type="email"/>
|
||||||
|
<OptionField name="genre" text="Genre" value={data.genre}
|
||||||
|
values={{NA: 'N/A', H: 'H', F: 'F'}}/>
|
||||||
|
<OptionField name="country" text="Pays" value={data.country}
|
||||||
|
values={{NA: 'Sélectionner...', fr: 'FR', es: 'ES', be: 'BE'}}/>
|
||||||
|
<BirthDayField inti_date={data.birth_date ? data.birth_date.split('T')[0] : ''}
|
||||||
|
inti_category={data.categorie}/>
|
||||||
|
<OptionField name="role" text="Rôle" value={data.role}
|
||||||
|
values={{
|
||||||
|
MEMBRE: 'Membre',
|
||||||
|
PRESIDENT: 'Président',
|
||||||
|
TRESORIER: 'Trésorier',
|
||||||
|
SECRETAIRE: 'Secrétaire'
|
||||||
|
}}/>
|
||||||
|
<OptionField name="grade_arbitrage" text="Grade d'arbitrage" value={data.grade_arbitrage}
|
||||||
|
values={{NA: 'N/A', ASSESSEUR: 'Assesseur', ARBITRE: 'Arbitre'}} disabled={true}/>
|
||||||
|
<div className="row">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<label className="input-group-text" htmlFor="url_photo">Photos
|
||||||
|
(optionnelle)</label>
|
||||||
|
<input type="file" className="form-control" id="url_photo" name="url_photo"
|
||||||
|
accept=".jpg,.jpeg,.gif,.png,.svg"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="row">
|
||||||
|
<div className="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||||
|
<button type="submit" className="btn btn-primary">Enregistrer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>;
|
||||||
|
}
|
||||||
176
src/main/webapp/src/pages/club/member/LicenceCard.jsx
Normal file
176
src/main/webapp/src/pages/club/member/LicenceCard.jsx
Normal file
@ -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 <div className="card mb-4 mb-md-0">
|
||||||
|
<div className="card-header container-fluid">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col">Licence</div>
|
||||||
|
<div className="col" style={{textAlign: 'right'}}>
|
||||||
|
<button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#LicenceModal"
|
||||||
|
onClick={() => setModal(defaultLicence)}
|
||||||
|
disabled={licences.some(licence => licence.saison === getSaison())}>Demander
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<ul className="list-group">
|
||||||
|
{licences.map((licence, index) => {
|
||||||
|
return <div key={index}
|
||||||
|
className={"list-group-item d-flex justify-content-between align-items-start list-group-item-" +
|
||||||
|
(licence.validate ? "success" : (licence.certificate ? "warning" : "danger"))}>
|
||||||
|
<div className="me-auto">{licence?.saison}-{licence?.saison + 1}</div>
|
||||||
|
<button className="badge btn btn-primary rounded-pill" data-bs-toggle="modal"
|
||||||
|
data-bs-target="#LicenceModal" onClick={_ => setModal(licence)}>
|
||||||
|
{licence.saison === getSaison() ? <FontAwesomeIcon icon={faPen}/> :
|
||||||
|
<FontAwesomeIcon icon={faInfo}/>}</button>
|
||||||
|
</div>
|
||||||
|
})}
|
||||||
|
{error && <AxiosError error={error}/>}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="modal fade" id="LicenceModal" tabIndex="-1" aria-labelledby="LicenceModalLabel"
|
||||||
|
aria-hidden="true">
|
||||||
|
<div className="modal-dialog">
|
||||||
|
<div className="modal-content">
|
||||||
|
<ModalContent licence={modalLicence} dispatch={dispatch}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <form onSubmit={e => sendLicence(e, dispatch)}>
|
||||||
|
<input name="id" value={licence.id} readOnly hidden/>
|
||||||
|
<input name="membre" value={licence.membre} readOnly hidden/>
|
||||||
|
<div className="modal-header">
|
||||||
|
<h1 className="modal-title fs-5" id="LicenceModalLabel">
|
||||||
|
{isNew ? "Demande de licence " : "Edition de la demande "}
|
||||||
|
(saison {licence.saison}-{licence.saison + 1})</h1>
|
||||||
|
<button type="button" className="btn-close" data-bs-dismiss="modal"
|
||||||
|
aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div className="modal-body">
|
||||||
|
<div className="btn-group input-group mb-3 justify-content-md-center" role="group"
|
||||||
|
aria-label="Basic radio toggle button group">
|
||||||
|
<span className="input-group-text">Certificat médical</span>
|
||||||
|
<input type="radio" className="btn-check" id="btnradio1" autoComplete="off" value="false"
|
||||||
|
checked={certificate === false} onChange={handleCertificateChange} disabled={!currentSaison}/>
|
||||||
|
<label className="btn btn-outline-primary" htmlFor="btnradio1">Non</label>
|
||||||
|
<input type="radio" className="btn-check" name="certificate" id="btnradio2" autoComplete="off"
|
||||||
|
value="true" checked={certificate === true} onChange={handleCertificateChange}
|
||||||
|
disabled={!currentSaison}/>
|
||||||
|
<label className="btn btn-outline-primary" htmlFor="btnradio2">Oui</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="input-group mb-3 justify-content-md-center">
|
||||||
|
<div>Validation de la licence: <ColoredText boolean={licence.validate}/></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="modal-footer">
|
||||||
|
{currentSaison &&
|
||||||
|
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Enregistrer</button>}
|
||||||
|
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
|
||||||
|
{currentSaison && licence.validate === false &&
|
||||||
|
<button type="button" className="btn btn-danger" data-bs-dismiss="modal"
|
||||||
|
onClick={() => removeLicence(licence.id, dispatch)}>Annuler</button>}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
70
src/main/webapp/src/pages/club/member/MemberPage.jsx
Normal file
70
src/main/webapp/src/pages/club/member/MemberPage.jsx
Normal file
@ -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 <>
|
||||||
|
<h2>Page membre</h2>
|
||||||
|
<button type="button" className="btn btn-link" onClick={() => navigate("/club/member")}>
|
||||||
|
<< retour
|
||||||
|
</button>
|
||||||
|
{data
|
||||||
|
? <div>
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-lg-4">
|
||||||
|
<PhotoCard data={data}/>
|
||||||
|
<LoadingProvider><CompteInfo userData={data}/></LoadingProvider>
|
||||||
|
</div>
|
||||||
|
<div className="col-lg-8">
|
||||||
|
<InformationForm data={data}/>
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-md-6">
|
||||||
|
<LoadingProvider><LicenceCard userData={data}/></LoadingProvider>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<LoadingProvider><SelectCard/></LoadingProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
: error && <AxiosError error={error}/>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
function PhotoCard({data}) {
|
||||||
|
return <div className="card mb-4">
|
||||||
|
<div className="card-header">Licence n°{data.licence}</div>
|
||||||
|
<div className="card-body text-center">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<img
|
||||||
|
src={`${vite_url}/api/member/${data.id}/photo`}
|
||||||
|
alt="avatar"
|
||||||
|
className="rounded-circle img-fluid" style={{object_fit: 'contain'}}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectCard() {
|
||||||
|
return <div className="card mb-4 mb-md-0">
|
||||||
|
<div className="card-header">Sélection en équipe de France</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<p className="mb-1">Soon</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user