diff --git a/pom.xml b/pom.xml index 64b4168..6c3aafc 100644 --- a/pom.xml +++ b/pom.xml @@ -104,6 +104,11 @@ jodd-util 6.2.1 + + + io.quarkus + quarkus-websockets + diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/AffiliationRequestModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/AffiliationRequestModel.java new file mode 100644 index 0000000..fc09800 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/data/model/AffiliationRequestModel.java @@ -0,0 +1,43 @@ +package fr.titionfire.ffsaf.data.model; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import jakarta.persistence.*; +import lombok.*; + + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@RegisterForReflection + +@Entity +@Table(name = "affiliation_request") +public class AffiliationRequestModel { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + + String name; + String siren; + String RNA; + String address; + + String president_lname; + String president_fname; + String president_email; + int president_lincence; + + String tresorier_lname; + String tresorier_fname; + String tresorier_email; + int tresorier_lincence; + + String secretaire_lname; + String secretaire_fname; + String secretaire_email; + int secretaire_lincence; + + int saison; +} diff --git a/src/main/java/fr/titionfire/ffsaf/data/repository/AffiliationRequestRepository.java b/src/main/java/fr/titionfire/ffsaf/data/repository/AffiliationRequestRepository.java new file mode 100644 index 0000000..bbe55b0 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/data/repository/AffiliationRequestRepository.java @@ -0,0 +1,9 @@ +package fr.titionfire.ffsaf.data.repository; + +import fr.titionfire.ffsaf.data.model.AffiliationRequestModel; +import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class AffiliationRequestRepository implements PanacheRepositoryBase { +} diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java new file mode 100644 index 0000000..071b9a8 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java @@ -0,0 +1,60 @@ +package fr.titionfire.ffsaf.domain.service; + +import fr.titionfire.ffsaf.data.model.AffiliationRequestModel; +import fr.titionfire.ffsaf.data.repository.AffiliationRequestRepository; +import fr.titionfire.ffsaf.data.repository.CombRepository; +import fr.titionfire.ffsaf.rest.from.AffiliationRequestForm; +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 jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +@WithSession +@ApplicationScoped +public class AffiliationService { + + @Inject + CombRepository combRepository; + + @Inject + AffiliationRequestRepository repository; + + @ConfigProperty(name = "upload_dir") + String media; + + public Uni save(AffiliationRequestForm form) { + AffiliationRequestModel affModel = form.toModel(); + affModel.setSaison(Utils.getSaison()); + + return Uni.createFrom().item(affModel) + .call(model -> ((model.getPresident_lincence() != 0) ? combRepository.find("licence", + model.getPresident_lincence()).count().invoke(count -> { + if (count == 0) { + throw new IllegalArgumentException("Licence président inconnue"); + } + }) : Uni.createFrom().nullItem()) + ) + .call(model -> ((model.getTresorier_lincence() != 0) ? combRepository.find("licence", + model.getTresorier_lincence()).count().invoke(count -> { + if (count == 0) { + throw new IllegalArgumentException("Licence trésorier inconnue"); + } + }) : Uni.createFrom().nullItem()) + ) + .call(model -> ((model.getSecretaire_lincence() != 0) ? combRepository.find("licence", + model.getSecretaire_lincence()).count().invoke(count -> { + if (count == 0) { + throw new IllegalArgumentException("Licence secrétaire inconnue"); + } + }) : Uni.createFrom().nullItem()) + ).chain(model -> Panache.withTransaction(() -> repository.persist(model))) + .call(model -> Uni.createFrom().future(Utils.replacePhoto(model.getId(), form.getLogo(), media, + "aff_request/logo"))) + .call(model -> Uni.createFrom().future(Utils.replacePhoto(model.getId(), form.getStatus(), media, + "aff_request/status"))) + .map(__ -> "Ok"); + } +} 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 df578b9..a70711d 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/KeycloakService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/KeycloakService.java @@ -121,7 +121,7 @@ public class KeycloakService { return vertx.getOrCreateContext().executeBlocking(() -> { UserResource user = keycloak.realm(realm).users().get(id); UserRepresentation user2 = user.toRepresentation(); - return new Pair<>(user, new UserCompteState(user2.isEnabled(), user2.getUsername(), user2.isEmailVerified())) ; + return new Pair<>(user, new UserCompteState(user2.isEnabled(), user2.getUsername(), user2.isEmailVerified())); }); } @@ -195,6 +195,17 @@ public class KeycloakService { return membreService.setUserId(id, nid).map(__ -> "OK"); } + public Uni removeAccount(String userId) { + return vertx.getOrCreateContext().executeBlocking(() -> { + try (Response response = keycloak.realm(realm).users().delete(userId)) { + System.out.println(response.getStatusInfo()); + if (!response.getStatusInfo().equals(Response.Status.NO_CONTENT)) + throw new KeycloakException("Fail to delete user %s (reason=%s)".formatted(userId, response.getStatusInfo().getReasonPhrase())); + } + return null; + }); + } + private Optional getUser(String username) { List users = keycloak.realm(realm).users().searchByUsername(username, true); 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 d059892..cdea2aa 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java @@ -1,18 +1,17 @@ package fr.titionfire.ffsaf.domain.service; +import fr.titionfire.ffsaf.data.model.ClubModel; import fr.titionfire.ffsaf.data.model.MembreModel; import fr.titionfire.ffsaf.data.repository.ClubRepository; import fr.titionfire.ffsaf.data.repository.CombRepository; +import fr.titionfire.ffsaf.data.repository.LicenceRepository; 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.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.PageResult; -import fr.titionfire.ffsaf.utils.Pair; -import fr.titionfire.ffsaf.utils.RoleAsso; +import fr.titionfire.ffsaf.utils.*; import io.quarkus.hibernate.reactive.panache.Panache; import io.quarkus.hibernate.reactive.panache.PanacheQuery; import io.quarkus.hibernate.reactive.panache.common.WithSession; @@ -37,6 +36,9 @@ public class MembreService { CombRepository repository; @Inject ClubRepository clubRepository; + @Inject + LicenceRepository licenceRepository; + @Inject ServerCustom serverCustom; @Inject @@ -163,10 +165,74 @@ public class MembreService { .map(__ -> "OK"); } + public Uni add(FullMemberForm input) { + return clubRepository.findById(input.getClub()) + .chain(clubModel -> { + MembreModel model = getMembreModel(input, clubModel); + return Panache.withTransaction(() -> repository.persist(model)); + }) + .invoke(membreModel -> SReqComb.sendIfNeedAdd(serverCustom.clients, SimpleCombModel.fromModel(membreModel))) + .map(MembreModel::getId); + } + + public Uni add(FullMemberForm input, String subject) { + return repository.find("userId = ?1", subject).firstResult() + .chain(membreModel -> { + MembreModel model = getMembreModel(input, membreModel.getClub()); + model.setRole(RoleAsso.MEMBRE); + model.setGrade_arbitrage(GradeArbitrage.NA); + return Panache.withTransaction(() -> repository.persist(model)); + }) + .invoke(membreModel -> SReqComb.sendIfNeedAdd(serverCustom.clients, SimpleCombModel.fromModel(membreModel))) + .map(MembreModel::getId); + } + + public Uni delete(long id) { + return repository.findById(id) + .call(membreModel -> (membreModel.getUserId() != null) ? + keycloakService.removeAccount(membreModel.getUserId()) : Uni.createFrom().nullItem()) + .call(membreModel -> Panache.withTransaction(() -> repository.delete(membreModel))) + .invoke(membreModel -> SReqComb.sendRm(serverCustom.clients, id)) + .map(__ -> "Ok"); + } + + public Uni delete(long id, JsonWebToken idToken) { + return repository.findById(id) + .invoke(Unchecked.consumer(membreModel -> { + if (!GroupeUtils.isInClubGroup(membreModel.getClub().getId(), idToken)) + throw new ForbiddenException(); + })) + .call(membreModel -> licenceRepository.find("membre = ?1", membreModel).count() + .invoke(Unchecked.consumer(l -> { + if (l > 0) + throw new BadRequestException(); + }))) + .call(membreModel -> (membreModel.getUserId() != null) ? + keycloakService.removeAccount(membreModel.getUserId()) : Uni.createFrom().nullItem()) + .call(membreModel -> Panache.withTransaction(() -> repository.delete(membreModel))) + .invoke(membreModel -> SReqComb.sendRm(serverCustom.clients, id)) + .map(__ -> "Ok"); + } + public Uni setUserId(Long id, String id1) { return repository.findById(id).chain(membreModel -> { membreModel.setUserId(id1); return Panache.withTransaction(() -> repository.persist(membreModel)); }); } + + private static MembreModel getMembreModel(FullMemberForm input, ClubModel clubModel) { + MembreModel model = new MembreModel(); + model.setFname(input.getFname()); + model.setLname(input.getLname()); + model.setEmail(input.getEmail()); + model.setGenre(input.getGenre()); + model.setCountry(input.getCountry()); + model.setBirth_date(input.getBirth_date()); + model.setCategorie(input.getCategorie()); + model.setClub(clubModel); + model.setRole(input.getRole()); + model.setGrade_arbitrage(input.getGrade_arbitrage()); + return model; + } } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/AffiliationEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/AffiliationEndpoints.java new file mode 100644 index 0000000..dc2bbbc --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/AffiliationEndpoints.java @@ -0,0 +1,29 @@ +package fr.titionfire.ffsaf.rest; + +import fr.titionfire.ffsaf.rest.from.AffiliationRequestForm; +import io.smallrye.mutiny.Uni; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; + +@Path("api/affiliation") +public class AffiliationEndpoints { + + + + @POST + @Path("save") + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.MULTIPART_FORM_DATA) + public Uni saveAffRequest(AffiliationRequestForm form) { + System.out.println(form); + return Uni.createFrom().item("OK"); + } + /*@POST + @Path("affiliation") + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.MULTIPART_FORM_DATA) + public Uni saveAffRequest(AffiliationRequestForm form) { + System.out.println(form); + return service.save(form); + }*/ +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/AssoEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/AssoEndpoints.java index d254a1b..6bfa442 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/AssoEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/AssoEndpoints.java @@ -1,28 +1,53 @@ package fr.titionfire.ffsaf.rest; +import fr.titionfire.ffsaf.domain.service.AffiliationService; import fr.titionfire.ffsaf.rest.client.SirenService; import fr.titionfire.ffsaf.rest.data.UniteLegaleRoot; +import fr.titionfire.ffsaf.rest.from.AffiliationRequestForm; import io.smallrye.mutiny.Uni; +import jakarta.inject.Inject; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; +import jodd.net.MimeTypes; +import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.rest.client.inject.RestClient; +import java.io.*; +import java.net.URLConnection; +import java.nio.file.Files; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; + @Path("api/asso") public class AssoEndpoints { @RestClient SirenService sirenService; + @Inject + AffiliationService service; + + @ConfigProperty(name = "upload_dir") + String media; + @GET @Path("siren/{siren}") @Produces(MediaType.APPLICATION_JSON) public Uni getInfoSiren(@PathParam("siren") String siren) { return sirenService.get_unite(siren).onFailure().transform(throwable -> { - if (throwable instanceof WebApplicationException exception){ + if (throwable instanceof WebApplicationException exception) { if (exception.getResponse().getStatus() == 400) return new BadRequestException("Not found"); } return throwable; }); } + + @POST + @Path("affiliation") + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.MULTIPART_FORM_DATA) + public Uni saveAffRequest(AffiliationRequestForm form) { + return service.save(form); + } } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/CombEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/CombEndpoints.java index 82a0209..d5c5dfa 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/CombEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/CombEndpoints.java @@ -8,6 +8,7 @@ import fr.titionfire.ffsaf.rest.from.FullMemberForm; import fr.titionfire.ffsaf.utils.GroupeUtils; import fr.titionfire.ffsaf.utils.PageResult; import fr.titionfire.ffsaf.utils.Pair; +import fr.titionfire.ffsaf.utils.Utils; import io.quarkus.oidc.IdToken; import io.quarkus.security.Authenticated; import io.quarkus.security.identity.SecurityIdentity; @@ -58,8 +59,10 @@ public class CombEndpoints { @Path("/find/admin") @RolesAllowed({"federation_admin"}) @Produces(MediaType.APPLICATION_JSON) - public Uni> getFindAdmin(@QueryParam("limit") Integer limit, @QueryParam("page") Integer page, - @QueryParam("search") String search, @QueryParam("club") String club) { + public Uni> getFindAdmin(@QueryParam("limit") Integer limit, + @QueryParam("page") Integer page, + @QueryParam("search") String search, + @QueryParam("club") String club) { if (limit == null) limit = 50; if (page == null || page < 1) @@ -71,7 +74,8 @@ public class CombEndpoints { @Path("/find/club") @RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"}) @Produces(MediaType.APPLICATION_JSON) - public Uni> getFindClub(@QueryParam("limit") Integer limit, @QueryParam("page") Integer page, + public Uni> getFindClub(@QueryParam("limit") Integer limit, + @QueryParam("page") Integer page, @QueryParam("search") String search) { if (limit == null) limit = 50; @@ -88,7 +92,7 @@ public class CombEndpoints { return membreService.getById(id).onItem().invoke(checkPerm).map(SimpleMembre::fromModel); } - @POST + @PUT @Path("{id}") @RolesAllowed({"federation_admin"}) @Produces(MediaType.TEXT_PLAIN) @@ -99,7 +103,8 @@ public class CombEndpoints { 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 -> { + return Uni.createFrom().future(Utils.replacePhoto(id, input.getPhoto_data(), media, "ppMembre" + )).invoke(Unchecked.consumer(out -> { if (!out.equals("OK")) throw new InternalError("Fail to get MimeType " + out); })); else @@ -108,6 +113,31 @@ public class CombEndpoints { } @POST + @RolesAllowed({"federation_admin"}) + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.MULTIPART_FORM_DATA) + public Uni addAdminMembre(FullMemberForm input) { + return membreService.add(input) + .invoke(Unchecked.consumer(id -> { + if (id == null) throw new InternalError("Fail to creat member data"); + })).call(id -> { + if (input.getPhoto_data().length > 0) + return Uni.createFrom().future(Utils.replacePhoto(id, input.getPhoto_data(), media, "ppMembre" + )); + else + return Uni.createFrom().nullItem(); + }); + } + + @DELETE + @Path("{id}") + @RolesAllowed({"federation_admin"}) + @Produces(MediaType.TEXT_PLAIN) + public Uni deleteAdminMembre(@PathParam("id") long id) { + return membreService.delete(id); + } + + @PUT @Path("club/{id}") @RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"}) @Produces(MediaType.TEXT_PLAIN) @@ -118,7 +148,8 @@ public class CombEndpoints { 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 -> { + return Uni.createFrom().future(Utils.replacePhoto(id, input.getPhoto_data(), media, "ppMembre" + )).invoke(Unchecked.consumer(out -> { if (!out.equals("OK")) throw new InternalError("Fail to get MimeType " + out); })); else @@ -126,6 +157,32 @@ public class CombEndpoints { }); } + @POST + @Path("club") + @RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"}) + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.MULTIPART_FORM_DATA) + public Uni addMembre(FullMemberForm input) { + return membreService.add(input, idToken.getSubject()) + .invoke(Unchecked.consumer(id -> { + if (id == null) throw new InternalError("Fail to creat member data"); + })).call(id -> { + if (input.getPhoto_data().length > 0) + return Uni.createFrom().future(Utils.replacePhoto(id, input.getPhoto_data(), media, "ppMembre" + )); + else + return Uni.createFrom().nullItem(); + }); + } + + @DELETE + @Path("club/{id}") + @RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"}) + @Produces(MediaType.TEXT_PLAIN) + public Uni deleteMembre(@PathParam("id") long id) { + return membreService.delete(id, idToken); + } + private Future replacePhoto(long id, byte[] input) { return CompletableFuture.supplyAsync(() -> { try (InputStream is = new BufferedInputStream(new ByteArrayInputStream(input))) { diff --git a/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestForm.java b/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestForm.java index 16d34fa..04f002d 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestForm.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestForm.java @@ -1,5 +1,6 @@ package fr.titionfire.ffsaf.rest.from; +import fr.titionfire.ffsaf.data.model.AffiliationRequestModel; import jakarta.ws.rs.FormParam; import jakarta.ws.rs.core.MediaType; import lombok.Getter; @@ -28,4 +29,59 @@ public class AffiliationRequestForm { @FormParam("logo") @PartType(MediaType.APPLICATION_OCTET_STREAM) private byte[] logo = new byte[0]; + + @FormParam("president-nom") + private String president_lname = null; + @FormParam("president-prenom") + private String president_fname = null; + @FormParam("president-mail") + private String president_email = null; + @FormParam("president-licence") + private String president_lincence = null; + + @FormParam("tresorier-nom") + private String tresorier_lname = null; + @FormParam("tresorier-prenom") + private String tresorier_fname = null; + @FormParam("tresorier-mail") + private String tresorier_email = null; + @FormParam("tresorier-licence") + private String tresorier_lincence = null; + + @FormParam("secretaire-nom") + private String secretaire_lname = null; + @FormParam("secretaire-prenom") + private String secretaire_fname = null; + @FormParam("secretaire-mail") + private String secretaire_email = null; + @FormParam("secretaire-licence") + private String secretaire_lincence = null; + + public AffiliationRequestModel toModel() { + AffiliationRequestModel model = new AffiliationRequestModel(); + model.setName(this.getName()); + model.setSiren(this.getSiren()); + model.setRNA(this.getRna()); + model.setAddress(this.getAdresse()); + + model.setPresident_lname(this.getPresident_lname()); + model.setPresident_fname(this.getPresident_fname()); + model.setPresident_email(this.getPresident_email()); + model.setPresident_lincence((this.getPresident_lincence() == null || this.getPresident_lincence().isBlank()) + ? 0 : Integer.parseInt(this.getPresident_lincence())); + + model.setTresorier_lname(this.getTresorier_lname()); + model.setTresorier_fname(this.getTresorier_fname()); + model.setTresorier_email(this.getTresorier_email()); + model.setTresorier_lincence((this.getPresident_lincence() == null || this.getPresident_lincence().isBlank()) + ? 0 : Integer.parseInt(this.getTresorier_lincence())); + + model.setSecretaire_lname(this.getSecretaire_lname()); + model.setSecretaire_fname(this.getSecretaire_fname()); + model.setSecretaire_email(this.getSecretaire_email()); + model.setSecretaire_lincence((this.getPresident_lincence() == null || this.getPresident_lincence().isBlank()) + ? 0 : Integer.parseInt(this.getSecretaire_lincence())); + + return model; + } } diff --git a/src/main/java/fr/titionfire/ffsaf/utils/Utils.java b/src/main/java/fr/titionfire/ffsaf/utils/Utils.java index 1a33bb2..88c4925 100644 --- a/src/main/java/fr/titionfire/ffsaf/utils/Utils.java +++ b/src/main/java/fr/titionfire/ffsaf/utils/Utils.java @@ -1,7 +1,14 @@ package fr.titionfire.ffsaf.utils; +import jodd.net.MimeTypes; + +import java.io.*; +import java.net.URLConnection; +import java.nio.file.Files; import java.util.Calendar; import java.util.Date; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; public class Utils { @@ -19,4 +26,35 @@ public class Utils { return calendar.get(Calendar.YEAR) - 1; } } + + public static Future replacePhoto(long id, byte[] input, String media, String dir) { + 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) + throw new IOException("Fail to detect file extension for MIME type " + mimeType); + + File dirFile = new File(media, dir); + if (!dirFile.exists()) + if (dirFile.mkdirs()) + throw new IOException("Fail to create directory " + dir); + + FilenameFilter filter = (directory, filename) -> filename.startsWith(String.valueOf(id)); + File[] files = dirFile.listFiles(filter); + if (files != null) { + for (File file : files) { + //noinspection ResultOfMethodCallIgnored + file.delete(); + } + } + + String extension = "." + detectedExtensions[0]; + Files.write(new File(dirFile, id + extension).toPath(), input); + return "OK"; + } catch (IOException e) { + return e.getMessage(); + } + }); + } } diff --git a/src/main/java/fr/titionfire/ffsaf/ws/FileSocket.java b/src/main/java/fr/titionfire/ffsaf/ws/FileSocket.java new file mode 100644 index 0000000..be5e725 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/ws/FileSocket.java @@ -0,0 +1,84 @@ +package fr.titionfire.ffsaf.ws; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.websocket.*; +import jakarta.websocket.server.PathParam; +import jakarta.websocket.server.ServerEndpoint; +import lombok.AllArgsConstructor; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@ServerEndpoint("/api/ws/file/{id}") +@ApplicationScoped +public class FileSocket { + Map sessions = new ConcurrentHashMap<>(); + + @OnOpen + public void onOpen(Session session, @PathParam("id") String id) { + try { + File file = File.createTempFile("safca-", ".tmp"); + FileRecv fileRecv = new FileRecv(file, new FileOutputStream(file, true), System.currentTimeMillis(), + session); + System.out.println("File created: " + file.getAbsolutePath()); + sessions.put(id, fileRecv); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @OnClose + public void onClose(Session session, @PathParam("id") String id) { + if (sessions.containsKey(id)) { + FileRecv fileRecv = sessions.get(id); + if (fileRecv.fos != null) { + try { + fileRecv.fos.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + sessions.remove(id); + } + + @OnError + public void onError(Session session, @PathParam("id") String id, Throwable throwable) { + if (sessions.containsKey(id)) { + FileRecv fileRecv = sessions.get(id); + if (fileRecv.fos != null) { + try { + fileRecv.fos.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + sessions.remove(id); + } + + @OnMessage + public void onMessage(String message, @PathParam("id") String id) { + if (sessions.containsKey(id)) { + FileRecv fileRecv = sessions.get(id); + try { + fileRecv.fos.write(message.getBytes()); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + @AllArgsConstructor + @RegisterForReflection + class FileRecv { + File file; + FileOutputStream fos; + long time; + Session session; + } +} diff --git a/src/main/webapp/src/components/ConfirmDialog.jsx b/src/main/webapp/src/components/ConfirmDialog.jsx new file mode 100644 index 0000000..b3719ad --- /dev/null +++ b/src/main/webapp/src/components/ConfirmDialog.jsx @@ -0,0 +1,19 @@ + +export function ConfirmDialog({title, message, onConfirm = () => {}, onCancel = () => {}, id = "confirm-delete"}) { + return +} \ No newline at end of file diff --git a/src/main/webapp/src/pages/DemandeAff.jsx b/src/main/webapp/src/pages/DemandeAff.jsx index 585bae7..b8f92b7 100644 --- a/src/main/webapp/src/pages/DemandeAff.jsx +++ b/src/main/webapp/src/pages/DemandeAff.jsx @@ -31,28 +31,35 @@ export function DemandeAff() { event.preventDefault() const formData = new FormData(event.target) toast.promise( - apiAxios.post(`asso/affiliation`, formData), + apiAxios.post(`asso/affiliation`, formData, { headers: {'Accept': '*/*'}}), { pending: "Enregistrement de la demande d'affiliation en cours", success: "Demande d'affiliation enregistrée avec succès 🎉", error: "Échec de la demande d'affiliation 😕" } ).then(_ => { - navigate("/affiliation/ok") + // navigate("/affiliation/ok") }) } return

Demande d'affiliation

-

L'affiliation est annuelle et valable pour une saison sportive : du 1er septembre au 31 août de l’année suivante.

+

L'affiliation est annuelle et valable pour une saison sportive : du 1er septembre au 31 août de l’année + suivante.

Pour s’affilier, une association sportive doit réunir les conditions suivantes :
  • Avoir son siège social en France ou Principauté de Monaco
  • Être constituée conformément au chapitre 1er du titre II du livre 1er du Code du Sport
  • Poursuivre un objet social entrant dans la définition de l’article 1 des statuts de la Fédération
  • -
  • Disposer de statuts compatibles avec les principes d’organisation et de fonctionnement de la Fédération
  • -
  • Assurer en son sein la liberté d’opinion et le respect des droits de la défense, et s’interdire toute discrimination
  • -
  • Respecter les règles d’encadrement, d’hygiène et de sécurité établies par les règlements de la Fédération
  • +
  • Disposer de statuts compatibles avec les principes d’organisation et de fonctionnement de la + Fédération +
  • +
  • Assurer en son sein la liberté d’opinion et le respect des droits de la défense, et s’interdire toute + discrimination +
  • +
  • Respecter les règles d’encadrement, d’hygiène et de sécurité établies par les règlements de la + Fédération +
@@ -68,16 +75,20 @@ export function DemandeAff() {
-

Après validation de votre demande, vous recevrez un login et mot de passe provisoire pour accéder à votre espace FFSAF

+

Après validation de votre demande, vous recevrez un login et mot de passe provisoire pour + accéder à votre espace FFSAF

Notez que pour finaliser votre affiliation, il vous faudra :
    -
  • Disposer d’au moins trois membres licenciés, dont le président, le trésorier et le secrétaire
  • +
  • Disposer d’au moins trois membres licenciés, dont le président, le trésorier et le + secrétaire +
  • S'être acquitté des cotisations prévues par les règlements fédéraux
- +
@@ -113,7 +124,8 @@ function AssoInfo() { return <>
Nom de l'association* -
@@ -121,24 +133,29 @@ function AssoInfo() { N° SIREN* setSiren(e.target.value)}/> - +
Dénomination -
RNA - setRna(e.target.value)}/>
Adresse* - setAdresse(e.target.value)}/>
@@ -149,7 +166,8 @@ function AssoInfo() {
- +
; } @@ -159,19 +177,29 @@ function MembreInfo({role}) {
- +
- - + +
- - + + +
+
+
+
OU
+
+ +
@@ -181,7 +209,8 @@ export function DemandeAffOk() { return (

Demande d'affiliation envoyée avec succès

-

Une fois votre demande validée, vous recevrez un login et mot de passe provisoire pour accéder à votre espace FFSAF

+

Une fois votre demande validée, vous recevrez un login et mot de passe provisoire pour accéder à votre + espace FFSAF

); } \ No newline at end of file diff --git a/src/main/webapp/src/pages/admin/AdminRoot.jsx b/src/main/webapp/src/pages/admin/AdminRoot.jsx index 1ea1031..e2860ed 100644 --- a/src/main/webapp/src/pages/admin/AdminRoot.jsx +++ b/src/main/webapp/src/pages/admin/AdminRoot.jsx @@ -3,6 +3,7 @@ import './AdminRoot.css' import {LoadingProvider} from "../../hooks/useLoading.jsx"; import {MemberList} from "../MemberList.jsx"; import {MemberPage} from "./member/MemberPage.jsx"; +import {NewMemberPage} from "./member/NewMemberPage.jsx"; export function AdminRoot() { return <> @@ -13,7 +14,7 @@ export function AdminRoot() { } -export function getAdminChildren () { +export function getAdminChildren() { return [ { path: 'member', @@ -23,6 +24,10 @@ export function getAdminChildren () { path: 'member/:id', element: }, + { + path: 'member/new', + element: + }, { path: 'b', element:
Admin B
diff --git a/src/main/webapp/src/pages/admin/member/InformationForm.jsx b/src/main/webapp/src/pages/admin/member/InformationForm.jsx index 265decc..034ef09 100644 --- a/src/main/webapp/src/pages/admin/member/InformationForm.jsx +++ b/src/main/webapp/src/pages/admin/member/InformationForm.jsx @@ -5,6 +5,27 @@ import imageCompression from "browser-image-compression"; import {BirthDayField, OptionField, TextField} from "../../../components/MemberCustomFiels.jsx"; import {ClubSelect} from "../../../components/ClubSelect.jsx"; +export function addPhoto(event, formData, send) { + 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) + } +} + export function InformationForm({data}) { const setLoading = useLoadingSwitcher() const handleSubmit = (event) => { @@ -25,7 +46,7 @@ export function InformationForm({data}) { formData.append("grade_arbitrage", event.target.grade_arbitrage?.value); const send = (formData_) => { - apiAxios.post(`/member/${data.id}`, formData_, { + apiAxios.put(`/member/${data.id}`, formData_, { headers: { 'Accept': '*/*', 'Content-Type': 'multipart/form-data', @@ -40,25 +61,7 @@ export function InformationForm({data}) { 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) - } + addPhoto(event, formData, send); } return
diff --git a/src/main/webapp/src/pages/admin/member/MemberPage.jsx b/src/main/webapp/src/pages/admin/member/MemberPage.jsx index badac6f..d2dd0dd 100644 --- a/src/main/webapp/src/pages/admin/member/MemberPage.jsx +++ b/src/main/webapp/src/pages/admin/member/MemberPage.jsx @@ -6,6 +6,9 @@ import {CompteInfo} from "./CompteInfo.jsx"; import {PremForm} from "./PremForm.jsx"; import {InformationForm} from "./InformationForm.jsx"; import {LicenceCard} from "./LicenceCard.jsx"; +import {toast} from "react-toastify"; +import {apiAxios} from "../../../utils/Tools.js"; +import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx"; const vite_url = import.meta.env.VITE_URL; @@ -16,6 +19,19 @@ export function MemberPage() { const setLoading = useLoadingSwitcher() const {data, error} = useFetch(`/member/${id}`, setLoading, 1) + const handleRm = () => { + toast.promise( + apiAxios.delete(`/member/${id}`), + { + pending: "Suppression du compte en cours...", + success: "Compte supprimé avec succès 🎉", + error: "Échec de la suppression du compte 😕" + } + ).then(_ => { + navigate("/admin/member") + }) + } + return <>

Page membre

+ + diff --git a/src/main/webapp/src/pages/admin/member/NewMemberPage.jsx b/src/main/webapp/src/pages/admin/member/NewMemberPage.jsx new file mode 100644 index 0000000..18fb3b7 --- /dev/null +++ b/src/main/webapp/src/pages/admin/member/NewMemberPage.jsx @@ -0,0 +1,107 @@ +import {useNavigate} from "react-router-dom"; +import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; +import {apiAxios} from "../../../utils/Tools.js"; +import {toast} from "react-toastify"; +import {BirthDayField, OptionField, TextField} from "../../../components/MemberCustomFiels.jsx"; +import {ClubSelect} from "../../../components/ClubSelect.jsx"; +import {addPhoto} from "./InformationForm.jsx"; + +export function NewMemberPage() { + const navigate = useNavigate(); + + return <> +

Page membre

+ +
+
+ +
+
+ +} + +function Form() { + const navigate = useNavigate(); + const setLoading = useLoadingSwitcher() + + const handleSubmit = (event) => { + event.preventDefault(); + setLoading(1) + + const formData = new FormData(); + formData.append("id", -1); + formData.append("lname", event.target.lname?.value); + formData.append("fname", event.target.fname?.value); + formData.append("categorie", event.target.category?.value); + formData.append("club", event.target.club?.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); + formData.append("grade_arbitrage", event.target.grade_arbitrage?.value); + + const send = (formData_) => { + apiAxios.post(`/member`, formData_, { + headers: { + 'Accept': '*/*', + 'Content-Type': 'multipart/form-data', + } + }).then(data => { + toast.success('Profile crée avec succès 🎉'); + navigate(`/admin/member/${data.data}`) + }).catch(e => { + console.log(e.response) + toast.error('Échec de la création du profile 😕 (code: ' + e.response.status + ')'); + }).finally(() => { + if (setLoading) + setLoading(0) + }) + } + + addPhoto(event, formData, send); + } + + return +
+
Nouveau membre
+
+ + + + + + +
+ +
+ + +
+
+ + +
+
+
+
+ +
+
+
+
+ ; +} diff --git a/src/main/webapp/src/pages/club/ClubRoot.jsx b/src/main/webapp/src/pages/club/ClubRoot.jsx index 5ddc95a..a59b0d3 100644 --- a/src/main/webapp/src/pages/club/ClubRoot.jsx +++ b/src/main/webapp/src/pages/club/ClubRoot.jsx @@ -3,6 +3,7 @@ import {LoadingProvider} from "../../hooks/useLoading.jsx"; import {MemberPage} from "./member/MemberPage.jsx"; import {useAuth} from "../../hooks/useAuth.jsx"; import {MemberList} from "../MemberList.jsx"; +import {NewMemberPage} from "./member/NewMemberPage.jsx"; export function ClubRoot() { const {userinfo} = useAuth() @@ -35,6 +36,10 @@ export function getClubChildren() { path: 'member/:id', element: }, + { + path: 'member/new', + element: + }, { path: 'b', element:
Club B
diff --git a/src/main/webapp/src/pages/club/member/InformationForm.jsx b/src/main/webapp/src/pages/club/member/InformationForm.jsx index 4fd541c..d84f34e 100644 --- a/src/main/webapp/src/pages/club/member/InformationForm.jsx +++ b/src/main/webapp/src/pages/club/member/InformationForm.jsx @@ -3,9 +3,8 @@ 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"; +import {addPhoto} from "../../admin/member/InformationForm.jsx"; export function InformationForm({data}) { const setLoading = useLoadingSwitcher() @@ -25,7 +24,7 @@ export function InformationForm({data}) { formData.append("role", event.target.role?.value); const send = (formData_) => { - apiAxios.post(`/member/club/${data.id}`, formData_, { + apiAxios.put(`/member/club/${data.id}`, formData_, { headers: { 'Accept': '*/*', 'Content-Type': 'multipart/form-data', @@ -40,23 +39,7 @@ export function InformationForm({data}) { 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) - } + addPhoto(event, formData, send); } return
@@ -79,7 +62,7 @@ export function InformationForm({data}) { PRESIDENT: 'Président', TRESORIER: 'Trésorier', SECRETAIRE: 'Secrétaire' - }}/> + }} disabled={true}/>
diff --git a/src/main/webapp/src/pages/club/member/MemberPage.jsx b/src/main/webapp/src/pages/club/member/MemberPage.jsx index 622fd5d..634fa3b 100644 --- a/src/main/webapp/src/pages/club/member/MemberPage.jsx +++ b/src/main/webapp/src/pages/club/member/MemberPage.jsx @@ -5,6 +5,9 @@ import {AxiosError} from "../../../components/AxiosError.jsx"; import {CompteInfo} from "./CompteInfo.jsx"; import {InformationForm} from "./InformationForm.jsx"; import {LicenceCard} from "./LicenceCard.jsx"; +import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx"; +import {apiAxios} from "../../../utils/Tools.js"; +import {toast} from "react-toastify"; const vite_url = import.meta.env.VITE_URL; @@ -15,6 +18,19 @@ export function MemberPage() { const setLoading = useLoadingSwitcher() const {data, error} = useFetch(`/member/${id}`, setLoading, 1) + const handleRm = () => { + toast.promise( + apiAxios.delete(`/member/club/${id}`), + { + pending: "Suppression du compte en cours...", + success: "Compte supprimé avec succès 🎉", + error: "Échec de la suppression du compte 😕" + } + ).then(_ => { + navigate("/club/member") + }) + } + return <>

Page membre

+
+ +
+ diff --git a/src/main/webapp/src/pages/club/member/NewMemberPage.jsx b/src/main/webapp/src/pages/club/member/NewMemberPage.jsx new file mode 100644 index 0000000..d5fd913 --- /dev/null +++ b/src/main/webapp/src/pages/club/member/NewMemberPage.jsx @@ -0,0 +1,92 @@ +import {useNavigate} from "react-router-dom"; +import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; +import {apiAxios} from "../../../utils/Tools.js"; +import {toast} from "react-toastify"; +import {BirthDayField, OptionField, TextField} from "../../../components/MemberCustomFiels.jsx"; +import {ClubSelect} from "../../../components/ClubSelect.jsx"; +import {addPhoto} from "../../admin/member/InformationForm.jsx"; + +export function NewMemberPage() { + const navigate = useNavigate(); + + return <> +

Page membre

+ +
+
+ +
+
+ +} + +function Form() { + const navigate = useNavigate(); + const setLoading = useLoadingSwitcher() + + const handleSubmit = (event) => { + event.preventDefault(); + setLoading(1) + + const formData = new FormData(); + formData.append("id", -1); + 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); + + const send = (formData_) => { + apiAxios.post(`/member/club`, formData_, { + headers: { + 'Accept': '*/*', + 'Content-Type': 'multipart/form-data', + } + }).then(data => { + toast.success('Profile crée avec succès 🎉'); + navigate(`/club/member/${data.data}`) + }).catch(e => { + console.log(e.response) + toast.error('Échec de la création du profile 😕 (code: ' + e.response.status + ')'); + }).finally(() => { + if (setLoading) + setLoading(0) + }) + } + + addPhoto(event, formData, send); + } + + return +
+
Nouveau membre
+
+ + + + + + +
+
+ + +
+
+
+
+ +
+
+
+
+ ; +}