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)