From aba8d8a20291d8f33ada49631641c810f15cd2b4 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Tue, 30 Dec 2025 11:22:24 +0100 Subject: [PATCH] Re-update to 3.30.5 --- .gitea/workflows/deploy_in_prod.yml | 2 +- .gitignore | 1 + pom.xml | 18 ++- .../domain/service/AffiliationService.java | 29 ++--- .../ffsaf/domain/service/MembreService.java | 2 +- .../domain/service/VirusScannerService.java | 28 +++++ .../titionfire/ffsaf/rest/ClubEndpoints.java | 48 ++------ .../ffsaf/rest/MembreAdminEndpoints.java | 27 ++--- .../ffsaf/rest/MembreClubEndpoints.java | 27 ++--- .../rest/from/AffiliationRequestForm.java | 47 ++++---- .../rest/from/AffiliationRequestSaveForm.java | 59 +++++----- .../ffsaf/rest/from/FullClubForm.java | 29 ++--- .../ffsaf/rest/from/FullMemberForm.java | 22 ++-- .../java/fr/titionfire/ffsaf/utils/Utils.java | 110 ++++++++++++------ 14 files changed, 231 insertions(+), 218 deletions(-) create mode 100644 src/main/java/fr/titionfire/ffsaf/domain/service/VirusScannerService.java diff --git a/.gitea/workflows/deploy_in_prod.yml b/.gitea/workflows/deploy_in_prod.yml index 464262f..96ac412 100644 --- a/.gitea/workflows/deploy_in_prod.yml +++ b/.gitea/workflows/deploy_in_prod.yml @@ -20,7 +20,7 @@ jobs: - name: Set up JDK 17 uses: actions/setup-java@v4 with: - java-version: '17.0.12' + java-version: '21' distribution: 'graalvm' cache: 'maven' diff --git a/.gitignore b/.gitignore index ab7fed3..62c8bc8 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,4 @@ nb-configuration.xml /media/ /media-ext/ /sign.jpg +/.gitea/workflows/test.yml diff --git a/pom.xml b/pom.xml index 2c0622e..1aeb740 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ UTF-8 quarkus-bom io.quarkus.platform - 3.16.4 + 3.30.5 true 3.2.3 @@ -56,19 +56,15 @@ quarkus-rest-client-jackson - - io.vertx - vertx-mssql-client - 4.4.1 - io.quarkus quarkus-arc + - io.quarkiverse.tika - quarkus-tika - 2.0.4 + io.quarkiverse.antivirus + quarkus-antivirus + 1.3.0 @@ -96,7 +92,7 @@ org.projectlombok lombok - 1.18.22 + 1.18.42 provided @@ -127,7 +123,7 @@ org.apache.xmlgraphics fop - 2.6 + 2.11 io.quarkus diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java index 4dfd0ec..5dd2283 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java @@ -64,6 +64,9 @@ public class AffiliationService { @Inject LoggerService ls; + @Inject + VirusScannerService scanner; + @RestClient StateIdService stateIdService; @@ -174,15 +177,12 @@ public class AffiliationService { LOGGER.debug("Affiliation Request Created"); LOGGER.debug(form.toString()); - // noinspection ResultOfMethodCallIgnored,ReactiveStreamsUnusedPublisher + // noinspection ReactiveStreamsUnusedPublisher return pre_save(form, true) .chain(model -> Panache.withTransaction(() -> repositoryRequest.persist(model))) .onItem() - .invoke(model -> Uni.createFrom().future(Utils.replacePhoto(model.getId(), form.getLogo(), media, - "aff_request/logo"))) - .onItem() - .invoke(model -> Uni.createFrom().future(Utils.replacePhoto(model.getId(), form.getStatus(), media, - "aff_request/status"))) + .invoke(m -> Utils.uploadFile(scanner, form.getLogo(), m.getId(), media, "aff_request/logo")) + .invoke(m -> Utils.uploadFile(scanner, form.getStatus(), m.getId(), media, "aff_request/status")) .call(model -> reactiveMailer.send( Mail.withText("no-reply@ffsaf.fr", "[NOTIF] FFSAF - Nouvelle demande d'affiliation", @@ -245,11 +245,8 @@ public class AffiliationService { }) .chain(model -> Panache.withTransaction(() -> repositoryRequest.persist(model))) .onItem() - .invoke(model -> Uni.createFrom().future(Utils.replacePhoto(model.getId(), form.getLogo(), media, - "aff_request/logo"))) - .onItem() - .invoke(model -> Uni.createFrom().future(Utils.replacePhoto(model.getId(), form.getStatus(), media, - "aff_request/status"))) + .invoke(m -> Utils.uploadFile(scanner, form.getLogo(), m.getId(), media, "aff_request/logo")) + .invoke(m -> Utils.uploadFile(scanner, form.getStatus(), m.getId(), media, "aff_request/status")) .map(__ -> "Ok"); } @@ -319,13 +316,11 @@ public class AffiliationService { .recoverWithNull() .call(___ -> setMembre(form.new Member(3), club, req.getSaison())))) .onItem() - .invoke(model -> Uni.createFrom() - .future(Utils.replacePhoto(form.getId(), form.getLogo(), media, - "aff_request/logo"))) + .invoke(model -> Utils.uploadFile(scanner, form.getLogo(), form.getId(), media, + "aff_request/logo")) + .invoke(model -> Utils.uploadFile(scanner, form.getStatus(), form.getId(), media, + "aff_request/status")) .onItem() - .invoke(model -> Uni.createFrom() - .future(Utils.replacePhoto(form.getId(), form.getStatus(), media, - "aff_request/status"))) .call(model -> Utils.moveMedia(form.getId(), model.getId(), media, "aff_request/logo", "ppClub")) .call(model -> Utils.moveMedia(form.getId(), model.getId(), media, "aff_request/status", 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 023b543..66ffda8 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java @@ -467,7 +467,7 @@ public class MembreService { .call(membreModel -> licenceRepository.update("club_id = ?1 where membre = ?2 AND saison = ?3", (membreModel.getClub() == null) ? null : membreModel.getClub().getId(), membreModel, Utils.getSaison())) - .call(membreModel -> membre.getPhoto_data().length > 0 ? ls.logAUpdate("Photo", + .call(membreModel -> (membre.getPhoto_data() != null && membre.getPhoto_data().size() > 0) ? ls.logAUpdate("Photo", membreModel) : Uni.createFrom().nullItem()) .map(__ -> "OK"); } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/VirusScannerService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/VirusScannerService.java new file mode 100644 index 0000000..16e9763 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/VirusScannerService.java @@ -0,0 +1,28 @@ +package fr.titionfire.ffsaf.domain.service; + +import io.quarkiverse.antivirus.runtime.Antivirus; +import io.quarkiverse.antivirus.runtime.AntivirusScanResult; +import io.smallrye.mutiny.Uni; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import java.io.InputStream; +import java.util.List; + +@ApplicationScoped +public class VirusScannerService { + + @Inject + Antivirus antivirus; + + public Uni> scanFileReactive(String fileName, InputStream inputStream) { + System.out.println("Starting reactive virus scan for file: " + fileName); + + // Wrap the blocking antivirus scan in a reactive context + // This moves the blocking operation to a worker thread + return Uni.createFrom().item(() -> { + System.out.println("Scanning file on worker thread: " + fileName); + return antivirus.scan(fileName, inputStream); + }).runSubscriptionOn(io.smallrye.mutiny.infrastructure.Infrastructure.getDefaultWorkerPool()); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java index 11ae0da..7128d0c 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java @@ -3,10 +3,10 @@ package fr.titionfire.ffsaf.rest; import fr.titionfire.ffsaf.data.model.ClubModel; import fr.titionfire.ffsaf.domain.service.ClubService; import fr.titionfire.ffsaf.domain.service.PDFService; +import fr.titionfire.ffsaf.domain.service.VirusScannerService; import fr.titionfire.ffsaf.net2.data.SimpleClubModel; import fr.titionfire.ffsaf.rest.data.*; import fr.titionfire.ffsaf.rest.exception.DForbiddenException; -import fr.titionfire.ffsaf.rest.exception.DInternalError; import fr.titionfire.ffsaf.rest.from.FullClubForm; import fr.titionfire.ffsaf.rest.from.PartClubForm; import fr.titionfire.ffsaf.utils.Contact; @@ -47,6 +47,9 @@ public class ClubEndpoints { @Inject SecurityCtx securityCtx; + @Inject + VirusScannerService scannerService; + @ConfigProperty(name = "upload_dir") String media; @@ -121,9 +124,8 @@ public class ClubEndpoints { }) public Uni getById( @Parameter(description = "Identifiant de club") @PathParam("id") long id) { - return clubService.getById(id).onItem().invoke(checkPerm).map(SimpleClub::fromModel).invoke(m -> { - m.setContactMap(Contact.toSite()); - }); + return clubService.getById(id).onItem().invoke(checkPerm).map(SimpleClub::fromModel) + .invoke(m -> m.setContactMap(Contact.toSite())); } @PUT @@ -145,25 +147,9 @@ public class ClubEndpoints { return clubService.update(id, input) .invoke(Unchecked.consumer(out -> { if (!out.equals("OK")) throw new InternalError("Fail to update data: " + out); - })).chain(() -> { - if (input.getLogo().length > 0) - return Uni.createFrom().future(Utils.replacePhoto(id, input.getLogo(), media, "ppClub" - )).invoke(Unchecked.consumer(out -> { - if (!out.equals("OK")) - throw new DInternalError("Impossible de reconnaitre le fichier: " + out); - })); // TODO log - else - return Uni.createFrom().nullItem(); - }).chain(() -> { - if (input.getStatus().length > 0) - return Uni.createFrom().future(Utils.replacePhoto(id, input.getStatus(), media, "clubStatus" - )).invoke(Unchecked.consumer(out -> { - if (!out.equals("OK")) - throw new DInternalError("Impossible de reconnaitre le fichier: " + out); - })); // TODO log - else - return Uni.createFrom().nullItem(); - }); + })) + .chain(() -> Utils.uploadFile(scannerService, input.getLogo(), id, media, "ppClub")) + .chain(() -> Utils.uploadFile(scannerService, input.getStatus(), id, media, "clubStatus")); } @PUT @@ -181,19 +167,9 @@ public class ClubEndpoints { return clubService.add(input) .invoke(Unchecked.consumer(id -> { if (id == null) throw new InternalError("Fail to create club data"); - })).call(id -> { - if (input.getLogo().length > 0) - return Uni.createFrom().future(Utils.replacePhoto(id, input.getLogo(), media, "ppClub" - )); // TODO log - else - return Uni.createFrom().nullItem(); - }).call(id -> { - if (input.getStatus().length > 0) - return Uni.createFrom().future(Utils.replacePhoto(id, input.getStatus(), media, "clubStatus" - )); // TODO log - else - return Uni.createFrom().nullItem(); - }); + })) + .call(id -> Utils.uploadFile(scannerService, input.getLogo(), id, media, "ppClub")) + .call(id -> Utils.uploadFile(scannerService, input.getStatus(), id, media, "clubStatus")); } @DELETE diff --git a/src/main/java/fr/titionfire/ffsaf/rest/MembreAdminEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/MembreAdminEndpoints.java index aa9a62b..b7898df 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/MembreAdminEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/MembreAdminEndpoints.java @@ -2,6 +2,7 @@ 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.VirusScannerService; import fr.titionfire.ffsaf.rest.data.SimpleMembre; import fr.titionfire.ffsaf.rest.exception.DForbiddenException; import fr.titionfire.ffsaf.rest.exception.DInternalError; @@ -21,6 +22,7 @@ import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.jboss.logging.Logger; import java.util.List; import java.util.function.Consumer; @@ -29,10 +31,14 @@ import java.util.function.Consumer; @Path("api/member") @RolesAllowed({"federation_admin"}) public class MembreAdminEndpoints { + private static final Logger LOGGER = Logger.getLogger(MembreAdminEndpoints.class); @Inject MembreService membreService; + @Inject + VirusScannerService scannerService; + @ConfigProperty(name = "upload_dir") String media; @@ -97,16 +103,8 @@ public class MembreAdminEndpoints { .invoke(Unchecked.consumer(out -> { if (!out.equals("OK")) throw new DInternalError("Impossible de reconnaitre le fichier: " + out); - })).chain(() -> { - if (input.getPhoto_data().length > 0) - return Uni.createFrom().future(Utils.replacePhoto(id, input.getPhoto_data(), media, "ppMembre" - )).invoke(Unchecked.consumer(out -> { - if (!out.equals("OK")) - throw new DInternalError("Impossible de reconnaitre le fichier: " + out); - })); - else - return Uni.createFrom().nullItem(); - }); + })) + .call(__ -> Utils.uploadFile(scannerService, input.getPhoto_data(), id, media, "ppMembre")); } @POST @@ -123,13 +121,8 @@ public class MembreAdminEndpoints { 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(); - }); + })) + .call(id -> Utils.uploadFile(scannerService, input.getPhoto_data(), id, media, "ppMembre")); } @DELETE diff --git a/src/main/java/fr/titionfire/ffsaf/rest/MembreClubEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/MembreClubEndpoints.java index 71160f5..cbba68b 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/MembreClubEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/MembreClubEndpoints.java @@ -1,9 +1,9 @@ package fr.titionfire.ffsaf.rest; import fr.titionfire.ffsaf.domain.service.MembreService; +import fr.titionfire.ffsaf.domain.service.VirusScannerService; import fr.titionfire.ffsaf.rest.data.SimpleMembre; import fr.titionfire.ffsaf.rest.data.SimpleMembreInOutData; -import fr.titionfire.ffsaf.rest.exception.DInternalError; import fr.titionfire.ffsaf.rest.from.FullMemberForm; import fr.titionfire.ffsaf.utils.PageResult; import fr.titionfire.ffsaf.utils.SecurityCtx; @@ -37,6 +37,9 @@ public class MembreClubEndpoints { @Inject SecurityCtx securityCtx; + @Inject + VirusScannerService scannerService; + @GET @Path("/find/club") @Produces(MediaType.APPLICATION_JSON) @@ -59,7 +62,8 @@ public class MembreClubEndpoints { limit = 50; if (page == null || page < 1) page = 1; - return membreService.search(limit, page - 1, search, licenceRequest, payment, order, categorie, archive, securityCtx.getSubject()); + return membreService.search(limit, page - 1, search, licenceRequest, payment, order, categorie, archive, + securityCtx.getSubject()); } @GET @@ -107,16 +111,7 @@ public class MembreClubEndpoints { return membreService.update(id, input, securityCtx) .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(Utils.replacePhoto(id, input.getPhoto_data(), media, "ppMembre" - )).invoke(Unchecked.consumer(out -> { - if (!out.equals("OK")) - throw new DInternalError("Impossible de reconnaitre le fichier: " + out); - })); - else - return Uni.createFrom().nullItem(); - }); + })).chain(() -> Utils.uploadFile(scannerService, input.getPhoto_data(), id, media, "ppMembre")); } @POST @@ -134,13 +129,7 @@ public class MembreClubEndpoints { return membreService.add(input, securityCtx.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(); - }); + })).call(id -> Utils.uploadFile(scannerService, input.getPhoto_data(), id, media, "ppMembre")); } @DELETE 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 ae657de..0ab495c 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestForm.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestForm.java @@ -9,6 +9,7 @@ import lombok.ToString; import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.jboss.resteasy.reactive.PartType; +import org.jboss.resteasy.reactive.multipart.FileUpload; @Getter @ToString(exclude = {"status", "logo"}) @@ -17,85 +18,85 @@ public class AffiliationRequestForm { @FormParam("id") private Long id = null; - @Schema(description = "Le nom de l'association.", example = "Association sportive", required = true) + @Schema(description = "Le nom de l'association.", examples = "Association sportive", required = true) @FormParam("name") private String name = null; - @Schema(description = "Le numéro SIRET/RNA de l'association.", example = "12345678901234", required = true) + @Schema(description = "Le numéro SIRET/RNA de l'association.", examples = "12345678901234", required = true) @FormParam("state_id") private String state_id = null; - @Schema(description = "L'adresse de l'association.", example = "1 rue de l'exemple, 75000 Paris", required = true) + @Schema(description = "L'adresse de l'association.", examples = "1 rue de l'exemple, 75000 Paris", required = true) @FormParam("adresse") private String adresse = null; - @Schema(description = "Email de contact de l'association", example = "test@test.fr") + @Schema(description = "Email de contact de l'association", examples = "test@test.fr") @FormParam("contact") private String contact = null; - @Schema(description = "La saison de l'affiliation.", example = "2025", required = true) + @Schema(description = "La saison de l'affiliation.", examples = "2025", required = true) @FormParam("saison") private int saison = -1; - @Schema(description = "Le statut de l'association.", type = SchemaType.ARRAY, implementation = byte.class) + @Schema(description = "Le statut de l'association.", type = SchemaType.ARRAY) @FormParam("status") @PartType(MediaType.APPLICATION_OCTET_STREAM) - private byte[] status = new byte[0]; + private FileUpload status = null; - @Schema(description = "Le logo de l'association.", type = SchemaType.ARRAY, implementation = byte.class) + @Schema(description = "Le logo de l'association.", type = SchemaType.ARRAY) @FormParam("logo") @PartType(MediaType.APPLICATION_OCTET_STREAM) - private byte[] logo = new byte[0]; + private FileUpload logo = null; - @Schema(description = "Le nom du premier membre de l'association.", example = "Doe", required = true) + @Schema(description = "Le nom du premier membre de l'association.", examples = "Doe", required = true) @FormParam("m1_nom") private String m1_lname = null; - @Schema(description = "Le prénom du premier membre de l'association.", example = "John", required = true) + @Schema(description = "Le prénom du premier membre de l'association.", examples = "John", required = true) @FormParam("m1_prenom") private String m1_fname = null; - @Schema(description = "L'adresse e-mail du premier membre de l'association.", example = "john.doe@test.com", required = true) + @Schema(description = "L'adresse e-mail du premier membre de l'association.", examples = "john.doe@test.com", required = true) @FormParam("m1_mail") private String m1_email = null; - @Schema(description = "Le numéro de licence du premier membre de l'association. (null si non licencié)", example = "12345") + @Schema(description = "Le numéro de licence du premier membre de l'association. (null si non licencié)", examples = "12345") @FormParam("m1_licence") private String m1_lincence = null; - @Schema(description = "Le rôle du premier membre de l'association. (doit être PRESIDENT)", example = "PRESIDENT", required = true) + @Schema(description = "Le rôle du premier membre de l'association. (doit être PRESIDENT)", examples = "PRESIDENT", required = true) @FormParam("m1_role") private RoleAsso m1_role = null; - @Schema(description = "Le nom du deuxième membre de l'association.", example = "Xavier", required = true) + @Schema(description = "Le nom du deuxième membre de l'association.", examples = "Xavier", required = true) @FormParam("m2_nom") private String m2_lname = null; - @Schema(description = "Le prénom du deuxième membre de l'association.", example = "Login", required = true) + @Schema(description = "Le prénom du deuxième membre de l'association.", examples = "Login", required = true) @FormParam("m2_prenom") private String m2_fname = null; - @Schema(description = "L'adresse e-mail du deuxième membre de l'association.", example = "xavier.login@test.com", required = true) + @Schema(description = "L'adresse e-mail du deuxième membre de l'association.", examples = "xavier.login@test.com", required = true) @FormParam("m2_mail") private String m2_email = null; - @Schema(description = "Le numéro de licence du deuxième membre de l'association. (null si non licencié)", example = "04242") + @Schema(description = "Le numéro de licence du deuxième membre de l'association. (null si non licencié)", examples = "04242") @FormParam("m2_licence") private String m2_lincence = null; - @Schema(description = "Le rôle du deuxième membre de l'association.", example = "SECRETAIRE", required = true) + @Schema(description = "Le rôle du deuxième membre de l'association.", examples = "SECRETAIRE", required = true) @FormParam("m2_role") private RoleAsso m2_role = null; - @Schema(description = "Le nom du troisième membre de l'association.", example = "Doe2", required = true) + @Schema(description = "Le nom du troisième membre de l'association.", examples = "Doe2", required = true) @FormParam("m3_nom") private String m3_lname = null; - @Schema(description = "Le prénom du troisième membre de l'association.", example = "John2", required = true) + @Schema(description = "Le prénom du troisième membre de l'association.", examples = "John2", required = true) @FormParam("m3_prenom") private String m3_fname = null; - @Schema(description = "L'adresse e-mail du troisième membre de l'association.", example = "john.doe22@test.com", required = true) + @Schema(description = "L'adresse e-mail du troisième membre de l'association.", examples = "john.doe22@test.com", required = true) @FormParam("m3_mail") private String m3_email = null; @@ -103,7 +104,7 @@ public class AffiliationRequestForm { @FormParam("m3_licence") private String m3_lincence = null; - @Schema(description = "Le rôle du troisième membre de l'association.", example = "MEMBREBUREAU", required = true) + @Schema(description = "Le rôle du troisième membre de l'association.", examples = "MEMBREBUREAU", required = true) @FormParam("m3_role") private RoleAsso m3_role = null; diff --git a/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestSaveForm.java b/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestSaveForm.java index a1f66aa..1213dc1 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestSaveForm.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestSaveForm.java @@ -6,123 +6,124 @@ import jakarta.ws.rs.core.MediaType; import lombok.Getter; import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.jboss.resteasy.reactive.PartType; +import org.jboss.resteasy.reactive.multipart.FileUpload; @Getter public class AffiliationRequestSaveForm { - @Schema(description = "L'identifiant de l'affiliation.", example = "1", required = true) + @Schema(description = "L'identifiant de l'affiliation.", examples = "1", required = true) @FormParam("id") private Long id = null; - @Schema(description = "Le nom de l'association.", example = "Association sportive", required = true) + @Schema(description = "Le nom de l'association.", examples = "Association sportive", required = true) @FormParam("name") private String name = null; - @Schema(description = "Le numéro SIRET ou RNA de l'association.", example = "12345678901234", required = true) + @Schema(description = "Le numéro SIRET ou RNA de l'association.", examples = "12345678901234", required = true) @FormParam("state_id") private String state_id = null; - @Schema(description = "L'adresse de l'association.", example = "1 rue de l'exemple, 75000 Paris", required = true) + @Schema(description = "L'adresse de l'association.", examples = "1 rue de l'exemple, 75000 Paris", required = true) @FormParam("address") private String address = null; - @Schema(description = "Email de contact de l'association", example = "test@test.fr") + @Schema(description = "Email de contact de l'association", examples = "test@test.fr") @FormParam("contact") private String contact = null; @Schema(description = "Le statut de l'association.") @FormParam("status") @PartType(MediaType.APPLICATION_OCTET_STREAM) - private byte[] status = new byte[0]; + private FileUpload status = null; @Schema(description = "Le logo de l'association.") @FormParam("logo") @PartType(MediaType.APPLICATION_OCTET_STREAM) - private byte[] logo = new byte[0]; + private FileUpload logo = null; - @Schema(description = "Mode utiliser pour la sauvegarde du membre 1 (0 = licence mode, 2 = nom, prénom)", example = "0", required = true) + @Schema(description = "Mode utiliser pour la sauvegarde du membre 1 (0 = licence mode, 2 = nom, prénom)", examples = "0", required = true) @FormParam("m1_mode") private Integer m1_mode = null; - @Schema(description = "Le rôle du premier membre de l'association.", example = "PRÉSIDENT", required = true) + @Schema(description = "Le rôle du premier membre de l'association.", examples = "PRÉSIDENT", required = true) @FormParam("m1_role") private RoleAsso m1_role = null; - @Schema(description = "Le numéro de licence du premier membre de l'association. (null si non licencié)", example = "1234567", required = true) + @Schema(description = "Le numéro de licence du premier membre de l'association. (null si non licencié)", examples = "1234567", required = true) @FormParam("m1_licence") private String m1_lincence = null; - @Schema(description = "Le nom du premier membre de l'association.", example = "Dupont", required = true) + @Schema(description = "Le nom du premier membre de l'association.", examples = "Dupont", required = true) @FormParam("m1_lname") private String m1_lname = null; - @Schema(description = "Le prénom du premier membre de l'association.", example = "Jean", required = true) + @Schema(description = "Le prénom du premier membre de l'association.", examples = "Jean", required = true) @FormParam("m1_fname") private String m1_fname = null; - @Schema(description = "L'adresse e-mail du premier membre de l'association.", example = "jean.dupont@example.com", required = true) + @Schema(description = "L'adresse e-mail du premier membre de l'association.", examples = "jean.dupont@examples.com", required = true) @FormParam("m1_email") private String m1_email = null; @Schema(name = "keep_email", - description = "Conserver l'email de la base de donner (1 = conserve, 0 = replacer par 'm1_email')", example = "1", required = true) + description = "Conserver l'email de la base de donner (1 = conserve, 0 = replacer par 'm1_email')", examples = "1", required = true) @FormParam("m1_email_mode") private Integer m1_email_mode = null; - @Schema(description = "Mode utiliser pour la sauvegarde du membre 2 (0 = licence mode, 2 = nom, prénom)", example = "0", required = true) + @Schema(description = "Mode utiliser pour la sauvegarde du membre 2 (0 = licence mode, 2 = nom, prénom)", examples = "0", required = true) @FormParam("m2_mode") private Integer m2_mode = null; - @Schema(description = "Le rôle du deuxième membre de l'association.", example = "TRÉSORIER", required = true) + @Schema(description = "Le rôle du deuxième membre de l'association.", examples = "TRÉSORIER", required = true) @FormParam("m2_role") private RoleAsso m2_role = null; - @Schema(description = "Le numéro de licence du deuxième membre de l'association. (null si non licencié)", example = "2345678", required = true) + @Schema(description = "Le numéro de licence du deuxième membre de l'association. (null si non licencié)", examples = "2345678", required = true) @FormParam("m2_licence") private String m2_lincence = null; - @Schema(description = "Le nom du deuxième membre de l'association.", example = "Durand", required = true) + @Schema(description = "Le nom du deuxième membre de l'association.", examples = "Durand", required = true) @FormParam("m2_lname") private String m2_lname = null; - @Schema(description = "Le prénom du deuxième membre de l'association.", example = "Paul", required = true) + @Schema(description = "Le prénom du deuxième membre de l'association.", examples = "Paul", required = true) @FormParam("m2_fname") private String m2_fname = null; - @Schema(description = "L'adresse e-mail du deuxième membre de l'association.", example = "paul.durand@example.com", required = true) + @Schema(description = "L'adresse e-mail du deuxième membre de l'association.", examples = "paul.durand@examples.com", required = true) @FormParam("m2_email") private String m2_email = null; @Schema(name = "keep_email", - description = "Conserver l'email de la base de donner (1 = conserve, 0 = replacer par 'm2_email')", example = "1", required = true) + description = "Conserver l'email de la base de donner (1 = conserve, 0 = replacer par 'm2_email')", examples = "1", required = true) @FormParam("m2_email_mode") private Integer m2_email_mode = null; - @Schema(description = "Mode utiliser pour la sauvegarde du membre 3 (0 = licence mode, 2 = nom, prénom)", example = "0", required = true) + @Schema(description = "Mode utiliser pour la sauvegarde du membre 3 (0 = licence mode, 2 = nom, prénom)", examples = "0", required = true) @FormParam("m3_mode") private Integer m3_mode = null; - @Schema(description = "Le rôle du troisième membre de l'association.", example = "SECRÉTAIRE", required = true) + @Schema(description = "Le rôle du troisième membre de l'association.", examples = "SECRÉTAIRE", required = true) @FormParam("m3_role") private RoleAsso m3_role = null; - @Schema(description = "Le numéro de licence du troisième membre de l'association. (null si non licencié)", example = "3456789", required = true) + @Schema(description = "Le numéro de licence du troisième membre de l'association. (null si non licencié)", examples = "3456789", required = true) @FormParam("m3_licence") private String m3_lincence = null; - @Schema(description = "Le nom du troisième membre de l'association.", example = "Martin", required = true) + @Schema(description = "Le nom du troisième membre de l'association.", examples = "Martin", required = true) @FormParam("m3_lname") private String m3_lname = null; - @Schema(description = "Le prénom du troisième membre de l'association.", example = "Pierre", required = true) + @Schema(description = "Le prénom du troisième membre de l'association.", examples = "Pierre", required = true) @FormParam("m3_fname") private String m3_fname = null; - @Schema(description = "L'adresse e-mail du troisième membre de l'association.", example = "pierre.martin@example.com", required = true) + @Schema(description = "L'adresse e-mail du troisième membre de l'association.", examples = "pierre.martin@examples.com", required = true) @FormParam("m3_email") private String m3_email = null; @Schema(name = "keep_email", - description = "Conserver l'email de la base de donner (1 = conserve, 0 = replacer par 'm3_email')", example = "1", required = true) + description = "Conserver l'email de la base de donner (1 = conserve, 0 = replacer par 'm3_email')", examples = "1", required = true) @FormParam("m3_email_mode") private Integer m3_email_mode = null; @@ -174,8 +175,6 @@ public class AffiliationRequestSaveForm { ", state_id=" + state_id + ", address='" + address + '\'' + ", contact='" + contact + '\'' + - ", status_len=" + status.length + - ", logo_len=" + logo.length + ", m1_mode=" + m1_mode + ", m1_role=" + m1_role + ", m1_lincence='" + m1_lincence + '\'' + diff --git a/src/main/java/fr/titionfire/ffsaf/rest/from/FullClubForm.java b/src/main/java/fr/titionfire/ffsaf/rest/from/FullClubForm.java index 6a23b52..3d29c7f 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/from/FullClubForm.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/from/FullClubForm.java @@ -7,57 +7,58 @@ import lombok.ToString; import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.jboss.resteasy.reactive.PartType; +import org.jboss.resteasy.reactive.multipart.FileUpload; @ToString @Getter public class FullClubForm { @FormParam("id") - @Schema(description = "Identifiant du club", example = "1", required = true) + @Schema(description = "Identifiant du club", examples = "1", required = true) private String id = null; @FormParam("name") - @Schema(description = "Nom du club", example = "Association sportive", required = true) + @Schema(description = "Nom du club", examples = "Association sportive", required = true) private String name = null; @FormParam("country") - @Schema(description = "Pays du club", example = "FR", required = true) + @Schema(description = "Pays du club", examples = "FR", required = true) private String country = null; @FormParam("contact") - @Schema(description = "Les contacts du club", example = "{\"SITE\": \"www.test.com\", \"COURRIEL\": \"test@test.com\"}", required = true) + @Schema(description = "Les contacts du club", examples = "{\"SITE\": \"www.test.com\", \"COURRIEL\": \"test@test.com\"}", required = true) private String contact = null; @FormParam("training_location") - @Schema(description = "Liste des lieux d'entraînement", example = "[{\"text\":\"addr 1\",\"lng\":2.24654,\"lat\":52.4868658},{\"text\":\"addr 2\",\"lng\":2.88654,\"lat\":52.7865456}]", required = true) + @Schema(description = "Liste des lieux d'entraînement", examples = "[{\"text\":\"addr 1\",\"lng\":2.24654,\"lat\":52.4868658},{\"text\":\"addr 2\",\"lng\":2.88654,\"lat\":52.7865456}]", required = true) private String training_location = null; @FormParam("training_day_time") - @Schema(description = "Liste des jours et horaires d'entraînement (jours 0-6, 0=>lundi) (temps en minute depuis 00:00, 122=>2h02)", example = "[{\"day\":0,\"time_start\":164,\"time_end\":240},{\"day\":3,\"time_start\":124,\"time_end\":250}]", required = true) + @Schema(description = "Liste des jours et horaires d'entraînement (jours 0-6, 0=>lundi) (temps en minute depuis 00:00, 122=>2h02)", examples = "[{\"day\":0,\"time_start\":164,\"time_end\":240},{\"day\":3,\"time_start\":124,\"time_end\":250}]", required = true) private String training_day_time = null; @FormParam("contact_intern") - @Schema(description = "Contact interne du club", example = "john.doe@test.com") + @Schema(description = "Contact interne du club", examples = "john.doe@test.com") private String contact_intern = null; @FormParam("address") - @Schema(description = "Adresse postale du club", example = "1 rue de l'exemple, 75000 Paris", required = true) + @Schema(description = "Adresse postale du club", examples = "1 rue de l'exemple, 75000 Paris", required = true) private String address = null; @FormParam("state_id") - @Schema(description = "Numéro SIRET ou RNA du club", example = "12345678901234", required = true) + @Schema(description = "Numéro SIRET ou RNA du club", examples = "12345678901234", required = true) private String state_id = null; @FormParam("international") - @Schema(description = "Club international", example = "false", required = true) + @Schema(description = "Club international", examples = "false", required = true) private boolean international = false; @FormParam("status") @PartType(MediaType.APPLICATION_OCTET_STREAM) - @Schema(description = "Le statut de l'association.", type = SchemaType.ARRAY, implementation = byte.class) - private byte[] status = new byte[0]; + @Schema(description = "Le statut de l'association.", type = SchemaType.ARRAY) + private FileUpload status = null; @FormParam("logo") @PartType(MediaType.APPLICATION_OCTET_STREAM) - @Schema(description = "Le logo de l'association.", type = SchemaType.ARRAY, implementation = byte.class) - private byte[] logo = new byte[0]; + @Schema(description = "Le logo de l'association.", type = SchemaType.ARRAY) + private FileUpload logo = null; } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/from/FullMemberForm.java b/src/main/java/fr/titionfire/ffsaf/rest/from/FullMemberForm.java index 5f67731..e63add1 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/from/FullMemberForm.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/from/FullMemberForm.java @@ -8,32 +8,33 @@ import jakarta.ws.rs.core.MediaType; import lombok.Getter; import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.jboss.resteasy.reactive.PartType; +import org.jboss.resteasy.reactive.multipart.FileUpload; import java.util.Date; @Getter public class FullMemberForm { - @Schema(description = "L'identifiant du membre.", example = "1") + @Schema(description = "L'identifiant du membre.", examples = "1") @FormParam("id") private String id = null; - @Schema(description = "Le nom du membre.", example = "Dupont") + @Schema(description = "Le nom du membre.", examples = "Dupont") @FormParam("lname") private String lname = null; - @Schema(description = "Le prénom du membre.", example = "Jean") + @Schema(description = "Le prénom du membre.", examples = "Jean") @FormParam("fname") private String fname = null; - @Schema(description = "L'identifiant du club du membre.", example = "1") + @Schema(description = "L'identifiant du club du membre.", examples = "1") @FormParam("club") private Long club = null; - @Schema(description = "Le genre du membre.", example = "H") + @Schema(description = "Le genre du membre.", examples = "H") @FormParam("genre") private Genre genre; - @Schema(description = "Le pays du membre.", example = "FR") + @Schema(description = "Le pays du membre.", examples = "FR") @FormParam("country") private String country; @@ -41,22 +42,22 @@ public class FullMemberForm { @FormParam("birth_date") private Date birth_date = null; - @Schema(description = "L'adresse e-mail du membre.", example = "jean.dupont@example.com") + @Schema(description = "L'adresse e-mail du membre.", examples = "jean.dupont@example.com") @FormParam("email") private String email; - @Schema(description = "Le rôle du membre dans l'association.", example = "MEMBRE") + @Schema(description = "Le rôle du membre dans l'association.", examples = "MEMBRE") @FormParam("role") private RoleAsso role; - @Schema(description = "Le grade d'arbitrage du membre.", example = "ASSESSEUR") + @Schema(description = "Le grade d'arbitrage du membre.", examples = "ASSESSEUR") @FormParam("grade_arbitrage") private GradeArbitrage grade_arbitrage = GradeArbitrage.NA; @Schema(description = "La photo du membre.") @FormParam("photo_data") @PartType(MediaType.APPLICATION_OCTET_STREAM) - private byte[] photo_data = new byte[0]; + private FileUpload photo_data = null; @Override public String toString() { @@ -70,7 +71,6 @@ public class FullMemberForm { ", email='" + email + '\'' + ", role=" + role + ", grade_arbitrage=" + grade_arbitrage + - ", url_photo=" + photo_data.length + '}'; } } diff --git a/src/main/java/fr/titionfire/ffsaf/utils/Utils.java b/src/main/java/fr/titionfire/ffsaf/utils/Utils.java index 62e720b..e068822 100644 --- a/src/main/java/fr/titionfire/ffsaf/utils/Utils.java +++ b/src/main/java/fr/titionfire/ffsaf/utils/Utils.java @@ -2,13 +2,19 @@ package fr.titionfire.ffsaf.utils; import fr.titionfire.ffsaf.data.model.CompetitionGuestModel; import fr.titionfire.ffsaf.data.model.MembreModel; +import fr.titionfire.ffsaf.domain.service.VirusScannerService; +import fr.titionfire.ffsaf.rest.exception.DBadRequestException; +import fr.titionfire.ffsaf.rest.exception.DInternalError; +import fr.titionfire.ffsaf.rest.exception.DetailException; +import io.quarkiverse.antivirus.runtime.AntivirusScanResult; import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.unchecked.Unchecked; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import jodd.net.MimeTypes; -import org.apache.tika.Tika; import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.multipart.FileUpload; import java.io.*; import java.net.URISyntaxException; @@ -183,44 +189,72 @@ public class Utils { }); } - public static Future replacePhoto(long id, byte[] input, String media, String dir) { - return CompletableFuture.supplyAsync(() -> { - if (input == null || input.length == 0) - return "OK"; + public static Uni uploadFile(VirusScannerService ss, FileUpload file, long id, String media, String dir) { + if (file == null || file.size() == 0) + return Uni.createFrom().item("Ok"); - try (InputStream is = new BufferedInputStream(new ByteArrayInputStream(input))) { - String mimeType; - try { - Tika tika = new Tika(); - mimeType = tika.detect(is);// Magic.getMagicMatch(input, false).getMimeType(); - } catch (IOException e) { - 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(id + "."); - File[] files = dirFile.listFiles(filter); - if (files != null) { - for (File file : files) { - //noinspection ResultOfMethodCallIgnored - file.delete(); + LOGGER.infof("Received file upload request for: %s (size: %d bytes)", file.fileName(), file.size()); + return Uni.createFrom().item(() -> { + try { + // Read the entire file into memory for virus scanning + // ByteArrayInputStream fully supports mark/reset operations required by ClamAV + // This ensures we can scan the file content before any filesystem storage + InputStream fileStream = java.nio.file.Files.newInputStream(file.uploadedFile()); + byte[] fileBytes = fileStream.readAllBytes(); + fileStream.close(); // Close the file stream immediately + return new ByteArrayInputStream(fileBytes); + } catch (IOException e) { + throw new RuntimeException("Failed to read uploaded file: " + e.getMessage(), e); + } + }).runSubscriptionOn(io.smallrye.mutiny.infrastructure.Infrastructure.getDefaultWorkerPool()).onItem() + .transformToUni(inputStream -> { + // Perform virus scanning reactively using the input stream + return ss.scanFileReactive(file.fileName(), inputStream).onItem().invoke(() -> { + try { + inputStream.close(); + } catch (IOException e) { + LOGGER.warn("Warning: Failed to close input stream: " + e.getMessage()); + } + }); + }) + .onItem().transformToUni(scanResults -> Uni.createFrom().item(Unchecked.supplier(() -> { + // Check if any scanner found a threat + for (AntivirusScanResult result : scanResults) { + if (result.getStatus() != Response.Status.OK.getStatusCode()) { + LOGGER.warnf("THREAT DETECTED in %s: %s", file.fileName(), result.getMessage()); + throw new DetailException(Response.Status.fromStatusCode(result.getStatus()), + "THREAT_DETECTED on File " + file.fileName() + " is infected: " + result.getMessage()); + } } - } - String extension = "." + detectedExtensions[0]; - Files.write(new File(dirFile, id + extension).toPath(), input); - return "OK"; - } catch (IOException e) { - return e.getMessage(); - } - }); + // File is clean - now we can safely process it + LOGGER.infof("File is clean: %s (size=%d) (contentType=%s)", file.fileName(), file.size(), + file.contentType()); + + String[] detectedExtensions = MimeTypes.findExtensionsByMimeTypes(file.contentType(), false); + if (detectedExtensions.length == 0) + throw new DBadRequestException( + "Fail to detect file extension for MIME type " + file.contentType()); + + File dirFile = new File(media, dir); + if (!dirFile.exists()) + if (!dirFile.mkdirs()) + throw new DInternalError("Fail to create directory " + dir); + + FilenameFilter filter = (directory, filename) -> filename.startsWith(id + "."); + File[] files = dirFile.listFiles(filter); + if (files != null) { + for (File f : files) { + //noinspection ResultOfMethodCallIgnored + f.delete(); + } + } + + File f = file.filePath().toFile(); + //noinspection ResultOfMethodCallIgnored + f.renameTo(new File(dirFile, id + "." + detectedExtensions[0])); + return "ok"; + }))); } public static Uni getMediaFile(long id, String media, String dirname, @@ -368,8 +402,8 @@ public class Utils { return result.toString().trim(); } - public static String getFullName(Object ...models) { - for (Object model : models){ + public static String getFullName(Object... models) { + for (Object model : models) { if (model == null) continue; if (model instanceof MembreModel membreModel) -- 2.49.0