diff --git a/.gitea/workflows/deploy_in_prod.yml b/.gitea/workflows/deploy_in_prod.yml new file mode 100644 index 0000000..7367f7c --- /dev/null +++ b/.gitea/workflows/deploy_in_prod.yml @@ -0,0 +1,41 @@ +name: Deploy Production Server + +# Only run the workflow when a PR is merged on main and closed +on: + pull_request: + types: + - closed + branches: + - 'master' + +# Here we check that the PR was correctly merged to main +jobs: + if_merged: + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'graalvm' + cache: 'maven' + + - name: Build site + run: | + cp vite.env src/main/webapp/.env + cd src/main/webapp + npm install + npm run build + cd ../../.. + rm -rf src/main/resources/META-INF/resources + mkdir -p src/main/resources/META-INF/ + mv dist src/main/resources/META-INF/resources + + - name: Build application + run: | + cp ../vite.env src/main/webapp/.env + chmod 740 mvnw + ./mvnw package -Pnative -DskipTests \ No newline at end of file diff --git a/pom.xml b/pom.xml index 6c3aafc..609c526 100644 --- a/pom.xml +++ b/pom.xml @@ -109,6 +109,32 @@ io.quarkus quarkus-websockets + + + net.sf.jmimemagic + jmimemagic + 0.1.3 + + + + io.quarkus + quarkus-smallrye-openapi + + + io.quarkus + quarkus-swagger-ui + + + + io.quarkus + quarkus-cache + + + + com.github.librepdf + openpdf + 2.0.3 + diff --git a/src/main/java/fr/titionfire/ExampleResource.java b/src/main/java/fr/titionfire/ExampleResource.java index 617736e..cc432e7 100644 --- a/src/main/java/fr/titionfire/ExampleResource.java +++ b/src/main/java/fr/titionfire/ExampleResource.java @@ -2,6 +2,7 @@ package fr.titionfire; import io.quarkus.oidc.IdToken; import io.quarkus.oidc.RefreshToken; +import io.quarkus.oidc.UserInfo; import io.quarkus.security.identity.SecurityIdentity; import jakarta.inject.Inject; import jakarta.ws.rs.GET; @@ -30,6 +31,9 @@ public class ExampleResource { @IdToken JsonWebToken idToken; + @Inject + UserInfo userInfo; + /** * Injection point for the Access Token issued by the OpenID Connect Provider */ @@ -59,7 +63,8 @@ public class ExampleResource { .append("") .append("").append("").append("").toString(); diff --git a/src/main/java/fr/titionfire/PingPage.java b/src/main/java/fr/titionfire/PingPage.java new file mode 100644 index 0000000..2acd198 --- /dev/null +++ b/src/main/java/fr/titionfire/PingPage.java @@ -0,0 +1,27 @@ +package fr.titionfire; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +@Tag(name = "Ping API", description = "API pour tester la connectivité") +@Path("/api") +public class PingPage { + + @Operation(summary = "Renvoie un message de réussite", description = "Cette méthode renvoie un message de réussite si la connexion est établie avec succès.") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Réussite") + }) + @GET + @Produces(MediaType.TEXT_PLAIN) + public Response get() { + return Response.ok().build(); + } + +} diff --git a/src/main/java/fr/titionfire/SomePage.java b/src/main/java/fr/titionfire/SomePage.java index 08bd7d1..5bdb78f 100644 --- a/src/main/java/fr/titionfire/SomePage.java +++ b/src/main/java/fr/titionfire/SomePage.java @@ -1,17 +1,15 @@ package fr.titionfire; import io.quarkus.qute.Template; -import io.quarkus.qute.TemplateInstance; - +import io.smallrye.mutiny.Uni; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import static java.util.Objects.requireNonNull; -@Path("/some-page") +@Path("api/some-page") public class SomePage { private final Template page; @@ -22,8 +20,11 @@ public class SomePage { @GET @Produces(MediaType.TEXT_HTML) - public TemplateInstance get(@QueryParam("name") String name) { - return page.data("name", name); + public Uni get() { + return Uni.createFrom() + .completionStage(() -> page + .data("name", "test") + .renderAsync()); } } diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/AffiliationModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/AffiliationModel.java index d3d0dfd..96688bb 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/AffiliationModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/AffiliationModel.java @@ -3,6 +3,7 @@ package fr.titionfire.ffsaf.data.model; import io.quarkus.runtime.annotations.RegisterForReflection; import jakarta.persistence.*; import lombok.*; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @Getter @Setter @@ -16,11 +17,13 @@ import lombok.*; public class AffiliationModel { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Identifiant de l'affiliation", example = "42") Long id; @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "club", referencedColumnName = "id") ClubModel club; + @Schema(description = "Saison de l'affiliation", example = "2021") int saison; } diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/AffiliationRequestModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/AffiliationRequestModel.java index fc09800..9e4ce1b 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/AffiliationRequestModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/AffiliationRequestModel.java @@ -1,5 +1,6 @@ package fr.titionfire.ffsaf.data.model; +import fr.titionfire.ffsaf.utils.RoleAsso; import io.quarkus.runtime.annotations.RegisterForReflection; import jakarta.persistence.*; import lombok.*; @@ -20,24 +21,27 @@ public class AffiliationRequestModel { Long id; String name; - String siren; + long siret; String RNA; String address; - String president_lname; - String president_fname; - String president_email; - int president_lincence; + String m1_lname; + String m1_fname; + String m1_email; + int m1_lincence; + RoleAsso m1_role; - String tresorier_lname; - String tresorier_fname; - String tresorier_email; - int tresorier_lincence; + String m2_lname; + String m2_fname; + String m2_email; + int m2_lincence; + RoleAsso m2_role; - String secretaire_lname; - String secretaire_fname; - String secretaire_email; - int secretaire_lincence; + String m3_lname; + String m3_fname; + String m3_email; + int m3_lincence; + RoleAsso m3_role; int saison; } diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/ClubModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/ClubModel.java index 791c31a..e69b1fc 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/ClubModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/ClubModel.java @@ -4,6 +4,7 @@ import fr.titionfire.ffsaf.utils.Contact; import io.quarkus.runtime.annotations.RegisterForReflection; import jakarta.persistence.*; import lombok.*; +import org.eclipse.microprofile.openapi.annotations.media.Schema; import java.util.List; import java.util.Map; @@ -20,37 +21,55 @@ import java.util.Map; public class ClubModel { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Identifiant du club", example = "1") Long id; + @Schema(description = "Identifiant long du club (UUID)", example = "b94f3167-3f6a-449c-a73b-ec84202bf07e") String clubId; + @Schema(description = "Nom du club", example = "Association sportive") String name; + @Schema(description = "Pays du club", example = "FR") String country; - String shieldURL; - //@Enumerated(EnumType.STRING) @ElementCollection @CollectionTable(name = "club_contact_mapping", joinColumns = {@JoinColumn(name = "club_id", referencedColumnName = "id")}) @MapKeyColumn(name = "contact_type") + @Schema(description = "Les contacts du club", example = "{\"SITE\": \"www.test.com\", \"COURRIEL\": \"test@test.com\"}") Map contact; + @Lob + @Column(length = 4096) + @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}]") String training_location; + @Lob + @Column(length = 4096) + @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}]") String training_day_time; + @Schema(description = "Contact interne du club", example = "john.doe@test.com") String contact_intern; + @Schema(description = "Adresse postale du club", example = "1 rue de l'exemple, 75000 Paris") + String address; + + @Schema(description = "RNA du club", example = "W123456789") String RNA; - String SIRET; + @Schema(description = "Numéro SIRET du club", example = "12345678901234") + Long SIRET; - String no_affiliation; + @Schema(description = "Numéro d'affiliation du club", example = "12345") + Long no_affiliation; + @Schema(description = "Club international", example = "false") boolean international; - @OneToMany(mappedBy = "club", fetch = FetchType.EAGER) + @OneToMany(mappedBy = "club", fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @Schema(description = "Liste des affiliations du club (optionnel)") List affiliations; } diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionModel.java new file mode 100644 index 0000000..c9f6f15 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionModel.java @@ -0,0 +1,48 @@ +package fr.titionfire.ffsaf.data.model; + +import fr.titionfire.ffsaf.utils.CompetitionSystem; +import io.quarkus.runtime.annotations.RegisterForReflection; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.Date; +import java.util.List; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@RegisterForReflection + +@Entity +@Table(name = "compet") +public class CompetitionModel { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + + @Column(name = "system_type") + CompetitionSystem system; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "club", referencedColumnName = "id") + ClubModel club; + + String name; + + String uuid; + + Date date; + + @ManyToMany + @JoinTable(name = "register", + uniqueConstraints = @UniqueConstraint(columnNames = {"id_competition", "id_membre"}), + joinColumns = @JoinColumn(name = "id_competition"), + inverseJoinColumns = @JoinColumn(name = "id_membre")) + List insc; + + String owner; +} diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/LicenceModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/LicenceModel.java index ae5d0e7..890be1a 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/LicenceModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/LicenceModel.java @@ -3,6 +3,7 @@ package fr.titionfire.ffsaf.data.model; import io.quarkus.runtime.annotations.RegisterForReflection; import jakarta.persistence.*; import lombok.*; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @Getter @Setter @@ -16,15 +17,20 @@ import lombok.*; public class LicenceModel { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "L'identifiant de la licence.") Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "membre", referencedColumnName = "id") + @Schema(description = "Le membre de la licence. (optionnel)") MembreModel membre; + @Schema(description = "La saison de la licence.", example = "2025") int saison; - boolean certificate; + @Schema(description = "Nom du médecin sur certificat médical.", example = "M. Jean") + String certificate; + @Schema(description = "Licence validée", example = "true") boolean validate; } diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/MatchModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/MatchModel.java new file mode 100644 index 0000000..ece2466 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/data/model/MatchModel.java @@ -0,0 +1,56 @@ +package fr.titionfire.ffsaf.data.model; + +import fr.titionfire.ffsaf.utils.CompetitionSystem; +import fr.titionfire.ffsaf.utils.ScoreEmbeddable; +import io.quarkus.runtime.annotations.RegisterForReflection; +import jakarta.persistence.*; +import lombok.*; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@RegisterForReflection + +@Entity +@ToString +@Table(name = "match") +public class MatchModel { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + + @Column(name = "system_type") + CompetitionSystem system; + Long systemId; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "c1", referencedColumnName = "id") + MembreModel c1_id = null; + + String c1_str = null; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "c2", referencedColumnName = "id") + MembreModel c2_id = null; + + String c2_str = null; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "id_poule", referencedColumnName = "id") + PouleModel poule = null; + + long poule_ord = 0; + + boolean isEnd = true; + + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "score", joinColumns = @JoinColumn(name = "id_match")) + List scores = new ArrayList<>(); + + char groupe = 'A'; +} diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/MembreModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/MembreModel.java index 334c3fc..2e181c2 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/MembreModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/MembreModel.java @@ -7,6 +7,7 @@ import fr.titionfire.ffsaf.utils.RoleAsso; import io.quarkus.runtime.annotations.RegisterForReflection; import jakarta.persistence.*; import lombok.*; +import org.eclipse.microprofile.openapi.annotations.media.Schema; import java.util.Date; import java.util.List; @@ -24,35 +25,50 @@ public class MembreModel { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "L'identifiant du membre.", example = "1") Long id; + @Schema(description = "L'identifiant long du membre (userID).", example = "e81d1d35-d897-421e-8086-6c5e74d13c6e") String userId; + @Schema(description = "Le nom du membre.", example = "Dupont") String lname; + @Schema(description = "Le prénom du membre.", example = "Jean") String fname; + @Schema(description = "La catégorie du membre.", example = "SENIOR") Categorie categorie; @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "club", referencedColumnName = "id") + @Schema(description = "Le club du membre.") ClubModel club; + @Schema(description = "Le genre du membre.", example = "H") Genre genre; + @Schema(description = "Le numéro de licence du membre.", example = "12345") int licence; + @Schema(description = "Le pays du membre.", example = "FR") String country; + @Schema(description = "La date de naissance du membre.") Date birth_date; + @Schema(description = "L'adresse e-mail du membre.", example = "jean.dupont@example.com") String email; + @Schema(description = "Le rôle du membre dans l'association.", example = "MEMBRE") RoleAsso role; + @Schema(description = "Le grade d'arbitrage du membre.", example = "NA") GradeArbitrage grade_arbitrage; + @Schema(hidden = true) String url_photo; - @OneToMany(mappedBy = "membre", fetch = FetchType.LAZY) + @OneToMany(mappedBy = "membre", fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @Schema(description = "Les licences du membre. (optionnel)") List licences; } diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/PouleModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/PouleModel.java new file mode 100644 index 0000000..f48bafb --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/data/model/PouleModel.java @@ -0,0 +1,45 @@ +package fr.titionfire.ffsaf.data.model; + +import fr.titionfire.ffsaf.utils.CompetitionSystem; +import io.quarkus.runtime.annotations.RegisterForReflection; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@RegisterForReflection + +@Entity +@Table(name = "poule") +public class PouleModel { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + + @Column(name = "system_type") + CompetitionSystem system; + Long systemId; + + String name = ""; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "id_compet", referencedColumnName = "id") + CompetitionModel compet; + + @OneToMany(fetch = FetchType.LAZY) + @JoinColumn(name = "id_poule", referencedColumnName = "id") + List matchs; + + @OneToMany(fetch = FetchType.LAZY) + @JoinColumn(name = "id_poule", referencedColumnName = "id") + List tree; + + Integer type; +} diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/SequenceModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/SequenceModel.java new file mode 100644 index 0000000..f8fdad3 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/data/model/SequenceModel.java @@ -0,0 +1,24 @@ +package fr.titionfire.ffsaf.data.model; + +import fr.titionfire.ffsaf.utils.SequenceType; +import io.quarkus.runtime.annotations.RegisterForReflection; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.*; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@RegisterForReflection + +@Entity +@Table(name = "sequence") +public class SequenceModel { + @Id + SequenceType type; + + long value; +} diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/TreeModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/TreeModel.java new file mode 100644 index 0000000..c4a701b --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/data/model/TreeModel.java @@ -0,0 +1,39 @@ +package fr.titionfire.ffsaf.data.model; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@RegisterForReflection + +@Entity +@Table(name = "tree") +public class TreeModel { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + + @Column(name = "id_poule") + Long poule; + + Integer level; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "match_id", referencedColumnName = "id") + MatchModel match; + + @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) + @JoinColumn(referencedColumnName = "id") + TreeModel left; + + @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) + @JoinColumn(referencedColumnName = "id") + TreeModel right; +} diff --git a/src/main/java/fr/titionfire/ffsaf/data/repository/AffiliationRepository.java b/src/main/java/fr/titionfire/ffsaf/data/repository/AffiliationRepository.java new file mode 100644 index 0000000..b09aa38 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/data/repository/AffiliationRepository.java @@ -0,0 +1,9 @@ +package fr.titionfire.ffsaf.data.repository; + +import fr.titionfire.ffsaf.data.model.AffiliationModel; +import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class AffiliationRepository implements PanacheRepositoryBase { +} diff --git a/src/main/java/fr/titionfire/ffsaf/data/repository/CompetitionRepository.java b/src/main/java/fr/titionfire/ffsaf/data/repository/CompetitionRepository.java new file mode 100644 index 0000000..6c277e0 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/data/repository/CompetitionRepository.java @@ -0,0 +1,9 @@ +package fr.titionfire.ffsaf.data.repository; + +import fr.titionfire.ffsaf.data.model.CompetitionModel; +import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class CompetitionRepository implements PanacheRepositoryBase { +} diff --git a/src/main/java/fr/titionfire/ffsaf/data/repository/MatchRepository.java b/src/main/java/fr/titionfire/ffsaf/data/repository/MatchRepository.java new file mode 100644 index 0000000..ab284c4 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/data/repository/MatchRepository.java @@ -0,0 +1,9 @@ +package fr.titionfire.ffsaf.data.repository; + +import fr.titionfire.ffsaf.data.model.MatchModel; +import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class MatchRepository implements PanacheRepositoryBase { +} diff --git a/src/main/java/fr/titionfire/ffsaf/data/repository/PouleRepository.java b/src/main/java/fr/titionfire/ffsaf/data/repository/PouleRepository.java new file mode 100644 index 0000000..7535bd3 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/data/repository/PouleRepository.java @@ -0,0 +1,9 @@ +package fr.titionfire.ffsaf.data.repository; + +import fr.titionfire.ffsaf.data.model.PouleModel; +import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class PouleRepository implements PanacheRepositoryBase { +} diff --git a/src/main/java/fr/titionfire/ffsaf/data/repository/SequenceRepository.java b/src/main/java/fr/titionfire/ffsaf/data/repository/SequenceRepository.java new file mode 100644 index 0000000..9cd235f --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/data/repository/SequenceRepository.java @@ -0,0 +1,21 @@ +package fr.titionfire.ffsaf.data.repository; + +import fr.titionfire.ffsaf.data.model.SequenceModel; +import fr.titionfire.ffsaf.utils.SequenceType; +import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase; +import io.smallrye.mutiny.Uni; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class SequenceRepository implements PanacheRepositoryBase { + + public Uni getNextValueInTransaction(SequenceType type) { + return this.findById(type).onItem().ifNull() + .switchTo(() -> this.persist(new SequenceModel(type, 1L))) + .chain(v -> { + v.setValue(v.getValue() + 1); + return this.persistAndFlush(v); + }) + .map(SequenceModel::getValue); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/data/repository/TreeRepository.java b/src/main/java/fr/titionfire/ffsaf/data/repository/TreeRepository.java new file mode 100644 index 0000000..57bb22b --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/data/repository/TreeRepository.java @@ -0,0 +1,9 @@ +package fr.titionfire.ffsaf.data.repository; + +import fr.titionfire.ffsaf.data.model.TreeModel; +import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class TreeRepository implements PanacheRepositoryBase { +} diff --git a/src/main/java/fr/titionfire/ffsaf/domain/entity/ClubEntity.java b/src/main/java/fr/titionfire/ffsaf/domain/entity/ClubEntity.java index ab59f8b..cb840e9 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/entity/ClubEntity.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/entity/ClubEntity.java @@ -18,14 +18,13 @@ public class ClubEntity { private String name; private String clubId; private String country; - private String shieldURL; private Map contact; private String training_location; private String training_day_time; private String contact_intern; private String RNA; - private String SIRET; - private String no_affiliation; + private Long SIRET; + private Long no_affiliation; private boolean international; public static ClubEntity fromModel (ClubModel model) { @@ -38,7 +37,6 @@ public class ClubEntity { .name(model.getName()) .clubId(model.getClubId()) .country(model.getCountry()) - .shieldURL(model.getShieldURL()) .contact(model.getContact()) .training_location(model.getTraining_location()) .training_day_time(model.getTraining_day_time()) 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 071b9a8..30c300a 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java @@ -1,16 +1,26 @@ 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.data.model.*; +import fr.titionfire.ffsaf.data.repository.*; +import fr.titionfire.ffsaf.rest.data.SimpleAffiliation; +import fr.titionfire.ffsaf.rest.data.SimpleReqAffiliation; +import fr.titionfire.ffsaf.rest.exception.DBadRequestException; +import fr.titionfire.ffsaf.rest.exception.DNotFoundException; import fr.titionfire.ffsaf.rest.from.AffiliationRequestForm; +import fr.titionfire.ffsaf.rest.from.AffiliationRequestSaveForm; +import fr.titionfire.ffsaf.utils.SequenceType; import fr.titionfire.ffsaf.utils.Utils; import io.quarkus.hibernate.reactive.panache.Panache; import io.quarkus.hibernate.reactive.panache.common.WithSession; import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.unchecked.Unchecked; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.hibernate.reactive.mutiny.Mutiny; + +import java.util.List; +import java.util.stream.Stream; @WithSession @ApplicationScoped @@ -20,41 +30,336 @@ public class AffiliationService { CombRepository combRepository; @Inject - AffiliationRequestRepository repository; + ClubRepository clubRepository; + + @Inject + AffiliationRequestRepository repositoryRequest; + + @Inject + AffiliationRepository repository; + + @Inject + KeycloakService keycloakService; + + @Inject + SequenceRepository sequenceRepository; + + @Inject + LicenceRepository licenceRepository; @ConfigProperty(name = "upload_dir") String media; - public Uni save(AffiliationRequestForm form) { + public Uni> getAllReq() { + return repositoryRequest.listAll(); + } + + public Uni pre_save(AffiliationRequestForm form, boolean unique) { AffiliationRequestModel affModel = form.toModel(); - affModel.setSaison(Utils.getSaison()); + int currentSaison = Utils.getSaison(); return Uni.createFrom().item(affModel) - .call(model -> ((model.getPresident_lincence() != 0) ? combRepository.find("licence", - model.getPresident_lincence()).count().invoke(count -> { + .invoke(Unchecked.consumer(model -> { + if (model.getSaison() != currentSaison && model.getSaison() != currentSaison + 1) { + throw new DBadRequestException("Saison non valid"); + } + })) + .chain(() -> repositoryRequest.count("siret = ?1 and saison = ?2", affModel.getSiret(), + affModel.getSaison())) + .onItem().invoke(Unchecked.consumer(count -> { + if (count != 0 && unique) { + throw new DBadRequestException("Demande d'affiliation déjà existante"); + } + })) + .chain(() -> clubRepository.find("SIRET = ?1", affModel.getSiret()).firstResult().chain(club -> + repository.count("club = ?1 and saison = ?2", club, affModel.getSaison()))) + .onItem().invoke(Unchecked.consumer(count -> { + if (count != 0) { + throw new DBadRequestException("Affiliation déjà existante"); + } + })) + .map(o -> affModel) + .call(model -> ((model.getM1_lincence() != -1) ? combRepository.find("licence", + model.getM1_lincence()).count().invoke(Unchecked.consumer(count -> { if (count == 0) { - throw new IllegalArgumentException("Licence président inconnue"); + throw new DBadRequestException("Licence membre n°1 inconnue"); } - }) : Uni.createFrom().nullItem()) + })) : Uni.createFrom().nullItem()) ) - .call(model -> ((model.getTresorier_lincence() != 0) ? combRepository.find("licence", - model.getTresorier_lincence()).count().invoke(count -> { + .call(model -> ((model.getM2_lincence() != -1) ? combRepository.find("licence", + model.getM2_lincence()).count().invoke(Unchecked.consumer(count -> { if (count == 0) { - throw new IllegalArgumentException("Licence trésorier inconnue"); + throw new DBadRequestException("Licence membre n°2 inconnue"); } - }) : Uni.createFrom().nullItem()) + })) : Uni.createFrom().nullItem()) ) - .call(model -> ((model.getSecretaire_lincence() != 0) ? combRepository.find("licence", - model.getSecretaire_lincence()).count().invoke(count -> { + .call(model -> ((model.getM3_lincence() != -1) ? combRepository.find("licence", + model.getM3_lincence()).count().invoke(Unchecked.consumer(count -> { if (count == 0) { - throw new IllegalArgumentException("Licence secrétaire inconnue"); + throw new DBadRequestException("Licence membre n°3 inconnue"); } - }) : Uni.createFrom().nullItem()) - ).chain(model -> Panache.withTransaction(() -> repository.persist(model))) - .call(model -> Uni.createFrom().future(Utils.replacePhoto(model.getId(), form.getLogo(), media, + })) : Uni.createFrom().nullItem()) + ); + } + + public Uni saveEdit(AffiliationRequestForm form) { + return pre_save(form, false) + .chain(model -> repositoryRequest.findById(form.getId()) + .onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé")) + .chain(origine -> { + origine.setName(model.getName()); + origine.setRNA(model.getRNA()); + origine.setAddress(model.getAddress()); + origine.setM1_lname(model.getM1_lname()); + origine.setM1_fname(model.getM1_fname()); + origine.setM1_lincence(model.getM1_lincence()); + origine.setM1_role(model.getM1_role()); + origine.setM1_email(model.getM1_email()); + origine.setM2_lname(model.getM2_lname()); + origine.setM2_fname(model.getM2_fname()); + origine.setM2_lincence(model.getM2_lincence()); + origine.setM2_role(model.getM2_role()); + origine.setM2_email(model.getM2_email()); + origine.setM3_lname(model.getM3_lname()); + origine.setM3_fname(model.getM3_fname()); + origine.setM3_lincence(model.getM3_lincence()); + origine.setM3_role(model.getM3_role()); + origine.setM3_email(model.getM3_email()); + + return Panache.withTransaction(() -> repositoryRequest.persist(origine)); + })); + } + + public Uni save(AffiliationRequestForm form) { + // noinspection ResultOfMethodCallIgnored + 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"))) - .call(model -> Uni.createFrom().future(Utils.replacePhoto(model.getId(), form.getStatus(), media, + .onItem() + .invoke(model -> Uni.createFrom().future(Utils.replacePhoto(model.getId(), form.getStatus(), media, "aff_request/status"))) .map(__ -> "Ok"); } + + public Uni saveAdmin(AffiliationRequestSaveForm form) { + return repositoryRequest.findById(form.getId()) + .onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé")) + .map(model -> { + model.setName(form.getName()); + model.setSiret(form.getSiret()); + model.setRNA(form.getRna()); + model.setAddress(form.getAddress()); + + if (form.getM1_mode() == 2) { + model.setM1_lname(form.getM1_lname()); + model.setM1_fname(form.getM1_fname()); + } else { + model.setM1_lincence( + form.getM1_lincence() == null ? 0 : Integer.parseInt(form.getM1_lincence())); + } + model.setM1_role(form.getM1_role()); + if (form.getM1_email_mode() == 0) + model.setM1_email(form.getM1_email()); + + if (form.getM2_mode() == 2) { + model.setM2_lname(form.getM2_lname()); + model.setM2_fname(form.getM2_fname()); + } else { + model.setM2_lincence( + form.getM2_lincence() == null ? 0 : Integer.parseInt(form.getM2_lincence())); + } + model.setM2_role(form.getM2_role()); + if (form.getM2_email_mode() == 0) + model.setM2_email(form.getM2_email()); + + if (form.getM3_mode() == 2) { + model.setM3_lname(form.getM3_lname()); + model.setM3_fname(form.getM3_fname()); + } else { + model.setM3_lincence( + form.getM3_lincence() == null ? 0 : Integer.parseInt(form.getM3_lincence())); + } + model.setM3_role(form.getM3_role()); + if (form.getM3_email_mode() == 0) + model.setM3_email(form.getM3_email()); + + return model; + }) + .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"))) + .map(__ -> "Ok"); + } + + private Uni setMembre(AffiliationRequestSaveForm.Member member, ClubModel club, int saison) { + return Uni.createFrom().nullItem().chain(__ -> { + if (member.getMode() == 2) { + MembreModel membreModel = new MembreModel(); + membreModel.setFname(member.getFname()); + membreModel.setLname(member.getLname().toUpperCase()); + membreModel.setClub(club); + membreModel.setRole(member.getRole()); + membreModel.setEmail(member.getEmail()); + return Panache.withTransaction(() -> + combRepository.persist(membreModel) + .chain(m -> sequenceRepository.getNextValueInTransaction(SequenceType.Licence) + .invoke(l -> m.setLicence(Math.toIntExact(l))) + .chain(() -> combRepository.persist(m)))); + } else { + return combRepository.find("licence", Integer.parseInt(member.getLicence())).firstResult() + .onItem().ifNull().switchTo(() -> { + MembreModel membreModel = new MembreModel(); + membreModel.setFname(member.getFname()); + membreModel.setLname(member.getLname().toUpperCase()); + return Panache.withTransaction( + () -> sequenceRepository.getNextValueInTransaction(SequenceType.Licence) + .invoke(l -> membreModel.setLicence(Math.toIntExact(l))) + .chain(() -> combRepository.persist(membreModel))); + }) + .map(m -> { + m.setClub(club); + m.setRole(member.getRole()); + m.setEmail(member.getEmail()); + return m; + }).call(m -> Panache.withTransaction(() -> combRepository.persist(m))); + } + }) + .call(m -> (m.getUserId() == null) ? keycloakService.initCompte(m.getId()) : + keycloakService.setClubGroupMembre(m, club)) + .call(m -> Panache.withTransaction(() -> licenceRepository.persist( + new LicenceModel(null, m, saison, null, true)))); + } + + public Uni accept(AffiliationRequestSaveForm form) { + return repositoryRequest.findById(form.getId()) + .onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé")) + .chain(req -> + clubRepository.find("SIRET = ?1", form.getSiret()).firstResult() + .chain(model -> (model == null) ? acceptNew(form, req) : acceptOld(form, req, model)) + .call(club -> setMembre(form.new Member(1), club, req.getSaison()) + .call(__ -> setMembre(form.new Member(2), club, req.getSaison()) + .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"))) + .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", + "clubStatus")) + ) + .map(__ -> "Ok"); + } + + private Uni acceptNew(AffiliationRequestSaveForm form, AffiliationRequestModel model) { + return Uni.createFrom().nullItem() + .chain(() -> { + ClubModel club = new ClubModel(); + club.setName(form.getName()); + club.setCountry("fr"); + club.setSIRET(form.getSiret()); + club.setRNA(form.getRna()); + club.setAddress(form.getAddress()); + club.setAffiliations(List.of(new AffiliationModel(null, club, model.getSaison()))); + return Panache.withTransaction(() -> clubRepository.persist(club) + .chain(c -> sequenceRepository.getNextValueInTransaction(SequenceType.Affiliation) + .invoke(c::setNo_affiliation) + .chain(() -> clubRepository.persist(c)) + .chain(() -> repositoryRequest.delete(model)) + ) + .chain(() -> repository.persist(new AffiliationModel(null, club, model.getSaison()))) + .map(c -> club)); + }); + } + + private Uni acceptOld(AffiliationRequestSaveForm form, AffiliationRequestModel model, ClubModel club) { + return Uni.createFrom().nullItem() + .chain(() -> { + club.setName(form.getName()); + club.setCountry("fr"); + club.setSIRET(form.getSiret()); + club.setRNA(form.getRna()); + club.setAddress(form.getAddress()); + return Panache.withTransaction(() -> clubRepository.persist(club) + .chain(() -> repository.persist(new AffiliationModel(null, club, model.getSaison()))) + .chain(() -> repositoryRequest.delete(model))); + }) + .map(__ -> club); + } + + public Uni getRequest(long id) { + return repositoryRequest.findById(id).map(SimpleReqAffiliation::fromModel) + .onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé")) + .call(out -> clubRepository.find("SIRET = ?1", out.getSiret()).firstResult().invoke(c -> { + if (c != null) { + out.setClub(c.getId()); + out.setClub_name(c.getName()); + out.setClub_no_aff(c.getNo_affiliation()); + } + }) + ); + } + + public Uni> getCurrentSaisonAffiliation() { + return repositoryRequest.list("saison = ?1 or saison = ?1 + 1", Utils.getSaison()) + .map(models -> models.stream() + .map(model -> new SimpleAffiliation(model.getId() * -1, model.getSiret(), model.getSaison(), + false)).toList()) + .chain(aff -> repository.list("saison = ?1", Utils.getSaison()) + .map(models -> models.stream().map(SimpleAffiliation::fromModel).toList()) + .map(aff2 -> Stream.concat(aff2.stream(), aff.stream()).toList()) + ); + } + + public Uni> getAffiliation(long id) { + return clubRepository.findById(id) + .onItem().ifNull().failWith(new DNotFoundException("Club non trouvé")) + .call(model -> Mutiny.fetch(model.getAffiliations())) + .chain(model -> repositoryRequest.list("siret = ?1", model.getSIRET()) + .map(reqs -> reqs.stream().map(req -> + new SimpleAffiliation(req.getId() * -1, model.getId(), req.getSaison(), false))) + .map(aff2 -> Stream.concat(aff2, + model.getAffiliations().stream().map(SimpleAffiliation::fromModel)).toList()) + ); + } + + public Uni setAffiliation(long id, int saison) { + return clubRepository.findById(id) + .onItem().ifNull().failWith(new DNotFoundException("Club non trouvé")) + .call(model -> Mutiny.fetch(model.getAffiliations())) + .invoke(Unchecked.consumer(club -> { + if (club.getAffiliations().stream().anyMatch(affiliation -> affiliation.getSaison() == saison)) { + throw new DBadRequestException("Affiliation déjà existante"); + } + })) + .chain(club -> + Panache.withTransaction(() -> repository.persist(new AffiliationModel(null, club, saison)) + .chain(c -> (club.getNo_affiliation() != null) ? Uni.createFrom().item(c) : + sequenceRepository.getNextValueInTransaction(SequenceType.Affiliation) + .invoke(club::setNo_affiliation) + .chain(() -> clubRepository.persist(club)) + .map(o -> c) + ))) + .map(SimpleAffiliation::fromModel); + } + + public Uni deleteAffiliation(long id) { + return Panache.withTransaction(() -> repository.deleteById(id)); + } + + public Uni deleteReqAffiliation(long id) { + return Panache.withTransaction(() -> repositoryRequest.deleteById(id)) + .call(__ -> Utils.deleteMedia(id, media, "aff_request/logo")) + .call(__ -> Utils.deleteMedia(id, media, "aff_request/status")); + } } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/ClubService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/ClubService.java index 4169c52..e64f69a 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/ClubService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/ClubService.java @@ -1,17 +1,44 @@ package fr.titionfire.ffsaf.domain.service; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import fr.titionfire.ffsaf.data.model.AffiliationModel; 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.net2.ServerCustom; import fr.titionfire.ffsaf.net2.data.SimpleClubModel; +import fr.titionfire.ffsaf.net2.request.SReqClub; +import fr.titionfire.ffsaf.rest.data.DeskMember; +import fr.titionfire.ffsaf.rest.data.RenewAffData; +import fr.titionfire.ffsaf.rest.data.SimpleClubList; +import fr.titionfire.ffsaf.rest.exception.DBadRequestException; +import fr.titionfire.ffsaf.rest.exception.DForbiddenException; +import fr.titionfire.ffsaf.rest.exception.DNotFoundException; +import fr.titionfire.ffsaf.rest.from.FullClubForm; +import fr.titionfire.ffsaf.rest.from.PartClubForm; +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; +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; import io.quarkus.vertx.VertxContextSupport; import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.unchecked.Unchecked; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.hibernate.reactive.mutiny.Mutiny; import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.function.Consumer; + +import static fr.titionfire.ffsaf.net2.Client_Thread.MAPPER; @WithSession @ApplicationScoped @@ -20,13 +47,27 @@ public class ClubService { @Inject ClubRepository repository; + @Inject + ServerCustom serverCustom; + + @Inject + CombRepository combRepository; + + @Inject + KeycloakService keycloakService; + + @ConfigProperty(name = "upload_dir") + String media; + public SimpleClubModel findByIdOptionalClub(long id) throws Throwable { - return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(() -> repository.findById(id).map(SimpleClubModel::fromModel))); + return VertxContextSupport.subscribeAndAwait( + () -> Panache.withTransaction(() -> repository.findById(id).map(SimpleClubModel::fromModel))); } public Collection findAllClub() throws Throwable { return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction( - () -> repository.findAll().list().map(clubModels -> clubModels.stream().map(SimpleClubModel::fromModel).toList()))); + () -> repository.findAll().list() + .map(clubModels -> clubModels.stream().map(SimpleClubModel::fromModel).toList()))); } public Uni> getAll() { @@ -39,4 +80,203 @@ public class ClubService { return Panache.withTransaction(() -> repository.persist(clubModel)); }); } + + public Uni> search(Integer limit, int page, String search, String country) { + if (search == null) + search = ""; + search = search + "%"; + + PanacheQuery query; + + if (country == null || country.isBlank()) + query = repository.find("name LIKE ?1", + Sort.ascending("name"), search).page(Page.ofSize(limit)); + else + query = repository.find("name LIKE ?1 AND country LIKE ?2", + Sort.ascending("name"), search, country + "%").page(Page.ofSize(limit)); + return getPageResult(query, limit, page); + } + + private Uni> getPageResult(PanacheQuery query, int limit, int page) { + return Uni.createFrom().item(new PageResult()) + .invoke(result -> result.setPage(page)) + .invoke(result -> result.setPage_size(limit)) + .call(result -> query.count().invoke(result::setResult_count)) + .call(result -> query.pageCount() + .invoke(Unchecked.consumer(pages -> { + if (page > pages) throw new DBadRequestException("Page out of range"); + })) + .invoke(result::setPage_count)) + .call(result -> query.page(Page.of(page, limit)).list() + .map(membreModels -> membreModels.stream().map(SimpleClubList::fromModel).toList()) + .invoke(result::setResult)); + } + + public Uni getById(long id) { + return repository.findById(id).call(m -> Mutiny.fetch(m.getContact())); + } + + public Uni getByClubId(String clubId) { + return repository.find("clubId", clubId).firstResult(); + } + + public Uni getOfUser(SecurityCtx securityCtx) { + return combRepository.find("userId = ?1", securityCtx.getSubject()).firstResult().invoke(Unchecked.consumer(m -> { + if (m == null || m.getClub() == null) + throw new DNotFoundException("Club non trouvé"); + })) + .map(MembreModel::getClub) + .call(club -> Mutiny.fetch(club.getContact())); + } + + public Uni> getClubDesk(Consumer consumer, long id) { + return repository.findById(id).invoke(consumer) + .chain(club -> combRepository.list("club = ?1", club)) + .map(combs -> combs.stream() + .filter(o -> o.getRole() != null && o.getRole().level >= RoleAsso.MEMBREBUREAU.level) + .sorted((o1, o2) -> o2.getRole().level - o1.getRole().level) + .map(DeskMember::fromModel) + .toList()); + } + + public Uni updateOfUser(SecurityCtx securityCtx, PartClubForm form) { + TypeReference> typeRef = new TypeReference<>() { + }; + + return combRepository.find("userId = ?1", securityCtx.getSubject()).firstResult().invoke(Unchecked.consumer(m -> { + if (m == null || m.getClub() == null) + throw new DNotFoundException("Club non trouvé"); + if (!securityCtx.isInClubGroup(m.getClub().getId())) + throw new DForbiddenException(); + })) + .map(MembreModel::getClub) + .call(club -> Mutiny.fetch(club.getContact())) + .chain(Unchecked.function(club -> { + club.setContact_intern(form.getContact_intern()); + club.setAddress(form.getAddress()); + + try { + club.setContact(MAPPER.readValue(form.getContact(), typeRef)); + } catch (JsonProcessingException e) { + throw new DBadRequestException("Erreur de format des contacts"); + } + + club.setTraining_location(form.getTraining_location()); + club.setTraining_day_time(form.getTraining_day_time()); + return Panache.withTransaction(() -> repository.persist(club)); + })) + .map(__ -> "OK"); + } + + public Uni update(long id, FullClubForm input) { + return repository.findById(id).call(m -> Mutiny.fetch(m.getContact())) + .onItem().transformToUni(Unchecked.function(m -> { + TypeReference> typeRef = new TypeReference<>() { + }; + + m.setName(input.getName()); + m.setCountry(input.getCountry()); + m.setInternational(input.isInternational()); + + if (!input.isInternational()) { + m.setTraining_location(input.getTraining_location()); + m.setTraining_day_time(input.getTraining_day_time()); + m.setContact_intern(input.getContact_intern()); + m.setRNA(input.getRna()); + if (input.getSiret() != null && !input.getSiret().isBlank()) + m.setSIRET(Long.parseLong(input.getSiret())); + m.setAddress(input.getAddress()); + + try { + m.setContact(MAPPER.readValue(input.getContact(), typeRef)); + } catch (JsonProcessingException e) { + throw new DBadRequestException("Erreur de format des contacts"); + } + } + return Panache.withTransaction(() -> repository.persist(m)); + })) + .invoke(membreModel -> SReqClub.sendIfNeed(serverCustom.clients, + SimpleClubModel.fromModel(membreModel))) + .map(__ -> "OK"); + } + + public Uni add(FullClubForm input) { + TypeReference> typeRef = new TypeReference<>() { + }; + + return Uni.createFrom().nullItem() + .chain(() -> { + ClubModel clubModel = new ClubModel(); + + clubModel.setName(input.getName()); + clubModel.setCountry(input.getCountry()); + clubModel.setInternational(input.isInternational()); + clubModel.setNo_affiliation(null); + if (!input.isInternational()) { + clubModel.setTraining_location(input.getTraining_location()); + clubModel.setTraining_day_time(input.getTraining_day_time()); + clubModel.setContact_intern(input.getContact_intern()); + clubModel.setRNA(input.getRna()); + if (input.getSiret() != null && !input.getSiret().isBlank()) + clubModel.setSIRET(Long.parseLong(input.getSiret())); + clubModel.setAddress(input.getAddress()); + + try { + clubModel.setContact(MAPPER.readValue(input.getContact(), typeRef)); + } catch (JsonProcessingException ignored) { + } + } + + return Panache.withTransaction(() -> repository.persist(clubModel)); + }) + .call(clubModel -> keycloakService.getGroupFromClub(clubModel)) // create group in keycloak + .invoke(clubModel -> SReqClub.sendAddIfNeed(serverCustom.clients, SimpleClubModel.fromModel(clubModel))) + .map(ClubModel::getId); + } + + public Uni delete(long id) { + return repository.findById(id) + .chain(club -> combRepository.list("club = ?1", club) + .map(combModels -> combModels.stream().peek(combModel -> { + combModel.setClub(null); + combModel.setRole(RoleAsso.MEMBRE); + }).toList()) + .call(list -> (list.isEmpty()) ? Uni.createFrom().voidItem() : + Uni.join().all(list.stream().filter(m -> m.getUserId() != null) + .map(m -> keycloakService.clearUser(m.getUserId())).toList()) + .andCollectFailures()) + .chain(list -> Panache.withTransaction(() -> combRepository.persist(list))) + .map(o -> club) + ) + .call(clubModel -> (clubModel.getClubId() == null) ? Uni.createFrom() + .voidItem() : keycloakService.removeClubGroup(clubModel.getClubId())) + .invoke(membreModel -> SReqClub.sendRmIfNeed(serverCustom.clients, id)) + .chain(clubModel -> Panache.withTransaction(() -> repository.delete(clubModel))) + .call(__ -> Utils.deleteMedia(id, media, "ppClub")) + .call(__ -> Utils.deleteMedia(id, media, "clubStatus")); + } + + public Uni getRenewData(long id, List mIds) { + RenewAffData data = new RenewAffData(); + + return repository.findById(id) + .call(clubModel -> Mutiny.fetch(clubModel.getAffiliations())) + .invoke(clubModel -> { + data.setName(clubModel.getName()); + data.setSiret(clubModel.getSIRET()); + data.setRna(clubModel.getRNA()); + data.setAddress(clubModel.getAddress()); + data.setSaison( + clubModel.getAffiliations().stream().max(Comparator.comparing(AffiliationModel::getSaison)) + .map(AffiliationModel::getSaison).map(i -> Math.min(i + 1, Utils.getSaison() + 1)) + .orElse(Utils.getSaison())); + }) + .chain(club -> combRepository.list("id IN ?1", mIds)) + .invoke(combs -> data.setMembers(combs.stream() + .filter(o -> o.getRole() != null && o.getRole().level >= RoleAsso.MEMBREBUREAU.level) + .sorted((o1, o2) -> o2.getRole().level - o1.getRole().level) + .map(RenewAffData.RenewMember::new) + .toList())) + .map(o -> data); + } } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/CompetPermService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/CompetPermService.java new file mode 100644 index 0000000..3064021 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/CompetPermService.java @@ -0,0 +1,134 @@ +package fr.titionfire.ffsaf.domain.service; + +import fr.titionfire.ffsaf.data.model.CompetitionModel; +import fr.titionfire.ffsaf.data.repository.CompetitionRepository; +import fr.titionfire.ffsaf.net2.ServerCustom; +import fr.titionfire.ffsaf.net2.data.SimpleCompet; +import fr.titionfire.ffsaf.net2.request.SReqCompet; +import fr.titionfire.ffsaf.rest.exception.DForbiddenException; +import fr.titionfire.ffsaf.utils.CompetitionSystem; +import fr.titionfire.ffsaf.utils.SecurityCtx; +import io.quarkus.cache.Cache; +import io.quarkus.cache.CacheName; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.unchecked.Unchecked; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import java.util.HashMap; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +@ApplicationScoped +public class CompetPermService { + + @Inject + ServerCustom serverCustom; + + @Inject + @CacheName("safca-config") + Cache cache; + + @Inject + @CacheName("safca-have-access") + Cache cacheAccess; + + @Inject + CompetitionRepository competitionRepository; + + public Uni getSafcaConfig(long id) { + return cache.get(id, k -> { + CompletableFuture f = new CompletableFuture<>(); + SReqCompet.getConfig(serverCustom.clients, id, f); + System.out.println("get config"); + try { + return f.get(1500, TimeUnit.MILLISECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new RuntimeException(e); + } + }); + } + + public Uni> getAllHaveAccess(String subject) { + return cacheAccess.get(subject, k -> { + CompletableFuture> f = new CompletableFuture<>(); + SReqCompet.getAllHaveAccess(serverCustom.clients, subject, f); + System.out.println("get all have access"); + try { + return f.get(1500, TimeUnit.MILLISECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new RuntimeException(e); + } + }); + } + + public Uni hasViewPerm(SecurityCtx securityCtx, CompetitionModel competitionModel) { + return hasViewPerm(securityCtx, Uni.createFrom().item(competitionModel)); + } + + public Uni hasViewPerm(SecurityCtx securityCtx, long id) { + return hasViewPerm(securityCtx, competitionRepository.findById(id)); + } + + private Uni hasViewPerm(SecurityCtx securityCtx, Uni in) { + return in.call(o -> ( + securityCtx.getSubject().equals(o.getOwner()) || securityCtx.roleHas("federation_admin")) ? + Uni.createFrom().nullItem() + : + o.getSystem() == CompetitionSystem.SAFCA ? + hasSafcaViewPerm(securityCtx, o.getId()) + : Uni.createFrom().nullItem().invoke(Unchecked.consumer(__ -> { + if (!securityCtx.isInClubGroup(o.getClub().getId())) + throw new DForbiddenException(); + }) + )); + } + + public Uni hasEditPerm(SecurityCtx securityCtx, CompetitionModel competitionModel) { + return hasEditPerm(securityCtx, Uni.createFrom().item(competitionModel)); + } + + public Uni hasEditPerm(SecurityCtx securityCtx, long id) { + return hasEditPerm(securityCtx, competitionRepository.findById(id)); + } + + public Uni hasEditPerm(SecurityCtx securityCtx, Uni in) { + return in.call(o -> ( + securityCtx.getSubject().equals(o.getOwner()) || securityCtx.roleHas("federation_admin")) ? + Uni.createFrom().nullItem() + : + o.getSystem() == CompetitionSystem.SAFCA ? + hasSafcaEditPerm(securityCtx, o.getId()) + : Uni.createFrom().nullItem().invoke(Unchecked.consumer(__ -> { + if (!securityCtx.isInClubGroup(o.getClub().getId())) + throw new DForbiddenException(); + }) + )); + } + + private Uni hasSafcaViewPerm(SecurityCtx securityCtx, long id) { + return securityCtx.roleHas("safca_super_admin") ? + Uni.createFrom().nullItem() + : + getSafcaConfig(id).chain(Unchecked.function(o -> { + if (!o.admin().contains(UUID.fromString(securityCtx.getSubject())) && !o.table() + .contains(UUID.fromString(securityCtx.getSubject()))) + throw new DForbiddenException(); + return Uni.createFrom().nullItem(); + })); + } + + private Uni hasSafcaEditPerm(SecurityCtx securityCtx, long id) { + return securityCtx.roleHas("safca_super_admin") ? + Uni.createFrom().nullItem() + : + getSafcaConfig(id).chain(Unchecked.function(o -> { + if (!o.admin().contains(UUID.fromString(securityCtx.getSubject()))) + throw new DForbiddenException(); + return Uni.createFrom().nullItem(); + })); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java new file mode 100644 index 0000000..3a338ac --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java @@ -0,0 +1,248 @@ +package fr.titionfire.ffsaf.domain.service; + +import fr.titionfire.ffsaf.data.model.CompetitionModel; +import fr.titionfire.ffsaf.data.repository.ClubRepository; +import fr.titionfire.ffsaf.data.repository.CompetitionRepository; +import fr.titionfire.ffsaf.data.repository.MatchRepository; +import fr.titionfire.ffsaf.data.repository.PouleRepository; +import fr.titionfire.ffsaf.net2.ServerCustom; +import fr.titionfire.ffsaf.net2.data.SimpleCompet; +import fr.titionfire.ffsaf.net2.request.SReqCompet; +import fr.titionfire.ffsaf.rest.data.CompetitionData; +import fr.titionfire.ffsaf.rest.data.SimpleCompetData; +import fr.titionfire.ffsaf.rest.exception.DBadRequestException; +import fr.titionfire.ffsaf.rest.exception.DForbiddenException; +import fr.titionfire.ffsaf.utils.CompetitionSystem; +import fr.titionfire.ffsaf.utils.SecurityCtx; +import io.quarkus.hibernate.reactive.panache.Panache; +import io.quarkus.hibernate.reactive.panache.common.WithSession; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.unchecked.Unchecked; +import io.vertx.mutiny.core.Vertx; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.keycloak.representations.idm.UserRepresentation; + +import java.util.*; +import java.util.stream.Stream; + +@WithSession +@ApplicationScoped +public class CompetitionService { + + @Inject + CompetitionRepository repository; + + @Inject + PouleRepository pouleRepository; + + @Inject + MatchRepository matchRepository; + + @Inject + KeycloakService keycloakService; + + @Inject + ServerCustom serverCustom; + + @Inject + CompetPermService permService; + + @Inject + Vertx vertx; + + public Uni getById(SecurityCtx securityCtx, Long id) { + if (id == 0) { + return Uni.createFrom() + .item(new CompetitionData(null, "", "", new Date(), CompetitionSystem.SAFCA, + null, "", "")); + } + return permService.hasViewPerm(securityCtx, id) + .map(CompetitionData::fromModel) + .chain(data -> + vertx.getOrCreateContext().executeBlocking(() -> { + keycloakService.getUser(UUID.fromString(data.getOwner())) + .ifPresent(user -> data.setOwner(user.getUsername())); + return data; + }) + ); + } + + public Uni> getAll(SecurityCtx securityCtx) { + return repository.listAll() + .chain(o -> + permService.getAllHaveAccess(securityCtx.getSubject()) + .chain(map -> Uni.createFrom().item(o.stream() + .filter(p -> { + if (securityCtx.getSubject().equals(p.getOwner())) + return true; + if (p.getSystem() == CompetitionSystem.SAFCA) { + if (map.containsKey(p.getId())) + return map.get(p.getId()).equals("admin"); + return securityCtx.roleHas("federation_admin") + || securityCtx.roleHas("safca_super_admin"); + } + return securityCtx.roleHas("federation_admin"); + }) + .map(CompetitionData::fromModel).toList()) + )); + } + + public Uni> getAllSystem(SecurityCtx securityCtx, + CompetitionSystem system) { + if (system == CompetitionSystem.SAFCA) { + return permService.getAllHaveAccess(securityCtx.getSubject()) + .chain(map -> + repository.list("system = ?1", system) + .map(data -> data.stream() + .filter(p -> { + if (securityCtx.getSubject().equals(p.getOwner())) + return true; + if (map.containsKey(p.getId())) + return map.get(p.getId()).equals("admin"); + return securityCtx.roleHas("federation_admin") + || securityCtx.roleHas("safca_super_admin"); + }) + .map(CompetitionData::fromModel).toList()) + ); + } + + return repository.list("system = ?1", system) + .map(data -> data.stream() + .filter(p -> { + if (securityCtx.getSubject().equals(p.getOwner())) + return true; + return securityCtx.roleHas("federation_admin") || + securityCtx.isInClubGroup(p.getClub().getId()); + }) + .map(CompetitionData::fromModel).toList()); + } + + public Uni addOrUpdate(SecurityCtx securityCtx, CompetitionData data) { + if (data.getId() == null) { + return new ClubRepository().findById(data.getClub()).invoke(Unchecked.consumer(clubModel -> { + if (!securityCtx.isInClubGroup(clubModel.getId())) + throw new DForbiddenException(); + })) // TODO check if user can create competition + .chain(clubModel -> { + CompetitionModel model = new CompetitionModel(); + + model.setId(null); + model.setSystem(data.getSystem()); + model.setClub(clubModel); + model.setDate(data.getDate()); + model.setInsc(new ArrayList<>()); + model.setUuid(UUID.randomUUID().toString()); + model.setName(data.getName()); + model.setOwner(securityCtx.getSubject()); + + return Panache.withTransaction(() -> repository.persist(model)); + }).map(CompetitionData::fromModel) + .call(__ -> permService.cacheAccess.invalidate(securityCtx.getSubject())); + } else { + return permService.hasEditPerm(securityCtx, data.getId()) + .chain(model -> { + model.setDate(data.getDate()); + model.setName(data.getName()); + + return vertx.getOrCreateContext().executeBlocking(() -> + keycloakService.getUser(data.getOwner()).map(UserRepresentation::getId).orElse(null)) + .invoke(Unchecked.consumer(newOwner -> { + if (newOwner == null) + throw new DBadRequestException("User " + data.getOwner() + " not found"); + if (!newOwner.equals(model.getOwner())) { + if (!securityCtx.roleHas("federation_admin") + && !securityCtx.roleHas("safca_super_admin") + && !securityCtx.getSubject().equals(model.getOwner())) + throw new DForbiddenException(); + model.setOwner(newOwner); + } + })) + .chain(__ -> Panache.withTransaction(() -> repository.persist(model))); + }).map(CompetitionData::fromModel) + .call(__ -> permService.cacheAccess.invalidate(securityCtx.getSubject())); + } + } + + public Uni delete(SecurityCtx securityCtx, Long id) { + return repository.findById(id).invoke(Unchecked.consumer(c -> { + if (!securityCtx.getSubject().equals(c.getOwner()) || securityCtx.roleHas("federation_admin")) + throw new DForbiddenException(); + })) + .call(competitionModel -> pouleRepository.list("compet = ?1", competitionModel) + .call(pouleModels -> Uni.join() + .all(pouleModels.stream() + .map(pouleModel -> Panache.withTransaction( + () -> matchRepository.delete("poule = ?1", pouleModel.getId()))) + .toList()) + .andCollectFailures())) + .call(competitionModel -> Panache.withTransaction( + () -> pouleRepository.delete("compet = ?1", competitionModel))) + .chain(model -> Panache.withTransaction(() -> repository.delete("id", model.getId()))) + .invoke(o -> SReqCompet.rmCompet(serverCustom.clients, id)) + .call(__ -> permService.cache.invalidate(id)); + } + + public Uni getSafcaData(SecurityCtx securityCtx, Long id) { + return permService.getSafcaConfig(id) + .call(Unchecked.function(o -> { + if (!securityCtx.getSubject().equals(o.owner()) + && !securityCtx.roleHas("federation_admin") + && !securityCtx.roleHas("safca_super_admin") + && !o.admin().contains(UUID.fromString(securityCtx.getSubject())) + && !o.table().contains(UUID.fromString(securityCtx.getSubject()))) + throw new DForbiddenException(); + return Uni.createFrom().nullItem(); + })) + .chain(simpleCompet -> { + SimpleCompetData data = SimpleCompetData.fromModel(simpleCompet); + return vertx.getOrCreateContext().executeBlocking(() -> { + data.setAdmin(simpleCompet.admin().stream().map(uuid -> keycloakService.getUser(uuid)) + .filter(Optional::isPresent) + .map(user -> user.get().getUsername()) + .toList()); + data.setTable(simpleCompet.table().stream().map(uuid -> keycloakService.getUser(uuid)) + .filter(Optional::isPresent) + .map(user -> user.get().getUsername()) + .toList()); + + return data; + }); + }); + } + + public Uni setSafcaData(SecurityCtx securityCtx, SimpleCompetData data) { + return permService.hasEditPerm(securityCtx, data.getId()) + .chain(__ -> vertx.getOrCreateContext().executeBlocking(() -> { + ArrayList admin = new ArrayList<>(); + ArrayList table = new ArrayList<>(); + for (String username : data.getAdmin()) { + Optional opt = keycloakService.getUser(username); + if (opt.isEmpty()) + throw new DBadRequestException("User " + username + " not found"); + admin.add(UUID.fromString(opt.get().getId())); + } + for (String username : data.getTable()) { + Optional opt = keycloakService.getUser(username); + if (opt.isEmpty()) + throw new DBadRequestException("User " + username + " not found"); + table.add(UUID.fromString(opt.get().getId())); + } + + return new SimpleCompet(data.getId(), "", data.isShow_blason(), + data.isShow_flag(), admin, table); + })) + .invoke(simpleCompet -> SReqCompet.sendUpdate(serverCustom.clients, simpleCompet)) + .call(simpleCompet -> permService.getSafcaConfig(data.getId()) + .call(c -> Uni.join().all(Stream.concat( + Stream.concat( + c.admin().stream().filter(uuid -> !simpleCompet.admin().contains(uuid)), + simpleCompet.admin().stream().filter(uuid -> !c.admin().contains(uuid))), + Stream.concat( + c.table().stream().filter(uuid -> !simpleCompet.table().contains(uuid)), + simpleCompet.table().stream().filter(uuid -> !c.table().contains(uuid)))) + .map(uuid -> permService.cacheAccess.invalidate(uuid.toString())).toList()) + .andCollectFailures())) + .call(__ -> permService.cache.invalidate(data.getId())); + } +} 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 f59c861..a87c2e5 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/KeycloakService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/KeycloakService.java @@ -2,6 +2,7 @@ package fr.titionfire.ffsaf.domain.service; import fr.titionfire.ffsaf.data.model.ClubModel; import fr.titionfire.ffsaf.data.model.MembreModel; +import fr.titionfire.ffsaf.rest.exception.DInternalError; import fr.titionfire.ffsaf.utils.*; import io.quarkus.runtime.annotations.RegisterForReflection; import io.smallrye.mutiny.Uni; @@ -23,6 +24,7 @@ import java.text.Normalizer; import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.UUID; @ApplicationScoped public class KeycloakService { @@ -49,21 +51,26 @@ public class KeycloakService { return vertx.getOrCreateContext().executeBlocking(() -> { GroupRepresentation clubGroup = keycloak.realm(realm).groups().groups().stream().filter(g -> g.getName().equals("club")) - .findAny().orElseThrow(() -> new KeycloakException("Fail to fetch group %s".formatted("club"))); + .findAny() + .orElseThrow(() -> new KeycloakException("Fail to fetch group %s".formatted("club"))); GroupRepresentation groupRepresentation = new GroupRepresentation(); groupRepresentation.setName(club.getId() + "-" + club.getName()); try (Response response = keycloak.realm(realm).groups().group(clubGroup.getId()).subGroup(groupRepresentation)) { - if (!response.getStatusInfo().equals(Response.Status.CREATED) && !response.getStatusInfo().equals(Response.Status.CONFLICT)) - throw new KeycloakException("Fail to set group parent for club: %s (reason=%s)".formatted(club.getName(), - response.getStatusInfo().getReasonPhrase())); + if (!response.getStatusInfo().equals(Response.Status.CREATED) && !response.getStatusInfo() + .equals(Response.Status.CONFLICT)) + throw new KeycloakException( + "Fail to set group parent for club: %s (reason=%s)".formatted(club.getName(), + response.getStatusInfo().getReasonPhrase())); } return keycloak.realm(realm).groups().group(clubGroup.getId()).getSubGroups(0, 1000, true).stream() - .filter(g -> g.getName().startsWith(club.getId() + "-")).findAny().map(GroupRepresentation::getId) - .orElseThrow(() -> new KeycloakException("Fail to fetch group %s*".formatted(club.getId() + "-"))); + .filter(g -> g.getName().startsWith(club.getId() + "-")).findAny() + .map(GroupRepresentation::getId) + .orElseThrow( + () -> new KeycloakException("Fail to fetch group %s*".formatted(club.getId() + "-"))); } ).call(id -> clubService.setClubId(club.getId(), id)); } @@ -72,21 +79,24 @@ public class KeycloakService { public Uni getUserFromMember(MembreModel membreModel) { if (membreModel.getUserId() == null) { - return Uni.createFrom().failure(new NullPointerException("No keycloak user linked to the user id=" + membreModel.getId())); + return Uni.createFrom() + .failure(new DInternalError("No keycloak user linked to the user id=" + membreModel.getId())); } return Uni.createFrom().item(membreModel::getUserId); } public Uni setClubGroupMembre(MembreModel membreModel, ClubModel club) { return getGroupFromClub(club).chain( - clubId -> getUserFromMember(membreModel).chain(userId -> vertx.getOrCreateContext().executeBlocking(() -> { - UserResource user = keycloak.realm(realm).users().get(userId); - user.groups().stream().filter(g -> g.getPath().startsWith("/club")).forEach(g -> user.leaveGroup(g.getId())); - user.joinGroup(clubId); - LOGGER.infof("Set club \"%s\" to user %s (%s)", club.getName(), userId, - user.toRepresentation().getUsername()); - return "OK"; - }))); + clubId -> getUserFromMember(membreModel).chain( + userId -> vertx.getOrCreateContext().executeBlocking(() -> { + UserResource user = keycloak.realm(realm).users().get(userId); + user.groups().stream().filter(g -> g.getPath().startsWith("/club")) + .forEach(g -> user.leaveGroup(g.getId())); + user.joinGroup(clubId); + LOGGER.infof("Set club \"%s\" to user %s (%s)", club.getName(), userId, + user.toRepresentation().getUsername()); + return "OK"; + }))); } public Uni setEmail(String userId, String email) { @@ -104,13 +114,14 @@ public class KeycloakService { public Uni setAutoRoleMembre(String id, RoleAsso role, GradeArbitrage gradeArbitrage) { List toRemove = new ArrayList<>(List.of("club_president", "club_tresorier", "club_secretaire", - "asseseur", "arbitre")); + "club_respo_intra", "asseseur", "arbitre")); List toAdd = new ArrayList<>(); switch (role) { - case PRESIDENT -> toAdd.add("club_president"); - case TRESORIER -> toAdd.add("club_tresorier"); - case SECRETAIRE -> toAdd.add("club_secretaire"); + case PRESIDENT, VPRESIDENT -> toAdd.add("club_president"); + case TRESORIER, VTRESORIER -> toAdd.add("club_tresorier"); + case SECRETAIRE, VSECRETAIRE -> toAdd.add("club_secretaire"); + case MEMBREBUREAU -> toAdd.add("club_respo_intra"); } switch (gradeArbitrage) { case ARBITRE -> toAdd.addAll(List.of("asseseur", "arbitre")); @@ -132,7 +143,8 @@ public class KeycloakService { public Uni> fetchRole(String id) { return vertx.getOrCreateContext().executeBlocking(() -> - keycloak.realm(realm).users().get(id).roles().realmLevel().listEffective().stream().map(RoleRepresentation::getName).toList()); + keycloak.realm(realm).users().get(id).roles().realmLevel().listEffective().stream() + .map(RoleRepresentation::getName).toList()); } public Uni updateRole(String id, List toAdd, List toRemove) { @@ -184,17 +196,19 @@ public class KeycloakService { RequiredAction.UPDATE_PASSWORD.name())); try (Response response = keycloak.realm(realm).users().create(user)) { - if (!response.getStatusInfo().equals(Response.Status.CREATED) && !response.getStatusInfo().equals(Response.Status.CONFLICT)) + if (!response.getStatusInfo().equals(Response.Status.CREATED) && !response.getStatusInfo() + .equals(Response.Status.CONFLICT)) throw new KeycloakException("Fail to creat user %s (reason=%s)".formatted(login, response.getStatusInfo().getReasonPhrase())); } String finalLogin = login; - return getUser(login).orElseThrow(() -> new KeycloakException("Fail to fetch user %s".formatted(finalLogin))); + return getUser(login).orElseThrow( + () -> new KeycloakException("Fail to fetch user %s".formatted(finalLogin))); }) - .invoke(user -> keycloak.realm(realm).users().get(user.getId()) - .executeActionsEmail(List.of(RequiredAction.VERIFY_EMAIL.name(), - RequiredAction.UPDATE_PASSWORD.name()))) + //.invoke(user -> keycloak.realm(realm).users().get(user.getId()) // TODO enable for production + // .executeActionsEmail(List.of(RequiredAction.VERIFY_EMAIL.name(), + // RequiredAction.UPDATE_PASSWORD.name()))) .invoke(user -> membreModel.setUserId(user.getId())) .call(user -> membreService.setUserId(membreModel.getId(), user.getId())) .call(user -> setClubGroupMembre(membreModel, membreModel.getClub())); @@ -216,7 +230,31 @@ public class KeycloakService { }); } - private Optional getUser(String username) { + public Uni removeClubGroup(String clubId) { + return vertx.getOrCreateContext().executeBlocking(() -> { + keycloak.realm(realm).groups().group(clubId).remove(); + return null; + }); + } + + public Uni clearUser(String userId) { + List toRemove = new ArrayList<>( + List.of("club_president", "club_tresorier", "club_secretaire", "club_respo_intra")); + + return vertx.getOrCreateContext().executeBlocking(() -> { + UserResource user = keycloak.realm(realm).users().get(userId); + + RoleScopeResource resource = user.roles().realmLevel(); + List roles = keycloak.realm(realm).roles().list(); + resource.remove(roles.stream().filter(r -> toRemove.contains(r.getName())).toList()); + + user.groups().stream().filter(g -> g.getPath().startsWith("/club")) + .forEach(g -> user.leaveGroup(g.getId())); + return "OK"; + }); + } + + public Optional getUser(String username) { List users = keycloak.realm(realm).users().searchByUsername(username, true); if (users.isEmpty()) @@ -225,8 +263,19 @@ public class KeycloakService { return Optional.of(users.get(0)); } + + public Optional getUser(UUID userId) { + UserResource user = keycloak.realm(realm).users().get(userId.toString()); + if (user == null) + return Optional.empty(); + else + return Optional.of(user.toRepresentation()); + } + private String makeLogin(MembreModel model) { - return Normalizer.normalize((model.getFname().toLowerCase() + "." + model.getLname().toLowerCase()).replace(' ', '_'), Normalizer.Form.NFD) + return Normalizer.normalize( + (model.getFname().toLowerCase() + "." + model.getLname().toLowerCase()).replace(' ', '_'), + Normalizer.Form.NFD) .replaceAll("\\p{M}", ""); } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/LicenceService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/LicenceService.java index 6843358..ac99ae2 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/LicenceService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/LicenceService.java @@ -4,7 +4,11 @@ import fr.titionfire.ffsaf.data.model.LicenceModel; import fr.titionfire.ffsaf.data.model.MembreModel; import fr.titionfire.ffsaf.data.repository.CombRepository; import fr.titionfire.ffsaf.data.repository.LicenceRepository; +import fr.titionfire.ffsaf.data.repository.SequenceRepository; +import fr.titionfire.ffsaf.rest.exception.DBadRequestException; import fr.titionfire.ffsaf.rest.from.LicenceForm; +import fr.titionfire.ffsaf.utils.SecurityCtx; +import fr.titionfire.ffsaf.utils.SequenceType; import fr.titionfire.ffsaf.utils.Utils; import io.quarkus.hibernate.reactive.panache.Panache; import io.quarkus.hibernate.reactive.panache.common.WithSession; @@ -12,8 +16,6 @@ import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.unchecked.Unchecked; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; -import jakarta.ws.rs.BadRequestException; -import org.eclipse.microprofile.jwt.JsonWebToken; import org.hibernate.reactive.mutiny.Mutiny; import java.util.List; @@ -29,34 +31,52 @@ public class LicenceService { @Inject CombRepository combRepository; + @Inject + SequenceRepository sequenceRepository; + public Uni> getLicence(long id, Consumer checkPerm) { - return combRepository.findById(id).invoke(checkPerm).chain(combRepository -> Mutiny.fetch(combRepository.getLicences())); + return combRepository.findById(id).invoke(checkPerm) + .chain(combRepository -> Mutiny.fetch(combRepository.getLicences())); } - public Uni> getCurrentSaisonLicence(JsonWebToken idToken) { - if (idToken == null) + public Uni> getCurrentSaisonLicence(SecurityCtx securityCtx) { + if (securityCtx.getSubject() == null) return repository.find("saison = ?1", Utils.getSaison()).list(); - return combRepository.find("userId = ?1", idToken.getSubject()).firstResult().map(MembreModel::getClub) + return combRepository.find("userId = ?1", securityCtx.getSubject()).firstResult().map(MembreModel::getClub) .chain(clubModel -> combRepository.find("club = ?1", clubModel).list()) .chain(membres -> repository.find("saison = ?1 AND membre IN ?2", Utils.getSaison(), membres).list()); } public Uni setLicence(long id, LicenceForm form) { if (form.getId() == -1) { - return combRepository.findById(id).chain(combRepository -> { + return combRepository.findById(id).chain(membreModel -> { LicenceModel model = new LicenceModel(); - model.setMembre(combRepository); + model.setMembre(membreModel); model.setSaison(form.getSaison()); - model.setCertificate(form.isCertificate()); + model.setCertificate(form.getCertificate()); model.setValidate(form.isValidate()); - return Panache.withTransaction(() -> repository.persist(model)); + return Panache.withTransaction(() -> repository.persist(model) + .call(m -> (m.isValidate() && membreModel.getLicence() <= 0) ? + sequenceRepository.getNextValueInTransaction(SequenceType.Licence) + .invoke(i -> membreModel.setLicence(Math.toIntExact(i))) + .chain(() -> combRepository.persist(membreModel)) + : Uni.createFrom().nullItem() + )); }); } else { return repository.findById(form.getId()).chain(model -> { - model.setCertificate(form.isCertificate()); + model.setCertificate(form.getCertificate()); model.setValidate(form.isValidate()); - return Panache.withTransaction(() -> repository.persist(model)); + return Panache.withTransaction(() -> repository.persist(model) + .call(m -> m.isValidate() ? Mutiny.fetch(m.getMembre()) + .call(membreModel -> (membreModel.getLicence() <= 0) ? + sequenceRepository.getNextValueInTransaction(SequenceType.Licence) + .invoke(i -> membreModel.setLicence(Math.toIntExact(i))) + .chain(() -> combRepository.persist(membreModel)) + : Uni.createFrom().nullItem()) + : Uni.createFrom().nullItem() + )); }); } } @@ -68,20 +88,21 @@ public class LicenceService { public Uni askLicence(long id, LicenceForm form, Consumer checkPerm) { return combRepository.findById(id).invoke(checkPerm).chain(membreModel -> { if (form.getId() == -1) { - return repository.find("saison = ?1 AND membre = ?2", Utils.getSaison(), membreModel).count().invoke(Unchecked.consumer(count -> { - if (count > 0) - throw new BadRequestException(); - })).chain(__ -> combRepository.findById(id).chain(combRepository -> { - LicenceModel model = new LicenceModel(); - model.setMembre(combRepository); - model.setSaison(Utils.getSaison()); - model.setCertificate(form.isCertificate()); - model.setValidate(false); - return Panache.withTransaction(() -> repository.persist(model)); - })); + return repository.find("saison = ?1 AND membre = ?2", Utils.getSaison(), membreModel).count() + .invoke(Unchecked.consumer(count -> { + if (count > 0) + throw new DBadRequestException("Licence déjà demandée"); + })).chain(__ -> combRepository.findById(id).chain(combRepository -> { + LicenceModel model = new LicenceModel(); + model.setMembre(combRepository); + model.setSaison(Utils.getSaison()); + model.setCertificate(form.getCertificate()); + model.setValidate(false); + return Panache.withTransaction(() -> repository.persist(model)); + })); } else { return repository.findById(form.getId()).chain(model -> { - model.setCertificate(form.isCertificate()); + model.setCertificate(form.getCertificate()); return Panache.withTransaction(() -> repository.persist(model)); }); } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/MatchService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/MatchService.java new file mode 100644 index 0000000..71c6896 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/MatchService.java @@ -0,0 +1,114 @@ +package fr.titionfire.ffsaf.domain.service; + +import fr.titionfire.ffsaf.data.model.MatchModel; +import fr.titionfire.ffsaf.data.repository.CombRepository; +import fr.titionfire.ffsaf.data.repository.MatchRepository; +import fr.titionfire.ffsaf.data.repository.PouleRepository; +import fr.titionfire.ffsaf.rest.data.MatchData; +import fr.titionfire.ffsaf.utils.CompetitionSystem; +import fr.titionfire.ffsaf.utils.ScoreEmbeddable; +import fr.titionfire.ffsaf.utils.SecurityCtx; +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 java.util.List; + +@WithSession +@ApplicationScoped +public class MatchService { + + @Inject + MatchRepository repository; + + @Inject + PouleRepository pouleRepository; + + @Inject + CombRepository combRepository; + + @Inject + CompetPermService permService; + + public Uni getById(SecurityCtx securityCtx, CompetitionSystem system, Long id) { + return repository.find("systemId = ?1 AND system = ?2", id, system).firstResult() + .onItem().ifNull().failWith(() -> new RuntimeException("Match not found")) + .call(data -> permService.hasViewPerm(securityCtx, data.getPoule().getCompet())) + .map(MatchData::fromModel); + } + + public Uni> getAllByPoule(SecurityCtx securityCtx, CompetitionSystem system, Long id) { + return pouleRepository.find("systemId = ?1 AND system = ?2", id, system).firstResult() + .onItem().ifNull().failWith(() -> new RuntimeException("Poule not found")) + .call(data -> permService.hasViewPerm(securityCtx, data.getCompet())) + .chain(data -> repository.list("poule = ?1", data.getId()) + .map(o -> o.stream().map(MatchData::fromModel).toList())); + } + + public Uni addOrUpdate(SecurityCtx securityCtx, CompetitionSystem system, MatchData data) { + return repository.find("systemId = ?1 AND system = ?2", data.getId(), system).firstResult() + .chain(o -> { + if (o == null) { + return pouleRepository.find("systemId = ?1 AND system = ?2", data.getPoule(), system) + .firstResult() + .onItem().ifNull().failWith(() -> new RuntimeException("Poule not found")) + .call(o2 -> permService.hasEditPerm(securityCtx, o2.getCompet())) + .map(pouleModel -> { + MatchModel model = new MatchModel(); + + model.setId(null); + model.setSystem(system); + model.setSystemId(data.getId()); + model.setPoule(pouleModel); + return model; + }); + } else { + return pouleRepository.find("systemId = ?1 AND system = ?2", data.getPoule(), system) + .firstResult() + .onItem().ifNull().failWith(() -> new RuntimeException("Poule not found")) + .call(o2 -> permService.hasEditPerm(securityCtx, o2.getCompet())) + .map(__ -> o); + } + } + ) + .chain(o -> { + o.setC1_str(data.getC1_str()); + o.setC2_str(data.getC2_str()); + o.setPoule_ord(data.getPoule_ord()); + o.getScores().clear(); + o.getScores().addAll(data.getScores()); + + return Uni.createFrom().nullItem() + .chain(() -> (data.getC1_id() == null) ? + Uni.createFrom().nullItem() : combRepository.findById(data.getC1_id())) + .invoke(o::setC1_id) + .chain(() -> (data.getC1_id() == null) ? + Uni.createFrom().nullItem() : combRepository.findById(data.getC2_id())) + .invoke(o::setC2_id) + .chain(() -> Panache.withTransaction(() -> repository.persist(o))); + }) + .map(MatchData::fromModel); + } + + public Uni updateScore(SecurityCtx securityCtx, CompetitionSystem system, Long id, + List scores) { + return repository.find("systemId = ?1 AND system = ?2", id, system).firstResult() + .onItem().ifNull().failWith(() -> new RuntimeException("Match not found")) + .call(o2 -> permService.hasEditPerm(securityCtx, o2.getPoule().getCompet())) + .invoke(data -> { + data.getScores().clear(); + data.getScores().addAll(scores); + }) + .chain(data -> Panache.withTransaction(() -> repository.persist(data))) + .map(o -> "OK"); + } + + public Uni delete(SecurityCtx securityCtx, CompetitionSystem system, Long id) { + return repository.find("systemId = ?1 AND system = ?2", id, system).firstResult() + .onItem().ifNull().failWith(() -> new RuntimeException("Match not found")) + .call(o2 -> permService.hasEditPerm(securityCtx, o2.getPoule().getCompet())) + .chain(data -> Panache.withTransaction(() -> repository.delete(data))); + } +} 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 c545e09..672f059 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java @@ -1,6 +1,12 @@ package fr.titionfire.ffsaf.domain.service; +import com.lowagie.text.*; +import com.lowagie.text.pdf.BaseFont; +import com.lowagie.text.pdf.PdfPCell; +import com.lowagie.text.pdf.PdfPTable; +import com.lowagie.text.pdf.PdfWriter; import fr.titionfire.ffsaf.data.model.ClubModel; +import fr.titionfire.ffsaf.data.model.LicenceModel; import fr.titionfire.ffsaf.data.model.MembreModel; import fr.titionfire.ffsaf.data.repository.ClubRepository; import fr.titionfire.ffsaf.data.repository.CombRepository; @@ -8,7 +14,12 @@ 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.MeData; +import fr.titionfire.ffsaf.rest.data.SimpleLicence; import fr.titionfire.ffsaf.rest.data.SimpleMembre; +import fr.titionfire.ffsaf.rest.exception.DBadRequestException; +import fr.titionfire.ffsaf.rest.exception.DForbiddenException; +import fr.titionfire.ffsaf.rest.exception.DNotFoundException; import fr.titionfire.ffsaf.rest.from.ClubMemberForm; import fr.titionfire.ffsaf.rest.from.FullMemberForm; import fr.titionfire.ffsaf.utils.*; @@ -17,15 +28,22 @@ import io.quarkus.hibernate.reactive.panache.PanacheQuery; import io.quarkus.hibernate.reactive.panache.common.WithSession; import io.quarkus.panache.common.Page; import io.quarkus.panache.common.Sort; -import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.vertx.VertxContextSupport; import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.unchecked.Unchecked; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; -import jakarta.ws.rs.BadRequestException; -import jakarta.ws.rs.ForbiddenException; -import org.eclipse.microprofile.jwt.JsonWebToken; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.hibernate.reactive.mutiny.Mutiny; + +import java.io.*; +import java.nio.file.Files; +import java.text.SimpleDateFormat; +import java.util.List; +import java.util.Objects; @WithSession @@ -44,6 +62,9 @@ public class MembreService { @Inject KeycloakService keycloakService; + @ConfigProperty(name = "upload_dir") + String media; + public SimpleCombModel find(int licence, String np) throws Throwable { return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(() -> repository.find("licence = ?1 AND (lname ILIKE ?2 OR fname ILIKE ?2)", @@ -51,7 +72,8 @@ public class MembreService { } public SimpleCombModel findByIdOptionalComb(long id) throws Throwable { - return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(() -> repository.findById(id).map(SimpleCombModel::fromModel))); + return VertxContextSupport.subscribeAndAwait( + () -> Panache.withTransaction(() -> repository.findById(id).map(SimpleCombModel::fromModel))); } public Uni> searchAdmin(int limit, int page, String search, String club) { @@ -78,7 +100,8 @@ public class MembreService { return repository.find("userId = ?1", subject).firstResult() .chain(membreModel -> { PanacheQuery query = repository.find("club = ?1 AND (lname LIKE ?2 OR fname LIKE ?2)", - Sort.ascending("fname", "lname"), membreModel.getClub(), finalSearch).page(Page.ofSize(limit)); + Sort.ascending("fname", "lname"), membreModel.getClub(), finalSearch) + .page(Page.ofSize(limit)); return getPageResult(query, limit, page); }); } @@ -90,7 +113,7 @@ public class MembreService { .call(result -> query.count().invoke(result::setResult_count)) .call(result -> query.pageCount() .invoke(Unchecked.consumer(pages -> { - if (page > pages) throw new BadRequestException(); + if (page > pages) throw new DBadRequestException("Page out of range"); })) .invoke(result::setPage_count)) .call(result -> query.page(Page.of(page, limit)).list() @@ -102,13 +125,23 @@ public class MembreService { return repository.findById(id); } + public Uni getByIdWithLicence(long id) { + return repository.findById(id) + .call(m -> Mutiny.fetch(m.getLicences())); + } + + public Uni getByLicence(long licence) { + return repository.find("licence = ?1", licence).firstResult(); + } + public Uni update(long id, FullMemberForm membre) { return repository.findById(id) - .chain(membreModel -> clubRepository.findById(membre.getClub()).map(club -> new Pair<>(membreModel, club))) + .chain(membreModel -> clubRepository.findById(membre.getClub()) + .map(club -> new Pair<>(membreModel, club))) .onItem().transformToUni(pair -> { MembreModel m = pair.getKey(); m.setFname(membre.getFname()); - m.setLname(membre.getLname()); + m.setLname(membre.getLname().toUpperCase()); m.setClub(pair.getValue()); m.setCountry(membre.getCountry()); m.setBirth_date(membre.getBirth_date()); @@ -119,49 +152,56 @@ public class MembreService { m.setEmail(membre.getEmail()); return Panache.withTransaction(() -> repository.persist(m)); }) - .invoke(membreModel -> SReqComb.sendIfNeed(serverCustom.clients, SimpleCombModel.fromModel(membreModel))) + .invoke(membreModel -> SReqComb.sendIfNeed(serverCustom.clients, + SimpleCombModel.fromModel(membreModel))) .call(membreModel -> (membreModel.getUserId() != null) ? - keycloakService.setClubGroupMembre(membreModel, membreModel.getClub()) : Uni.createFrom().nullItem()) + ((membreModel.getClub() != null) ? + keycloakService.setClubGroupMembre(membreModel, membreModel.getClub()) : + keycloakService.clearUser(membreModel.getUserId())) + : Uni.createFrom().nullItem()) .call(membreModel -> (membreModel.getUserId() != null) ? keycloakService.setAutoRoleMembre(membreModel.getUserId(), membreModel.getRole(), membreModel.getGrade_arbitrage()) : Uni.createFrom().nullItem()) .call(membreModel -> (membreModel.getUserId() != null) ? - keycloakService.setEmail(membreModel.getUserId(), membreModel.getEmail()) : Uni.createFrom().nullItem()) + keycloakService.setEmail(membreModel.getUserId(), membreModel.getEmail()) : Uni.createFrom() + .nullItem()) .map(__ -> "OK"); } - public Uni update(long id, ClubMemberForm membre, JsonWebToken idToken, SecurityIdentity securityIdentity) { + public Uni update(long id, ClubMemberForm membre, SecurityCtx securityCtx) { return repository.findById(id) .invoke(Unchecked.consumer(membreModel -> { - if (!GroupeUtils.isInClubGroup(membreModel.getClub().getId(), idToken)) - throw new ForbiddenException(); + if (!securityCtx.isInClubGroup(membreModel.getClub().getId())) + throw new DForbiddenException(); })) .invoke(Unchecked.consumer(membreModel -> { RoleAsso source = RoleAsso.MEMBRE; - if (securityIdentity.getRoles().contains("club_president")) source = RoleAsso.PRESIDENT; - else if (securityIdentity.getRoles().contains("club_secretaire")) source = RoleAsso.SECRETAIRE; - else if (securityIdentity.getRoles().contains("club_respo_intra")) source = RoleAsso.SECRETAIRE; - if (!membre.getRole().equals(membreModel.getRole()) && membre.getRole().level > source.level) - throw new ForbiddenException(); + if (securityCtx.roleHas("club_president")) source = RoleAsso.PRESIDENT; + else if (securityCtx.roleHas("club_secretaire")) source = RoleAsso.SECRETAIRE; + else if (securityCtx.roleHas("club_respo_intra")) source = RoleAsso.MEMBREBUREAU; + if (!membre.getRole().equals(membreModel.getRole()) && membre.getRole().level >= source.level) + throw new DForbiddenException("Permission insuffisante"); })) .onItem().transformToUni(target -> { target.setFname(membre.getFname()); - target.setLname(membre.getLname()); + target.setLname(membre.getLname().toUpperCase()); target.setCountry(membre.getCountry()); target.setBirth_date(membre.getBirth_date()); target.setGenre(membre.getGenre()); target.setCategorie(membre.getCategorie()); target.setEmail(membre.getEmail()); - if (!idToken.getSubject().equals(target.getUserId())) + if (!securityCtx.getSubject().equals(target.getUserId())) target.setRole(membre.getRole()); return Panache.withTransaction(() -> repository.persist(target)); }) - .invoke(membreModel -> SReqComb.sendIfNeed(serverCustom.clients, SimpleCombModel.fromModel(membreModel))) + .invoke(membreModel -> SReqComb.sendIfNeed(serverCustom.clients, + SimpleCombModel.fromModel(membreModel))) .call(membreModel -> (membreModel.getUserId() != null) ? keycloakService.setAutoRoleMembre(membreModel.getUserId(), membreModel.getRole(), membreModel.getGrade_arbitrage()) : Uni.createFrom().nullItem()) .call(membreModel -> (membreModel.getUserId() != null) ? - keycloakService.setEmail(membreModel.getUserId(), membreModel.getEmail()) : Uni.createFrom().nullItem()) + keycloakService.setEmail(membreModel.getUserId(), membreModel.getEmail()) : Uni.createFrom() + .nullItem()) .map(__ -> "OK"); } @@ -171,7 +211,8 @@ public class MembreService { MembreModel model = getMembreModel(input, clubModel); return Panache.withTransaction(() -> repository.persist(model)); }) - .invoke(membreModel -> SReqComb.sendIfNeedAdd(serverCustom.clients, SimpleCombModel.fromModel(membreModel))) + .invoke(membreModel -> SReqComb.sendIfNeedAdd(serverCustom.clients, + SimpleCombModel.fromModel(membreModel))) .map(MembreModel::getId); } @@ -183,7 +224,8 @@ public class MembreService { model.setGrade_arbitrage(GradeArbitrage.NA); return Panache.withTransaction(() -> repository.persist(model)); }) - .invoke(membreModel -> SReqComb.sendIfNeedAdd(serverCustom.clients, SimpleCombModel.fromModel(membreModel))) + .invoke(membreModel -> SReqComb.sendIfNeedAdd(serverCustom.clients, + SimpleCombModel.fromModel(membreModel))) .map(MembreModel::getId); } @@ -196,21 +238,22 @@ public class MembreService { .map(__ -> "Ok"); } - public Uni delete(long id, JsonWebToken idToken) { + public Uni delete(long id, SecurityCtx securityCtx) { return repository.findById(id) .invoke(Unchecked.consumer(membreModel -> { - if (!GroupeUtils.isInClubGroup(membreModel.getClub().getId(), idToken)) - throw new ForbiddenException(); + if (!securityCtx.isInClubGroup(membreModel.getClub().getId())) + throw new DForbiddenException(); })) .call(membreModel -> licenceRepository.find("membre = ?1", membreModel).count() .invoke(Unchecked.consumer(l -> { if (l > 0) - throw new BadRequestException(); + throw new DBadRequestException("Impossible de supprimer un membre avec des licences"); }))) .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)) + .call(__ -> Utils.deleteMedia(id, media, "ppMembre")) .map(__ -> "Ok"); } @@ -235,4 +278,221 @@ public class MembreService { model.setGrade_arbitrage(input.getGrade_arbitrage()); return model; } + + public Uni> getSimilar(String fname, String lname) { + return repository.listAll().map(membreModels -> membreModels.stream() + .filter(m -> StringSimilarity.similarity(m.getFname(), fname) <= 3 && + StringSimilarity.similarity(m.getLname(), lname) <= 3) + .map(SimpleMembre::fromModel).toList()); + } + + public Uni getMembre(String subject) { + MeData meData = new MeData(); + return repository.find("userId = ?1", subject).firstResult() + .invoke(meData::setMembre) + .chain(membreModel -> Mutiny.fetch(membreModel.getLicences())) + .map(licences -> licences.stream().map(SimpleLicence::fromModel).toList()) + .invoke(meData::setLicences) + .map(__ -> meData); + } + + public Uni getLicencePdf(String subject) { + return getLicencePdf(repository.find("userId = ?1", subject).firstResult() + .call(m -> Mutiny.fetch(m.getLicences()))); + } + + public Uni getLicencePdf(Uni uniBase) { + return uniBase + .map(Unchecked.function(m -> { + LicenceModel licence = m.getLicences().stream() + .filter(licenceModel -> licenceModel.getSaison() == Utils.getSaison() && licenceModel.isValidate()) + .findFirst() + .orElseThrow(() -> new DNotFoundException("Pas de licence pour la saison en cours")); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + make_pdf(m, out, licence); + + byte[] buff = out.toByteArray(); + String mimeType = "application/pdf"; + + Response.ResponseBuilder resp = Response.ok(buff); + resp.type(MediaType.APPLICATION_OCTET_STREAM); + resp.header(HttpHeaders.CONTENT_LENGTH, buff.length); + resp.header(HttpHeaders.CONTENT_TYPE, mimeType); + resp.header(HttpHeaders.CONTENT_DISPOSITION, + "inline; " + "filename=\"Attestation d'adhésion " + Utils.getSaison() + "-" + + (Utils.getSaison() + 1) + " de " + m.getLname() + " " + m.getFname() + ".pdf\""); + return resp.build(); + } catch (Exception e) { + throw new IOException(e); + } + })); + } + + private void make_pdf(MembreModel m, ByteArrayOutputStream out, LicenceModel licence) throws IOException { + SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy"); + Document document = new Document(); + PdfWriter.getInstance(document, out); + document.open(); + + document.addCreator("FFSAF"); + document.addTitle( + "Attestation d'adhésion " + Utils.getSaison() + "-" + (Utils.getSaison() + 1) + " de " + m.getLname() + " " + m.getFname()); + document.addCreationDate(); + document.addProducer("https://www.ffsaf.fr"); + + InputStream fontStream = MembreService.class.getClassLoader().getResourceAsStream("asset/DMSans-Regular.ttf"); + if (fontStream == null) { + throw new IOException("Font file not found"); + } + BaseFont customFont = BaseFont.createFont("asset/DMSans-Regular.ttf", BaseFont.WINANSI, BaseFont.EMBEDDED, true, + null, fontStream.readAllBytes()); + + // Adding font + Font headerFont = new Font(customFont, 26, Font.BOLD); + Font subHeaderFont = new Font(customFont, 16, Font.BOLD); + Font bigFont = new Font(customFont, 18, Font.BOLD); + Font bodyFont = new Font(customFont, 15, Font.BOLD); + Font smallFont = new Font(customFont, 10, Font.NORMAL); + + // Creating the main table + PdfPTable mainTable = new PdfPTable(2); + mainTable.setWidthPercentage(100); + mainTable.setSpacingBefore(20f); + mainTable.setSpacingAfter(0f); + mainTable.setWidths(new float[]{120, 300}); + mainTable.getDefaultCell().setBorder(PdfPCell.NO_BORDER); + + // Adding logo + Image logo = Image.getInstance( + Objects.requireNonNull( + getClass().getClassLoader().getResource("asset/FFSSAF-bord-blanc-fond-transparent.png"))); + logo.scaleToFit(120, 120); + PdfPCell logoCell = new PdfPCell(logo); + logoCell.setHorizontalAlignment(Element.ALIGN_CENTER); + logoCell.setVerticalAlignment(Element.ALIGN_MIDDLE); + logoCell.setPadding(0); + logoCell.setRowspan(1); + logoCell.setBorder(PdfPCell.NO_BORDER); + mainTable.addCell(logoCell); + + // Adding header + PdfPCell headerCell = new PdfPCell(new Phrase("FEDERATION FRANCE\nSOFT ARMORED FIGHTING", headerFont)); + headerCell.setHorizontalAlignment(Element.ALIGN_CENTER); + headerCell.setVerticalAlignment(Element.ALIGN_MIDDLE); + headerCell.setBorder(PdfPCell.NO_BORDER); + mainTable.addCell(headerCell); + + document.add(mainTable); + + Paragraph addr = new Paragraph("5 place de la Barreyre\n63320 Champeix", subHeaderFont); + addr.setAlignment(Element.ALIGN_CENTER); + addr.setSpacingAfter(2f); + document.add(addr); + + Paragraph association = new Paragraph("Association loi 1901 W633001595\nSIRET 829 458 355 00015", smallFont); + association.setAlignment(Element.ALIGN_CENTER); + document.add(association); + + // Adding spacing + document.add(new Paragraph("\n\n")); + + // Adding attestation + PdfPTable attestationTable = new PdfPTable(1); + attestationTable.setWidthPercentage(60); + PdfPCell attestationCell = new PdfPCell( + new Phrase("ATTESTATION D'ADHESION\nSaison " + Utils.getSaison() + "-" + (Utils.getSaison() + 1), + bigFont)); + attestationCell.setHorizontalAlignment(Element.ALIGN_CENTER); + attestationCell.setVerticalAlignment(Element.ALIGN_MIDDLE); + attestationCell.setPadding(20f); + attestationTable.addCell(attestationCell); + document.add(attestationTable); + + // Adding spacing + document.add(new Paragraph("\n\n")); + + // Adding member details table + PdfPTable memberTable = new PdfPTable(2); + memberTable.setWidthPercentage(100); + memberTable.setWidths(new float[]{130, 300}); + memberTable.getDefaultCell().setBorder(PdfPCell.NO_BORDER); + + // Adding member photo + Image memberPhoto; + FilenameFilter filter = (directory, filename) -> filename.startsWith(m.getId() + "."); + File[] files = new File(media, "ppMembre").listFiles(filter); + if (files != null && files.length > 0) { + File file = files[0]; + memberPhoto = Image.getInstance(Files.readAllBytes(file.toPath())); + } else { + memberPhoto = Image.getInstance( + Objects.requireNonNull(getClass().getClassLoader().getResource("asset/blank-profile-picture.png"))); + } + memberPhoto.scaleToFit(120, 150); + PdfPCell photoCell = new PdfPCell(memberPhoto); + photoCell.setHorizontalAlignment(Element.ALIGN_CENTER); + photoCell.setVerticalAlignment(Element.ALIGN_MIDDLE); + photoCell.setRowspan(5); + photoCell.setBorder(PdfPCell.NO_BORDER); + memberTable.addCell(photoCell); + + // Adding member details + memberTable.addCell(new Phrase("NOM : " + m.getLname().toUpperCase(), bodyFont)); + memberTable.addCell(new Phrase("Prénom : " + m.getFname(), bodyFont)); + memberTable.addCell(new Phrase("Licence n° : " + m.getLicence(), bodyFont)); + memberTable.addCell(new Phrase("Certificat médical par Dr " + licence.getCertificate(), bodyFont)); + memberTable.addCell(new Phrase("")); // Empty cell for spacing + + document.add(memberTable); + + // Adding spacing + document.add(new Paragraph("\n")); + + Paragraph memberClub = new Paragraph("CLUB : " + m.getClub().getName().toUpperCase(), bodyFont); + document.add(memberClub); + + Paragraph memberClubNumber = new Paragraph("N° club : " + m.getClub().getNo_affiliation(), bodyFont); + document.add(memberClubNumber); + + // Adding spacing + document.add(new Paragraph("\n")); + + Paragraph memberBirthdate = new Paragraph( + "Date de naissance : " + ((m.getBirth_date() == null) ? "--" : sdf.format(m.getBirth_date())), + bodyFont); + document.add(memberBirthdate); + + Paragraph memberGender = new Paragraph("Sexe : " + m.getGenre().str, bodyFont); + document.add(memberGender); + + Paragraph memberAgeCategory = new Paragraph("Catégorie d'âge : " + m.getCategorie().getName(), bodyFont); + document.add(memberAgeCategory); + + // Adding spacing + document.add(new Paragraph("\n\n")); + + // Adding attestation text + PdfPTable textTable = new PdfPTable(1); + textTable.setWidthPercentage(100); + PdfPCell textCell = new PdfPCell(new Phrase( + """ + Ce document atteste que l’adhérent + - est valablement enregistré auprès de la FFSAF, + - est assuré dans sa pratique du Béhourd Léger et du Battle Arc en entraînement et en compétition. + + Il peut donc s’inscrire à tout tournoi organisé sous l’égide de la FFSAF s’il remplit les éventuelles + conditions de qualification. + Il peut participer à tout entraînement dans un club affilié si celui ci autorise les visiteurs.""", + smallFont)); + textCell.setHorizontalAlignment(Element.ALIGN_LEFT); + textCell.setVerticalAlignment(Element.ALIGN_MIDDLE); + textCell.setBorder(PdfPCell.NO_BORDER); + textTable.addCell(textCell); + document.add(textTable); + + // Close the document + document.close(); + } } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/PouleService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/PouleService.java new file mode 100644 index 0000000..82a72d4 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/PouleService.java @@ -0,0 +1,284 @@ +package fr.titionfire.ffsaf.domain.service; + +import fr.titionfire.ffsaf.data.model.MatchModel; +import fr.titionfire.ffsaf.data.model.MembreModel; +import fr.titionfire.ffsaf.data.model.PouleModel; +import fr.titionfire.ffsaf.data.model.TreeModel; +import fr.titionfire.ffsaf.data.repository.*; +import fr.titionfire.ffsaf.rest.data.PouleData; +import fr.titionfire.ffsaf.rest.data.PouleFullData; +import fr.titionfire.ffsaf.rest.data.TreeData; +import fr.titionfire.ffsaf.utils.CompetitionSystem; +import fr.titionfire.ffsaf.utils.SecurityCtx; +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.hibernate.reactive.mutiny.Mutiny; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +@WithSession +@ApplicationScoped +public class PouleService { + + @Inject + PouleRepository repository; + + @Inject + CompetitionRepository competRepository; + + @Inject + MatchRepository matchRepository; + + @Inject + TreeRepository treeRepository; + + @Inject + CombRepository combRepository; + + @Inject + CompetPermService permService; + + public Uni getById(SecurityCtx securityCtx, CompetitionSystem system, Long id) { + return repository.find("systemId = ?1 AND system = ?2", id, system) + .firstResult() + .onItem().ifNull().failWith(() -> new RuntimeException("Poule not found")) + .call(data -> permService.hasViewPerm(securityCtx, data.getCompet())) + .map(PouleData::fromModel); + } + + public Uni> getAll(SecurityCtx securityCtx, CompetitionSystem system) { + return repository.list("system = ?1", system) + .chain(o -> + permService.getAllHaveAccess(securityCtx.getSubject()) + .chain(map -> Uni.createFrom().item(o.stream() + .filter(p -> { + if (securityCtx.getSubject().equals(p.getCompet().getOwner())) + return true; + if (p.getSystem() == CompetitionSystem.SAFCA) { + if (map.containsKey(p.getCompet().getId())) + return map.get(p.getId()).equals("admin"); + return securityCtx.roleHas("federation_admin") + || securityCtx.roleHas("safca_super_admin"); + } + return securityCtx.roleHas("federation_admin"); + }) + .map(PouleData::fromModel).toList()) + )); + } + + public Uni addOrUpdate(SecurityCtx securityCtx, CompetitionSystem system, PouleData data) { + return repository.find("systemId = ?1 AND system = ?2", data.getId(), system).firstResult() + .chain(o -> { + if (o == null) { + return competRepository.findById(data.getCompet()) + .onItem().ifNull().failWith(() -> new RuntimeException("Competition not found")) + .call(o2 -> permService.hasEditPerm(securityCtx, o2)) + .chain(competitionModel -> { + PouleModel model = new PouleModel(); + + model.setId(null); + model.setSystem(system); + model.setSystemId(data.getId()); + model.setCompet(competitionModel); + model.setName(data.getName()); + model.setMatchs(new ArrayList<>()); + model.setTree(new ArrayList<>()); + model.setType(data.getType()); + + return Panache.withTransaction(() -> repository.persist(model)); + }); + } else { + o.setName(data.getName()); + o.setType(data.getType()); + return Panache.withTransaction(() -> repository.persist(o)); + } + }).map(PouleData::fromModel); + } + + private MatchModel findMatch(List matchModelList, Long id) { + return matchModelList.stream().filter(m -> m.getSystemId().equals(id)) + .findFirst().orElse(null); + } + + private TreeModel findNode(List node, Long match_id) { + return node.stream().filter(m -> m.getMatch().getSystemId().equals(match_id)) + .findFirst().orElse(null); + } + + private void flatTreeChild(TreeModel current, ArrayList node) { + if (current != null) { + node.add(current); + flatTreeChild(current.getLeft(), node); + flatTreeChild(current.getRight(), node); + } + } + + private void flatTreeChild(TreeData current, ArrayList node) { + if (current != null) { + node.add(current); + flatTreeChild(current.getLeft(), node); + flatTreeChild(current.getRight(), node); + } + } + + private Uni persisteTree(TreeData data, List node, PouleModel poule, + List matchModelList) { + TreeModel mm = findNode(node, data.getMatch()); + if (mm == null) { + mm = new TreeModel(); + mm.setId(null); + } + mm.setLevel(data.getLevel()); + mm.setPoule(poule.getId()); + mm.setMatch(findMatch(matchModelList, data.getMatch())); + + return Uni.createFrom().item(mm) + .call(o -> (data.getLeft() == null ? Uni.createFrom().nullItem().invoke(o1 -> o.setLeft(null)) : + persisteTree(data.getLeft(), node, poule, matchModelList).invoke(o::setLeft))) + .call(o -> (data.getRight() == null ? Uni.createFrom().nullItem().invoke(o1 -> o.setRight(null)) : + persisteTree(data.getRight(), node, poule, matchModelList).invoke(o::setRight))) + .chain(o -> Panache.withTransaction(() -> treeRepository.persist(o))); + } + + public Uni syncPoule(SecurityCtx securityCtx, CompetitionSystem system, PouleFullData data) { + return repository.find("systemId = ?1 AND system = ?2", data.getId(), system) + .firstResult() + .onItem().ifNotNull().call(o2 -> permService.hasEditPerm(securityCtx, o2.getCompet())) + .onItem().ifNull().switchTo( + () -> competRepository.findById(data.getCompet()) + .onItem().ifNull().failWith(() -> new RuntimeException("Compet not found")) + .call(o -> permService.hasEditPerm(securityCtx, o)) + .map(o -> { + PouleModel model = new PouleModel(); + model.setId(null); + model.setSystem(system); + model.setSystemId(data.getId()); + model.setMatchs(new ArrayList<>()); + model.setTree(new ArrayList<>()); + model.setCompet(o); + return model; + })) + .call(o -> Mutiny.fetch(o.getMatchs())) + .call(o -> Mutiny.fetch(o.getTree())) + .map(o -> { + o.setName(data.getName()); + o.setType(data.getType()); + + WorkData workData = new WorkData(); + workData.poule = o; + return workData; + }) + .call(o -> Panache.withTransaction(() -> repository.persist(o.poule))) + .call(o -> (data.getMatches() == null || data.getMatches().isEmpty()) ? Uni.createFrom().nullItem() : + Uni.createFrom() + .item(data.getMatches().stream().flatMap(m -> Stream.of(m.getC1_id(), m.getC2_id()) + .filter(Objects::nonNull)).distinct().toList()) + .chain(ids -> ids.isEmpty() ? Uni.createFrom().nullItem() + : combRepository.list("id IN ?1", ids) + .invoke(o2 -> o2.forEach(m -> o.membres.put(m.getId(), m))) + ) + ) + .invoke(in -> { + ArrayList node = new ArrayList<>(); + for (TreeModel treeModel : in.poule.getTree()) + flatTreeChild(treeModel, node); + + ArrayList new_node = new ArrayList<>(); + for (TreeData treeModel : data.getTrees()) + flatTreeChild(treeModel, new_node); + + in.toRmNode = node.stream().filter(m -> new_node.stream() + .noneMatch(m2 -> m2.getMatch().equals(m.getMatch().getSystemId()))) + .map(TreeModel::getId).toList(); + + in.unlinkNode = node; + in.unlinkNode.forEach(n -> { + n.setRight(null); + n.setLeft(null); + }); + + in.toRmMatch = in.poule.getMatchs().stream() + .filter(m -> data.getMatches().stream().noneMatch(m2 -> m2.getId().equals(m.getSystemId()))) + .map(MatchModel::getId).toList(); + }) + .call(in -> in.unlinkNode.isEmpty() ? Uni.createFrom().nullItem() : + Panache.withTransaction(() -> treeRepository.persist(in.unlinkNode))) + .call(in -> in.toRmNode.isEmpty() ? Uni.createFrom().nullItem() : + Panache.withTransaction(() -> treeRepository.delete("id IN ?1", in.toRmNode))) + .call(in -> in.toRmMatch.isEmpty() ? Uni.createFrom().nullItem() : + Panache.withTransaction(() -> Uni.join().all( + in.toRmMatch.stream().map(l -> matchRepository.deleteById(l)).toList()) + .andCollectFailures())) + .call(in -> data.getMatches().isEmpty() ? Uni.createFrom().nullItem() : + Uni.join().all( + data.getMatches().stream().map(m -> { + MatchModel mm = findMatch(in.poule.getMatchs(), m.getId()); + if (mm == null) { + mm = new MatchModel(); + mm.setId(null); + mm.setSystem(system); + mm.setSystemId(m.getId()); + } + mm.setPoule(in.poule); + mm.setPoule_ord(m.getPoule_ord()); + mm.setC1_str(m.getC1_str()); + mm.setC2_str(m.getC2_str()); + mm.setC1_id(in.membres.getOrDefault(m.getC1_id(), null)); + mm.setC2_id(in.membres.getOrDefault(m.getC2_id(), null)); + mm.setEnd(m.isEnd()); + mm.setGroupe(m.getGroupe()); + mm.getScores().clear(); + mm.getScores().addAll(m.getScores()); + + MatchModel finalMm = mm; + return Panache.withTransaction(() -> matchRepository.persist(finalMm) + .invoke(o -> in.match.add(o))); + }).toList()) + .andCollectFailures()) + .call(in -> data.getTrees().isEmpty() ? Uni.createFrom().nullItem() : + Uni.join().all(data.getTrees().stream() + .map(m -> persisteTree(m, in.poule.getTree(), in.poule, in.match)).toList()) + .andCollectFailures()) + .map(__ -> "OK"); + } + + private static class WorkData { + PouleModel poule; + HashMap membres = new HashMap<>(); + List match = new ArrayList<>(); + List toRmMatch; + List unlinkNode; + List toRmNode; + } + + public Uni delete(SecurityCtx securityCtx, CompetitionSystem system, Long id) { + return repository.find("systemId = ?1 AND system = ?2", id, system).firstResult() + .onItem().ifNull().failWith(() -> new RuntimeException("Poule not found")) + .call(o -> permService.hasEditPerm(securityCtx, o.getCompet())) + .call(o -> Mutiny.fetch(o.getMatchs())) + .call(o -> Mutiny.fetch(o.getTree()) + .call(o2 -> o2.isEmpty() ? Uni.createFrom().nullItem() : + Uni.createFrom().item(o2.stream().peek(m -> { + m.setRight(null); + m.setLeft(null); + }).toList()) + .call(in -> Panache.withTransaction(() -> treeRepository.persist(in))) + .map(in -> in.stream().map(TreeModel::getId).toList()) + .call(in -> in.isEmpty() ? Uni.createFrom().nullItem() : + Panache.withTransaction(() -> treeRepository.delete("id IN ?1", in))) + ) + ) + .call(o -> o.getMatchs().isEmpty() ? Uni.createFrom().nullItem() : + Panache.withTransaction(() -> Uni.join().all( + o.getMatchs().stream().map(l -> matchRepository.deleteById(l.getId())).toList()) + .andCollectFailures())) + .chain(model -> Panache.withTransaction(() -> repository.delete("id", model.getId()))); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/TreeService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/TreeService.java new file mode 100644 index 0000000..2e10cc7 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/TreeService.java @@ -0,0 +1,4 @@ +package fr.titionfire.ffsaf.domain.service; + +public class TreeService { +} diff --git a/src/main/java/fr/titionfire/ffsaf/net2/Client_Thread.java b/src/main/java/fr/titionfire/ffsaf/net2/Client_Thread.java index 67972c5..7651c33 100644 --- a/src/main/java/fr/titionfire/ffsaf/net2/Client_Thread.java +++ b/src/main/java/fr/titionfire/ffsaf/net2/Client_Thread.java @@ -40,7 +40,7 @@ public class Client_Thread extends Thread { private boolean isAuth; - private final HashMap> waitResult = new HashMap<>(); + private final HashMap> waitResult = new HashMap<>(); public Client_Thread(ServerCustom serv, Socket s, PublicKey publicKey) throws IOException { this.serv = serv; @@ -162,7 +162,7 @@ public class Client_Thread extends Thread { sendReq(object, type, null); } - public void sendReq(Object object, String code, JsonConsumer consumer) { + public void sendReq(Object object, String code, JsonConsumer consumer) { UUID uuid; do { uuid = UUID.randomUUID(); diff --git a/src/main/java/fr/titionfire/ffsaf/net2/data/SimpleClubModel.java b/src/main/java/fr/titionfire/ffsaf/net2/data/SimpleClubModel.java index e36daef..5eb5781 100644 --- a/src/main/java/fr/titionfire/ffsaf/net2/data/SimpleClubModel.java +++ b/src/main/java/fr/titionfire/ffsaf/net2/data/SimpleClubModel.java @@ -22,6 +22,7 @@ public class SimpleClubModel { if (model == null) return null; - return new SimpleClubModel(model.getId(), model.getName(), model.getCountry(), model.getShieldURL()); + return new SimpleClubModel(model.getId(), model.getName(), model.getCountry(), + "/api/club/" + model.getClubId() + "/logo"); } } diff --git a/src/main/java/fr/titionfire/ffsaf/net2/data/SimpleCombModel.java b/src/main/java/fr/titionfire/ffsaf/net2/data/SimpleCombModel.java index 9a062ab..fb9fe39 100644 --- a/src/main/java/fr/titionfire/ffsaf/net2/data/SimpleCombModel.java +++ b/src/main/java/fr/titionfire/ffsaf/net2/data/SimpleCombModel.java @@ -8,12 +8,14 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @Getter @Setter @AllArgsConstructor @NoArgsConstructor @RegisterForReflection +@Schema(hidden = true) public class SimpleCombModel { Long id; String lname = ""; diff --git a/src/main/java/fr/titionfire/ffsaf/net2/data/SimpleCompet.java b/src/main/java/fr/titionfire/ffsaf/net2/data/SimpleCompet.java new file mode 100644 index 0000000..54707fb --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/net2/data/SimpleCompet.java @@ -0,0 +1,10 @@ +package fr.titionfire.ffsaf.net2.data; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +import java.util.List; +import java.util.UUID; + +@RegisterForReflection +public record SimpleCompet(long id, String owner, boolean show_blason, boolean show_flag, List admin, List table) { +} diff --git a/src/main/java/fr/titionfire/ffsaf/net2/packet/RFile.java b/src/main/java/fr/titionfire/ffsaf/net2/packet/RFile.java deleted file mode 100644 index 2820eb1..0000000 --- a/src/main/java/fr/titionfire/ffsaf/net2/packet/RFile.java +++ /dev/null @@ -1,42 +0,0 @@ -package fr.titionfire.ffsaf.net2.packet; - -import fr.titionfire.ffsaf.ws.FileSocket; -import jakarta.enterprise.context.ApplicationScoped; -import org.jboss.logging.Logger; - -import java.util.HashMap; -import java.util.UUID; - -@ApplicationScoped -public class RFile { - private static final Logger LOGGER = Logger.getLogger(RFile.class); - - final IAction requestSend = (client_Thread, message) -> { - try { - switch (message.data().get("type").asText()) { - case "match": - String code = UUID.randomUUID() + "-" + UUID.randomUUID(); - - FileSocket.FileRecv fileRecv = new FileSocket.FileRecv(null, message.data().get("name").asText(), null, null, - System.currentTimeMillis()); - FileSocket.sessions.put(code, fileRecv); - - client_Thread.sendRepTo(code, message); - break; - default: - client_Thread.sendErrTo("", message); - break; - - } - } catch (Throwable e) { - LOGGER.error(e.getMessage(), e); - client_Thread.sendErrTo(e.getMessage(), message); - } - }; - - public static void register(HashMap iMap) { - RFile rFile = new RFile(); - - iMap.put("requestSend", rFile.requestSend); - } -} diff --git a/src/main/java/fr/titionfire/ffsaf/net2/packet/RegisterAction.java b/src/main/java/fr/titionfire/ffsaf/net2/packet/RegisterAction.java index 3078ab9..bc7fe66 100644 --- a/src/main/java/fr/titionfire/ffsaf/net2/packet/RegisterAction.java +++ b/src/main/java/fr/titionfire/ffsaf/net2/packet/RegisterAction.java @@ -9,6 +9,5 @@ public class RegisterAction { RComb.register(iMap); RClub.register(iMap); - RFile.register(iMap); } } diff --git a/src/main/java/fr/titionfire/ffsaf/net2/request/SReqCompet.java b/src/main/java/fr/titionfire/ffsaf/net2/request/SReqCompet.java new file mode 100644 index 0000000..c7aa379 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/net2/request/SReqCompet.java @@ -0,0 +1,40 @@ +package fr.titionfire.ffsaf.net2.request; + +import fr.titionfire.ffsaf.net2.Client_Thread; +import fr.titionfire.ffsaf.net2.data.SimpleCompet; +import fr.titionfire.ffsaf.utils.JsonConsumer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.concurrent.CompletableFuture; + +public class SReqCompet { + + public static void sendUpdate(ArrayList client_Thread, SimpleCompet compet) { + for (Client_Thread client : client_Thread) { + client.sendNotify(compet, "sendConfig"); + } + } + + public static void getConfig(ArrayList client_Thread, long id_compet, + CompletableFuture future) { + if (client_Thread.isEmpty()) return; + client_Thread.get(0).sendReq(id_compet, "getConfig", + new JsonConsumer<>(SimpleCompet.class, future::complete)); + } + + public static void getAllHaveAccess(ArrayList client_Thread, String userId, + CompletableFuture> future) { + if (client_Thread.isEmpty()) return; + client_Thread.get(0).sendReq(userId, "getAllHaveAccess", + new JsonConsumer<>(HashMap.class, future::complete)); + } + + public static void rmCompet(ArrayList client_Thread, long id_compet) { + for (Client_Thread client : client_Thread) { + client.sendNotify(id_compet, "rmCompet"); + } + } + + +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/AffiliationEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/AffiliationEndpoints.java index dc2bbbc..a65e9a7 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/AffiliationEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/AffiliationEndpoints.java @@ -1,29 +1,94 @@ package fr.titionfire.ffsaf.rest; -import fr.titionfire.ffsaf.rest.from.AffiliationRequestForm; +import fr.titionfire.ffsaf.domain.service.AffiliationService; +import fr.titionfire.ffsaf.rest.data.SimpleAffiliation; +import fr.titionfire.ffsaf.rest.exception.DForbiddenException; +import fr.titionfire.ffsaf.utils.SecurityCtx; import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.unchecked.Unchecked; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.openapi.annotations.Operation; +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 java.util.List; +import java.util.function.Consumer; + +@Tag(name = "Affiliation API", description = "API pour gérer les affiliations") @Path("api/affiliation") public class AffiliationEndpoints { + @Inject + AffiliationService service; + @Inject + SecurityCtx securityCtx; + + Consumer checkPerm = Unchecked.consumer(id -> { + if (!securityCtx.roleHas("federation_admin") && !securityCtx.isInClubGroup(id)) + throw new DForbiddenException(); + }); + + @GET + @Path("/current") + @RolesAllowed({"federation_admin"}) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Renvoie les affiliations pour la saison en cours", description = "Cette méthode renvoie les affiliations pour la saison en cours. Seuls les administrateurs de la fédération peuvent accéder à cette méthode.") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Réussite"), + @APIResponse(responseCode = "403", description = "Accès refusé") + }) + public Uni> getCurrentSaisonAffiliationAdmin() { + return service.getCurrentSaisonAffiliation(); + } + + @GET + @Path("{id}") + @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Renvoie les affiliations pour un club", description = "Cette méthode renvoie les affiliations pour un club donné. Seuls les administrateurs de la fédération et les présidents, secrétaires et responsables intranet du club peuvent accéder à cette méthode.") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Réussite"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "Club non trouvé") + }) + public Uni> getAffiliation( + @Parameter(description = "L'identifiant du club") @PathParam("id") long id) { + return Uni.createFrom().item(id).invoke(checkPerm).chain(__ -> service.getAffiliation(id)); + } @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"); + @Path("{id}") + @RolesAllowed("federation_admin") + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Ajoute une affiliation pour un club", description = "Cette méthode ajoute une affiliation pour un club et une saison donné. Seuls les administrateurs de la fédération peuvent accéder à cette méthode.") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Réussite"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "Club non trouvé") + }) + public Uni setAffiliation( + @Parameter(description = "L'identifiant du club") @PathParam("id") long id, + @Parameter(description = "La saison à pour la quelle ajoute l'affiliation") @QueryParam("saison") int saison) { + return service.setAffiliation(id, saison); } - /*@POST - @Path("affiliation") + + @DELETE + @Path("{id}") + @RolesAllowed("federation_admin") @Produces(MediaType.TEXT_PLAIN) - @Consumes(MediaType.MULTIPART_FORM_DATA) - public Uni saveAffRequest(AffiliationRequestForm form) { - System.out.println(form); - return service.save(form); - }*/ + @Operation(summary = "Supprime une affiliation", description = "Cette méthode supprime l'affiliation {id}. Seuls les administrateurs de la fédération peuvent accéder à cette méthode.") + @APIResponses(value = { + @APIResponse(responseCode = "204", description = "Réussite"), + @APIResponse(responseCode = "403", description = "Accès refusé") + }) + public Uni deleteAffiliation( + @Parameter(description = "L'identifiant de l'affiliation") @PathParam("id") long id) { + return service.deleteAffiliation(id); + } } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/AffiliationRequestEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/AffiliationRequestEndpoints.java new file mode 100644 index 0000000..1ad6b57 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/AffiliationRequestEndpoints.java @@ -0,0 +1,191 @@ +package fr.titionfire.ffsaf.rest; + +import fr.titionfire.ffsaf.domain.service.AffiliationService; +import fr.titionfire.ffsaf.rest.data.SimpleReqAffiliation; +import fr.titionfire.ffsaf.rest.data.SimpleReqAffiliationResume; +import fr.titionfire.ffsaf.rest.exception.DForbiddenException; +import fr.titionfire.ffsaf.rest.from.AffiliationRequestForm; +import fr.titionfire.ffsaf.rest.from.AffiliationRequestSaveForm; +import fr.titionfire.ffsaf.utils.SecurityCtx; +import fr.titionfire.ffsaf.utils.Utils; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.unchecked.Unchecked; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.openapi.annotations.Operation; +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 java.net.URISyntaxException; +import java.util.List; +import java.util.function.Consumer; + +@Path("api/affiliation/request") +public class AffiliationRequestEndpoints { + + @Inject + AffiliationService service; + + @Inject + SecurityCtx securityCtx; + + @ConfigProperty(name = "upload_dir") + String media; + + Consumer checkPerm = Unchecked.consumer(id -> { + if (!securityCtx.roleHas("federation_admin") && !securityCtx.isInClubGroup(id)) + throw new DForbiddenException(); + }); + + @GET + @Path("") + @RolesAllowed({"federation_admin"}) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Renvoie toutes les demandes d'affiliation", description = "Cette méthode renvoie toutes les " + + "demandes d'affiliation sous forme de résumés.") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Réussite"), + @APIResponse(responseCode = "403", description = "Accès refusé") + }) + public Uni> getAllAffRequest() { + return service.getAllReq().map(o -> o.stream().map(SimpleReqAffiliationResume::fromModel).toList()); + } + + @POST + @Path("") + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Operation(summary = "Enregistre une nouvelle demande d'affiliation", description = "Cette méthode enregistre une " + + "nouvelle demande d'affiliation à partir des données soumises dans le formulaire. Ne nécessite pas d'authentification.") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Réussite"), + @APIResponse(responseCode = "400", description = "Données invalides"), + @APIResponse(responseCode = "403", description = "Accès refusé") + }) + public Uni saveAffRequest(AffiliationRequestForm form) { + return service.save(form); + } + + @GET + @Path("/{id}") + @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Renvoie une demande d'affiliation", description = "Cette méthode renvoie une demande d'affiliation " + + "pour l'identifiant spécifié.") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Réussite"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "Demande d'affiliation non trouvée") + }) + public Uni getAffRequest( + @Parameter(description = "L'identifiant de la demande d'affiliation") @PathParam("id") long id) { + return service.getRequest(id).invoke(Unchecked.consumer(o -> { + if (o.getClub() == null && !securityCtx.roleHas("federation_admin")) + throw new DForbiddenException(); + })).invoke(o -> checkPerm.accept(o.getClub())); + } + + @DELETE + @Path("/{id}") + @RolesAllowed({"federation_admin"}) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Supprime une demande d'affiliation", description = "Cette méthode supprime une demande " + + "d'affiliation pour l'identifiant spécifié.") + @APIResponses(value = { + @APIResponse(responseCode = "204", description = "Réussite"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "Demande d'affiliation non trouvée") + }) + public Uni getDelAffRequest( + @Parameter(description = "L'identifiant de la demande d'affiliation") @PathParam("id") long id) { + return service.getRequest(id).invoke(Unchecked.consumer(o -> { + if (o.getClub() == null && !securityCtx.roleHas("federation_admin")) + throw new DForbiddenException(); + })).invoke(o -> checkPerm.accept(o.getClub())) + .chain(o -> service.deleteReqAffiliation(id)); + } + + @PUT + @Path("/save") + @RolesAllowed({"federation_admin"}) + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Operation(summary = "Enregistre une demande d'affiliation en tant qu'admin", description = "Cette méthode " + + "enregistre une demande d'affiliation en tant qu'admin à partir des données soumises dans le formulaire.") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Réussite"), + @APIResponse(responseCode = "400", description = "Données invalides"), + @APIResponse(responseCode = "403", description = "Accès refusé") + }) + public Uni saveAdminAffRequest(AffiliationRequestSaveForm form) { + return service.saveAdmin(form); + } + + @PUT + @Path("/edit") + @RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"}) + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Operation(summary = "Modifie une demande d'affiliation", description = "Cette méthode modifie une demande " + + "d'affiliation à partir des données soumises dans le formulaire.") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Réussite"), + @APIResponse(responseCode = "400", description = "Données invalides"), + @APIResponse(responseCode = "403", description = "Accès refusé") + }) + public Uni saveEditAffRequest(AffiliationRequestForm form) { + return service.saveEdit(form); + } + + @PUT + @Path("/apply") + @RolesAllowed({"federation_admin"}) + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Operation(summary = "Accepte une demande d'affiliation", description = "Cette méthode accepte une demande " + + "d'affiliation à partir des données soumises dans le formulaire.") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Réussite"), + @APIResponse(responseCode = "400", description = "Données invalides"), + @APIResponse(responseCode = "403", description = "Accès refusé") + }) + public Uni acceptAffRequest(AffiliationRequestSaveForm form) { + return service.accept(form); + } + + @GET + @Path("/{id}/logo") + @RolesAllowed({"federation_admin"}) + @Operation(summary = "Renvoie le logo d'une demande d'affiliation", description = "Cette méthode renvoie le logo" + + " d'une demande d'affiliation pour l'identifiant spécifié.") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Réussite"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "Logo non trouvé") + }) + public Uni getLogo( + @Parameter(description = "L'identifiant de la demande d'affiliation") @PathParam("id") long id) throws URISyntaxException { + return Utils.getMediaFile(id, media, "aff_request/logo", Uni.createFrom().nullItem()); + } + + @GET + @Path("/{id}/status") + @RolesAllowed({"federation_admin"}) + @Operation(summary = "Renvoie le statut d'une demande d'affiliation", description = "Cette méthode renvoie le statut" + + " d'une demande d'affiliation pour l'identifiant spécifié.") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Réussite"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "Statut non trouvé") + }) + public Uni getStatus( + @Parameter(description = "L'identifiant de la demande d'affiliation") @PathParam("id") long id) throws URISyntaxException { + return Utils.getMediaFile(id, media, "aff_request/status", "affiliation_request_" + id + ".pdf", + Uni.createFrom().nullItem()); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/AssoEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/AssoEndpoints.java index 6bfa442..a7904c5 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/AssoEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/AssoEndpoints.java @@ -1,53 +1,31 @@ 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 fr.titionfire.ffsaf.rest.exception.DNotFoundException; 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.openapi.annotations.Operation; 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) + @Operation(hidden = true) public Uni getInfoSiren(@PathParam("siren") String siren) { return sirenService.get_unite(siren).onFailure().transform(throwable -> { if (throwable instanceof WebApplicationException exception) { if (exception.getResponse().getStatus() == 400) - return new BadRequestException("Not found"); + return new DNotFoundException("Siret introuvable"); } 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/AuthEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/AuthEndpoints.java index 3371743..9cb13ea 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/AuthEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/AuthEndpoints.java @@ -11,6 +11,9 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.jwt.JsonWebToken; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; import java.net.URI; import java.net.URISyntaxException; @@ -25,10 +28,16 @@ public class AuthEndpoints { SecurityIdentity securityIdentity; @Inject - JsonWebToken accessToken; + JsonWebToken IdToken; @GET @Produces(MediaType.TEXT_PLAIN) + @Operation(summary = "Vérifie si l'utilisateur est authentifié", description = "Cette méthode renvoie true si " + + "l'utilisateur est authentifié et false sinon.") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Réussite") + }) + public Boolean auth() { return !securityIdentity.isAnonymous(); } @@ -37,14 +46,21 @@ public class AuthEndpoints { @Path("/userinfo") @Authenticated @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Renvoie les informations de l'utilisateur authentifié", description = "Cette méthode renvoie les" + + " informations de l'utilisateur authentifié sous forme d'objet JSON.") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Réussite"), + @APIResponse(responseCode = "401", description = "Utilisateur non authentifié") + }) public UserInfo userinfo() { - return UserInfo.makeUserInfo(accessToken, securityIdentity); + return UserInfo.makeUserInfo(IdToken, securityIdentity); } @GET @Path("/login") @Authenticated @Produces(MediaType.TEXT_PLAIN) + @Operation(hidden = true) public Response login() throws URISyntaxException { return Response.temporaryRedirect(new URI(redirect)).build(); } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java index 20aee87..e3e473c 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java @@ -1,28 +1,318 @@ package fr.titionfire.ffsaf.rest; +import fr.titionfire.ffsaf.data.model.ClubModel; import fr.titionfire.ffsaf.domain.service.ClubService; import fr.titionfire.ffsaf.net2.data.SimpleClubModel; +import fr.titionfire.ffsaf.rest.data.DeskMember; +import fr.titionfire.ffsaf.rest.data.RenewAffData; +import fr.titionfire.ffsaf.rest.data.SimpleClub; +import fr.titionfire.ffsaf.rest.data.SimpleClubList; +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; +import fr.titionfire.ffsaf.utils.PageResult; +import fr.titionfire.ffsaf.utils.SecurityCtx; +import fr.titionfire.ffsaf.utils.Utils; import io.quarkus.security.Authenticated; import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.unchecked.Unchecked; +import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; +import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.openapi.annotations.Operation; +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 java.net.URISyntaxException; +import java.util.HashMap; import java.util.List; +import java.util.function.Consumer; +@Tag(name = "Club", description = "Gestion des clubs") @Path("api/club") public class ClubEndpoints { @Inject ClubService clubService; + @Inject + SecurityCtx securityCtx; + + @ConfigProperty(name = "upload_dir") + String media; + + Consumer checkPerm = Unchecked.consumer(clubModel -> { + if (!securityCtx.roleHas("federation_admin") && !securityCtx.isInClubGroup(clubModel.getId())) + throw new DForbiddenException(); + }); + Consumer checkPerm2 = Unchecked.consumer(id -> { + if (!securityCtx.roleHas("federation_admin") && !securityCtx.isInClubGroup(id)) + throw new DForbiddenException(); + }); + @GET @Path("/no_detail") @Authenticated @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Renvoie la liste de tous les clubs sans détails", description = "Renvoie la liste de tous les " + + "clubs sans les détails des membres et des affiliations") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "La liste de tous les clubs sans détails"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) public Uni> getAll() { return clubService.getAll().map(clubModels -> clubModels.stream().map(SimpleClubModel::fromModel).toList()); } + + @GET + @Path("/contact_type") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Renvoie les types de contacts pour les clubs", description = "Renvoie la liste des types de " + + "contacts possibles pour les clubs") + public Uni> getConcatType() { + return Uni.createFrom().item(Contact.toSite()); + } + + @GET + @Path("/find") + @RolesAllowed({"federation_admin"}) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Recherche des clubs en fonction de critères de recherche", description = "Recherche des clubs " + + "en fonction de critères de recherche tels que le nom, le pays, etc.") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "La liste des clubs correspondant aux critères de recherche"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Uni> getFindAdmin( + @Parameter(description = "Nombre max de résulta (max 50)") @QueryParam("limit") Integer limit, + @Parameter(description = "Page à consulter") @QueryParam("page") Integer page, + @Parameter(description = "Text à rechercher") @QueryParam("search") String search, + @Parameter(description = "Pays à filter") @QueryParam("country") String country) { + if (limit == null) + limit = 50; + if (page == null || page < 1) + page = 1; + return clubService.search(limit, page - 1, search, country); + } + + + @GET + @Path("{id}") + @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Renvoie les détails d'un club en fonction de son identifiant", description = "Renvoie les " + + "détails d'un club en fonction de son identifiant, y compris les informations sur les membres et les affiliations") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Les détails du club"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "Le club n'existe pas"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + 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()); + }); + } + + @PUT + @Path("{id}") + @RolesAllowed({"federation_admin"}) + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Operation(summary = "Met à jour les informations d'un club en fonction de son identifiant", description = "Met à " + + "jour les informations d'un club en fonction de son identifiant, y compris les informations sur les membres" + + " et les affiliations") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Le club a été mis à jour avec succès"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "Le club n'existe pas"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Uni setAdminClub( + @Parameter(description = "Identifiant de club") @PathParam("id") long id, FullClubForm input) { + 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); + })); + 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); + })); + else + return Uni.createFrom().nullItem(); + }); + } + + @PUT + @RolesAllowed({"federation_admin"}) + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Operation(summary = "Ajoute un nouveau club", description = "Ajoute un nouveau club avec les informations fournies" + + " dans le formulaire") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Le club a été ajouté avec succès"), + @APIResponse(responseCode = "400", description = "Les données envoyées sont invalides"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Uni addAdminClub(FullClubForm input) { + 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" + )); + else + return Uni.createFrom().nullItem(); + }).call(id -> { + if (input.getStatus().length > 0) + return Uni.createFrom().future(Utils.replacePhoto(id, input.getStatus(), media, "clubStatus" + )); + else + return Uni.createFrom().nullItem(); + }); + } + + @DELETE + @Path("{id}") + @RolesAllowed({"federation_admin"}) + @Produces(MediaType.TEXT_PLAIN) + @Operation(summary = "Supprime un club en fonction de son identifiant", description = "Supprime un club en fonction" + + " de son identifiant, ainsi que toutes les informations associées") + @APIResponses(value = { + @APIResponse(responseCode = "204", description = "Le club a été supprimé avec succès"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "Le club n'existe pas"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Uni deleteAdminClub( + @Parameter(description = "Identifiant de club") @PathParam("id") long id) { + return clubService.delete(id); + } + + @GET + @Path("/me") + @RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"}) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Renvoie les informations du club de l'utilisateur connecté", description = "Renvoie les " + + "informations du club de l'utilisateur connecté") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Les informations du club de l'utilisateur connecté"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "L'utilisateur n'est pas membre d'un club"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Uni getOfUser() { + return clubService.getOfUser(securityCtx).map(SimpleClub::fromModel) + .invoke(m -> m.setContactMap(Contact.toSite())); + } + + @PUT + @Path("/me") + @RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"}) + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Operation(summary = "Met à jour les informations du club de l'utilisateur connecté", description = "Met à jour les" + + " informations du club de l'utilisateur connecté") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Les informations du club de l'utilisateur connecté ont été mises à jour avec succès"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "L'utilisateur n'est pas membre d'un club"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Uni setClubOfUser(PartClubForm form) { + return clubService.updateOfUser(securityCtx, form); + } + + @GET + @Path("/renew/{id}") + @RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"}) + @Produces(MediaType.APPLICATION_JSON) + @Operation(hidden = true) + public Uni getRenew(@PathParam("id") long id, @QueryParam("m1") long m1_id, + @QueryParam("m2") long m2_id, @QueryParam("m3") long m3_id) { + return Uni.createFrom().item(id).invoke(checkPerm2) + .chain(__ -> clubService.getRenewData(id, List.of(m1_id, m2_id, m3_id))); + } + + @GET + @Path("/desk/{id}") + @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Renvoie la liste des membres du bureau du club", description = "Renvoie la liste des membres " + + "du bureau du club spécifié") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "La liste des membres du bureau du club"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "Le club n'existe pas"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Uni> getClubDesk( + @Parameter(description = "Identifiant de club") @PathParam("id") long id) { + return clubService.getClubDesk(checkPerm, id); + } + + @GET + @Path("{clubId}/logo") + @Operation(summary = "Renvoie le logo du club", description = "Renvoie le logo du club spécifié") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Le logo du club"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "Le club n'existe pas"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Uni getLogo( + @Parameter(description = "Identifiant long (clubId) de club") @PathParam("clubId") String clubId) { + return clubService.getByClubId(clubId).chain(Unchecked.function(clubModel -> { + try { + return Utils.getMediaFile((clubModel != null) ? clubModel.getId() : -1, media, "ppClub", + Uni.createFrom().nullItem()); + } catch (URISyntaxException e) { + throw new InternalError(); + } + })); + } + + @GET + @Path("{id}/status") + @RolesAllowed({"federation_admin", "club_president", "club_secretaire"}) + @Operation(summary = "Renvoie le statut du club", description = "Renvoie le statut du club spécifié") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Le statut du club"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "Le club n'existe pas"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Uni getStatus( + @Parameter(description = "Identifiant de club") @PathParam("id") long id) { + return clubService.getById(id).onItem().invoke(checkPerm).chain(Unchecked.function(clubModel -> { + try { + return Utils.getMediaFile(clubModel.getId(), media, "clubStatus", + "statue-" + clubModel.getName() + ".pdf", Uni.createFrom().nullItem()); + } catch (URISyntaxException e) { + throw new InternalError(); + } + })); + } + } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/CombEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/CombEndpoints.java deleted file mode 100644 index d5c5dfa..0000000 --- a/src/main/java/fr/titionfire/ffsaf/rest/CombEndpoints.java +++ /dev/null @@ -1,249 +0,0 @@ -package fr.titionfire.ffsaf.rest; - -import fr.titionfire.ffsaf.data.model.MembreModel; -import fr.titionfire.ffsaf.domain.service.MembreService; -import fr.titionfire.ffsaf.rest.data.SimpleMembre; -import fr.titionfire.ffsaf.rest.from.ClubMemberForm; -import fr.titionfire.ffsaf.rest.from.FullMemberForm; -import fr.titionfire.ffsaf.utils.GroupeUtils; -import fr.titionfire.ffsaf.utils.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; -import io.smallrye.mutiny.Uni; -import io.smallrye.mutiny.unchecked.Unchecked; -import jakarta.annotation.security.RolesAllowed; -import jakarta.inject.Inject; -import jakarta.ws.rs.*; -import jakarta.ws.rs.core.HttpHeaders; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import jodd.net.MimeTypes; -import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.eclipse.microprofile.jwt.JsonWebToken; - -import java.io.*; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLConnection; -import java.nio.file.Files; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Future; -import java.util.function.Consumer; - -@Authenticated -@Path("api/member") -public class CombEndpoints { - - @Inject - MembreService membreService; - - @ConfigProperty(name = "upload_dir") - String media; - - @Inject - @IdToken - JsonWebToken idToken; - - @Inject - SecurityIdentity securityIdentity; - - Consumer checkPerm = Unchecked.consumer(membreModel -> { - if (!securityIdentity.getRoles().contains("federation_admin") && !GroupeUtils.isInClubGroup(membreModel.getClub().getId(), idToken)) - throw new ForbiddenException(); - }); - - @GET - @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) { - if (limit == null) - limit = 50; - if (page == null || page < 1) - page = 1; - return membreService.searchAdmin(limit, page - 1, search, club); - } - - @GET - @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, - @QueryParam("search") String search) { - if (limit == null) - limit = 50; - if (page == null || page < 1) - page = 1; - return membreService.search(limit, page - 1, search, idToken.getSubject()); - } - - @GET - @Path("{id}") - @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) - @Produces(MediaType.APPLICATION_JSON) - public Uni getById(@PathParam("id") long id) { - return membreService.getById(id).onItem().invoke(checkPerm).map(SimpleMembre::fromModel); - } - - @PUT - @Path("{id}") - @RolesAllowed({"federation_admin"}) - @Produces(MediaType.TEXT_PLAIN) - @Consumes(MediaType.MULTIPART_FORM_DATA) - public Uni setAdminMembre(@PathParam("id") long id, FullMemberForm input) { - return membreService.update(id, input) - .invoke(Unchecked.consumer(out -> { - if (!out.equals("OK")) throw new InternalError("Fail to update data: " + out); - })).chain(() -> { - if (input.getPhoto_data().length > 0) - return Uni.createFrom().future(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 - return Uni.createFrom().nullItem(); - }); - } - - @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) - @Consumes(MediaType.MULTIPART_FORM_DATA) - public Uni setMembre(@PathParam("id") long id, ClubMemberForm input) { - return membreService.update(id, input, idToken, securityIdentity) - .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 InternalError("Fail to get MimeType " + out); - })); - else - return Uni.createFrom().nullItem(); - }); - } - - @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))) { - 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); - - FilenameFilter filter = (directory, filename) -> filename.startsWith(String.valueOf(id)); - File[] files = new File(media, "ppMembre").listFiles(filter); - if (files != null) { - for (File file : files) { - //noinspection ResultOfMethodCallIgnored - file.delete(); - } - } - - String extension = "." + detectedExtensions[0]; - Files.write(new File(media, "ppMembre/" + id + extension).toPath(), input); - return "OK"; - } catch (IOException e) { - return e.getMessage(); - } - }); - } - - @GET - @Path("{id}/photo") - @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) - public Uni getPhoto(@PathParam("id") long id) throws URISyntaxException { - Future> future = CompletableFuture.supplyAsync(() -> { - FilenameFilter filter = (directory, filename) -> filename.startsWith(String.valueOf(id)); - File[] files = new File(media, "ppMembre").listFiles(filter); - if (files != null && files.length > 0) { - File file = files[0]; - try { - byte[] data = Files.readAllBytes(file.toPath()); - return new Pair<>(file, data); - } catch (IOException ignored) { - } - } - return null; - }); - - URI uri = new URI("https://mdbcdn.b-cdn.net/img/Photos/new-templates/bootstrap-chat/ava2.webp"); - - return membreService.getById(id).onItem().invoke(checkPerm).chain(__ -> Uni.createFrom().future(future) - .map(filePair -> { - if (filePair == null) - return Response.temporaryRedirect(uri).build(); - - String mimeType = URLConnection.guessContentTypeFromName(filePair.getKey().getName()); - - Response.ResponseBuilder resp = Response.ok(filePair.getValue()); - resp.type(MediaType.APPLICATION_OCTET_STREAM); - resp.header(HttpHeaders.CONTENT_LENGTH, filePair.getValue().length); - resp.header(HttpHeaders.CONTENT_TYPE, mimeType); - resp.header(HttpHeaders.CONTENT_DISPOSITION, "inline; "); - - return resp.build(); - })); - } - -} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/CompetitionEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/CompetitionEndpoints.java new file mode 100644 index 0000000..3cde90e --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/CompetitionEndpoints.java @@ -0,0 +1,80 @@ +package fr.titionfire.ffsaf.rest; + +import fr.titionfire.ffsaf.domain.service.CompetitionService; +import fr.titionfire.ffsaf.rest.data.CompetitionData; +import fr.titionfire.ffsaf.rest.data.SimpleCompetData; +import fr.titionfire.ffsaf.utils.CompetitionSystem; +import fr.titionfire.ffsaf.utils.SecurityCtx; +import io.quarkus.security.Authenticated; +import io.smallrye.mutiny.Uni; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; + +import java.util.List; + +@Path("api/competition/") +public class CompetitionEndpoints { + + @Inject + CompetitionService service; + + @Inject + SecurityCtx securityCtx; + + @GET + @Path("{id}") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Uni getById(@PathParam("id") Long id) { + return service.getById(securityCtx, id); + } + + @GET + @Path("{id}/safcaData") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Uni getSafcaData(@PathParam("id") Long id) { + return service.getSafcaData(securityCtx, id); + } + + + @GET + @Path("all") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Uni> getAll() { + return service.getAll(securityCtx); + } + + @GET + @Path("all/{system}") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Uni> getAllSystem(@PathParam("system") CompetitionSystem system) { + return service.getAllSystem(securityCtx, system); + } + + @POST + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Uni addOrUpdate(CompetitionData data) { + return service.addOrUpdate(securityCtx, data); + } + + @POST + @Path("/safcaData") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Uni setSafcaData(SimpleCompetData data) { + return service.setSafcaData(securityCtx, data); + } + + @DELETE + @Path("{id}") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Uni delete(@PathParam("id") Long id) { + return service.delete(securityCtx, id); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/CompteEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/CompteEndpoints.java index d05c38d..4d08b15 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/CompteEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/CompteEndpoints.java @@ -1,21 +1,28 @@ package fr.titionfire.ffsaf.rest; import fr.titionfire.ffsaf.domain.service.KeycloakService; +import fr.titionfire.ffsaf.rest.exception.DForbiddenException; import fr.titionfire.ffsaf.rest.from.MemberPermForm; -import fr.titionfire.ffsaf.utils.GroupeUtils; import fr.titionfire.ffsaf.utils.Pair; -import io.quarkus.security.identity.SecurityIdentity; +import fr.titionfire.ffsaf.utils.SecurityCtx; import io.smallrye.mutiny.Uni; import io.vertx.mutiny.core.Vertx; import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; -import jakarta.ws.rs.*; -import org.eclipse.microprofile.jwt.JsonWebToken; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import org.eclipse.microprofile.openapi.annotations.Operation; +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.keycloak.representations.idm.GroupRepresentation; import java.util.ArrayList; import java.util.List; +@Tag(name = "Compte", description = "Gestion des comptes utilisateurs") @Path("api/compte") public class CompteEndpoints { @@ -23,10 +30,7 @@ public class CompteEndpoints { KeycloakService service; @Inject - JsonWebToken accessToken; - - @Inject - SecurityIdentity securityIdentity; + SecurityCtx securityCtx; @Inject Vertx vertx; @@ -34,11 +38,20 @@ public class CompteEndpoints { @GET @Path("{id}") @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) + @Operation(summary = "Renvoie les informations d'un compte utilisateur", description = "Renvoie les informations d'un" + + " compte utilisateur en fonction de son identifiant long (UUID)") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Les informations du compte utilisateur"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "Le compte utilisateur n'existe pas"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) public Uni getCompte(@PathParam("id") String id) { return service.fetchCompte(id).call(pair -> vertx.getOrCreateContext().executeBlocking(() -> { - if (!securityIdentity.getRoles().contains("federation_admin") && pair.getKey().groups().stream().map(GroupRepresentation::getPath) - .noneMatch(s -> s.startsWith("/club/") && GroupeUtils.contains(s, accessToken))) - throw new ForbiddenException(); + if (!securityCtx.roleHas("federation_admin") && pair.getKey().groups().stream() + .map(GroupRepresentation::getPath) + .noneMatch(s -> s.startsWith("/club/") && securityCtx.contains(s))) + throw new DForbiddenException(); return pair; })).map(Pair::getValue); } @@ -46,6 +59,14 @@ public class CompteEndpoints { @PUT @Path("{id}/init") @RolesAllowed("federation_admin") + @Operation(summary = "Initialise un compte utilisateur", description = "Initialise un compte utilisateur en fonction" + + " de son identifiant") + @APIResponses(value = { + @APIResponse(responseCode = "204", description = "Le compte utilisateur a été initialisé avec succès"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "Le membre n'existe pas"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) public Uni initCompte(@PathParam("id") long id) { return service.initCompte(id); } @@ -53,6 +74,7 @@ public class CompteEndpoints { @PUT @Path("{id}/setUUID/{nid}") @RolesAllowed("federation_admin") + @Operation(hidden = true) public Uni initCompte(@PathParam("id") long id, @PathParam("nid") String nid) { return service.setId(id, nid); } @@ -60,13 +82,29 @@ public class CompteEndpoints { @GET @Path("{id}/roles") @RolesAllowed("federation_admin") - public Uni getRole(@PathParam("id") String id) { + @Operation(summary = "Renvoie les rôles d'un compte utilisateur", description = "Renvoie les rôles d'un compte" + + " utilisateur en fonction de son identifiant") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Les rôles du compte utilisateur"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "Le compte utilisateur n'existe pas"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Uni> getRole(@PathParam("id") String id) { return service.fetchRole(id); } @PUT @Path("{id}/roles") @RolesAllowed("federation_admin") + @Operation(summary = "Met à jour les rôles d'un compte utilisateur", description = "Met à jour les rôles d'un compte" + + " utilisateur en fonction de son identifiant et des rôles fournis dans le formulaire") + @APIResponses(value = { + @APIResponse(responseCode = "204", description = "Les rôles du compte utilisateur ont été mis à jour avec succès"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "Le compte utilisateur n'existe pas"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) public Uni updateRole(@PathParam("id") String id, MemberPermForm form) { List toAdd = new ArrayList<>(); List toRemove = new ArrayList<>(); diff --git a/src/main/java/fr/titionfire/ffsaf/rest/CountriesEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/CountriesEndpoints.java new file mode 100644 index 0000000..fc49914 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/CountriesEndpoints.java @@ -0,0 +1,34 @@ +package fr.titionfire.ffsaf.rest; + +import io.smallrye.mutiny.Uni; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.openapi.annotations.Operation; + +import java.util.HashMap; +import java.util.Locale; + +@Path("api/countries") +public class CountriesEndpoints { + + @GET + @Path("/{lang}/{code}") + @Produces(MediaType.APPLICATION_JSON) + @Operation(hidden = true) + public Uni> getCountries(@PathParam("lang") String lang, @PathParam("code") String code) { + Locale locale = new Locale(lang, code); + return Uni.createFrom().item(new HashMap()) + .invoke(map -> { + String[] locales = Locale.getISOCountries(); + for (String countryCode : locales) { + if (countryCode.equals("AN")) + continue; + Locale obj = new Locale("", countryCode); + map.put(countryCode, obj.getDisplayName(locale)); + } + }); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/LicenceEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/LicenceEndpoints.java index 9e4d069..f3ddb59 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/LicenceEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/LicenceEndpoints.java @@ -3,17 +3,18 @@ package fr.titionfire.ffsaf.rest; import fr.titionfire.ffsaf.data.model.MembreModel; import fr.titionfire.ffsaf.domain.service.LicenceService; import fr.titionfire.ffsaf.rest.data.SimpleLicence; +import fr.titionfire.ffsaf.rest.exception.DForbiddenException; import fr.titionfire.ffsaf.rest.from.LicenceForm; -import fr.titionfire.ffsaf.utils.GroupeUtils; -import io.quarkus.oidc.IdToken; -import io.quarkus.security.identity.SecurityIdentity; +import fr.titionfire.ffsaf.utils.SecurityCtx; import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.unchecked.Unchecked; import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; -import org.eclipse.microprofile.jwt.JsonWebToken; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; import java.util.List; import java.util.function.Consumer; @@ -25,21 +26,25 @@ public class LicenceEndpoints { LicenceService licenceService; @Inject - @IdToken - JsonWebToken idToken; - - @Inject - SecurityIdentity securityIdentity; + SecurityCtx securityCtx; Consumer checkPerm = Unchecked.consumer(membreModel -> { - if (!securityIdentity.getRoles().contains("federation_admin") && !GroupeUtils.isInClubGroup(membreModel.getClub().getId(), idToken)) - throw new ForbiddenException(); + if (!securityCtx.roleHas("federation_admin") && !securityCtx.isInClubGroup(membreModel.getClub().getId())) + throw new DForbiddenException(); }); @GET @Path("{id}") @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Renvoie les licences d'un membre", description = "Renvoie les licences d'un membre en fonction " + + "de son identifiant") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "La liste des licences du membre"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "Le membre n'existe pas"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) public Uni> getLicence(@PathParam("id") long id) { return licenceService.getLicence(id, checkPerm).map(licenceModels -> licenceModels.stream().map(SimpleLicence::fromModel).toList()); } @@ -48,6 +53,12 @@ public class LicenceEndpoints { @Path("current/admin") @RolesAllowed({"federation_admin"}) @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Renvoie les licences de la saison en cours (pour les administrateurs)", description = "Renvoie" + + " les licences de la saison en cours (pour les administrateurs)") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "La liste des licences de la saison en cours"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) public Uni> getCurrentSaisonLicenceAdmin() { return licenceService.getCurrentSaisonLicence(null).map(licenceModels -> licenceModels.stream().map(SimpleLicence::fromModel).toList()); } @@ -56,8 +67,14 @@ public class LicenceEndpoints { @Path("current/club") @RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"}) @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Renvoie les licences de la saison en cours (pour les clubs)", description = "Renvoie les " + + "licences de la saison en cours (pour les clubs)") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "La liste des licences de la saison en cours"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) public Uni> getCurrentSaisonLicenceClub() { - return licenceService.getCurrentSaisonLicence(idToken).map(licenceModels -> licenceModels.stream().map(SimpleLicence::fromModel).toList()); + return licenceService.getCurrentSaisonLicence(securityCtx).map(licenceModels -> licenceModels.stream().map(SimpleLicence::fromModel).toList()); } @POST @@ -65,6 +82,14 @@ public class LicenceEndpoints { @RolesAllowed("federation_admin") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.MULTIPART_FORM_DATA) + @Operation(summary = "Créer une licence", description = "Créer unr licence en fonction de son identifiant et des " + + "informations fournies dans le formulaire (pour les administrateurs)") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "La licence a été mise à jour avec succès"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "La licence n'existe pas"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) public Uni setLicence(@PathParam("id") long id, LicenceForm form) { return licenceService.setLicence(id, form).map(SimpleLicence::fromModel); } @@ -73,6 +98,14 @@ public class LicenceEndpoints { @Path("{id}") @RolesAllowed("federation_admin") @Produces(MediaType.TEXT_PLAIN) + @Operation(summary = "Supprime une licence", description = "Supprime une licence en fonction de son identifiant " + + "(pour les administrateurs)") + @APIResponses(value = { + @APIResponse(responseCode = "204", description = "La licence a été supprimée avec succès"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "La licence n'existe pas"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) public Uni deleteLicence(@PathParam("id") long id) { return licenceService.deleteLicence(id); } @@ -82,6 +115,14 @@ public class LicenceEndpoints { @RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"}) @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.MULTIPART_FORM_DATA) + @Operation(summary = "Demande une nouvelle licence", description = "Demande une nouvelle licence en fonction de" + + " l'identifiant du membre et des informations fournies dans le formulaire (pour les clubs)") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "La demande de licence a été envoyée avec succès"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "Le membre n'existe pas"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) public Uni askLicence(@PathParam("id") long id, LicenceForm form) { return licenceService.askLicence(id, form, checkPerm).map(SimpleLicence::fromModel); } @@ -90,6 +131,14 @@ public class LicenceEndpoints { @Path("club/{id}") @RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"}) @Produces(MediaType.TEXT_PLAIN) + @Operation(summary = "Supprime une demande de licence", description = "Supprime une demande de licence en fonction " + + "de son identifiant (pour les clubs)") + @APIResponses(value = { + @APIResponse(responseCode = "204", description = "La demande de licence a été supprimée avec succès"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "La demande de licence n'existe pas"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) public Uni deleteAskLicence(@PathParam("id") long id) { return licenceService.deleteAskLicence(id, checkPerm); } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/MatchEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/MatchEndpoints.java new file mode 100644 index 0000000..097dac9 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/MatchEndpoints.java @@ -0,0 +1,63 @@ +package fr.titionfire.ffsaf.rest; + +import fr.titionfire.ffsaf.domain.service.MatchService; +import fr.titionfire.ffsaf.rest.data.MatchData; +import fr.titionfire.ffsaf.utils.CompetitionSystem; +import fr.titionfire.ffsaf.utils.ScoreEmbeddable; +import fr.titionfire.ffsaf.utils.SecurityCtx; +import io.quarkus.security.Authenticated; +import io.smallrye.mutiny.Uni; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; + +import java.util.List; + +@Authenticated +@Path("api/match/{system}/") +public class MatchEndpoints { + + @PathParam("system") + private CompetitionSystem system; + + @Inject + MatchService service; + + @Inject + SecurityCtx securityCtx; + + + @GET + @Path("{id}") + @Produces(MediaType.APPLICATION_JSON) + public Uni getById(@PathParam("id") Long id) { + return service.getById(securityCtx, system, id); + } + + @GET + @Path("getAllByPoule/{id}") + @Produces(MediaType.APPLICATION_JSON) + public Uni> getAllByPoule(@PathParam("id") Long id) { + return service.getAllByPoule(securityCtx, system, id); + } + + @POST + @Produces(MediaType.APPLICATION_JSON) + public Uni addOrUpdate(MatchData data) { + return service.addOrUpdate(securityCtx, system, data); + } + + @POST + @Path("score/{id}") + @Produces(MediaType.APPLICATION_JSON) + public Uni updateScore(@PathParam("id") Long id, List scores) { + return service.updateScore(securityCtx, system, id, scores); + } + + @DELETE + @Path("{id}") + @Produces(MediaType.APPLICATION_JSON) + public Uni delete(@PathParam("id") Long id) { + return service.delete(securityCtx, system, id); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/MembreAdminEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/MembreAdminEndpoints.java new file mode 100644 index 0000000..82865fd --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/MembreAdminEndpoints.java @@ -0,0 +1,144 @@ +package fr.titionfire.ffsaf.rest; + +import fr.titionfire.ffsaf.data.model.MembreModel; +import fr.titionfire.ffsaf.domain.service.MembreService; +import fr.titionfire.ffsaf.rest.data.SimpleMembre; +import fr.titionfire.ffsaf.rest.exception.DForbiddenException; +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; +import fr.titionfire.ffsaf.utils.Utils; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.unchecked.Unchecked; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.openapi.annotations.Operation; +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 java.util.List; +import java.util.function.Consumer; + +@Tag(name = "Membre admin", description = "Gestion des membres (pour les administrateurs)") +@Path("api/member") +@RolesAllowed({"federation_admin"}) +public class MembreAdminEndpoints { + + @Inject + MembreService membreService; + + @ConfigProperty(name = "upload_dir") + String media; + + @Inject + SecurityCtx securityCtx; + + Consumer checkPerm = Unchecked.consumer(membreModel -> { + if (!securityCtx.roleHas("federation_admin") && !securityCtx.isInClubGroup(membreModel.getClub().getId())) + throw new DForbiddenException(); + }); + + @GET + @Path("/find/admin") + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Recherche des membres par critères ", description = "Recherche des membres en fonction de " + + "critères tels que le nom, le prénom, le club, etc. ") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "La liste des membres correspondant aux critères de recherche"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Uni> getFindAdmin( + @Parameter(description = "Nombre max de résulta (max 50)") @QueryParam("limit") Integer limit, + @Parameter(description = "Page à consulter") @QueryParam("page") Integer page, + @Parameter(description = "Text à rechercher") @QueryParam("search") String search, + @Parameter(description = "Club à filter") @QueryParam("club") String club) { + if (limit == null) + limit = 50; + if (page == null || page < 1) + page = 1; + return membreService.searchAdmin(limit, page - 1, search, club); + } + + @GET + @Path("/find/similar") + @Produces(MediaType.APPLICATION_JSON) + @Operation(hidden = true) + public Uni> getSimilar(@QueryParam("fname") String fname, @QueryParam("lname") String lname) { + return membreService.getSimilar(fname, lname); + } + + @PUT + @Path("{id}") + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Operation(summary = "Met à jour les informations d'un membre en fonction de son identifiant", description = "Met à " + + "jour les informations d'un membre en fonction de son identifiant") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Le membre a été mis à jour avec succès"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "Le membre n'existe pas"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Uni setAdminMembre( + @Parameter(description = "Identifiant de membre") @PathParam("id") long id, FullMemberForm input) { + return membreService.update(id, input) + .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(); + }); + } + + @POST + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Operation(summary = "Ajoute un nouveau membre", description = "Ajoute un nouveau membre avec les informations " + + "fournies dans le formulaire") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Le membre a été ajouté avec succès"), + @APIResponse(responseCode = "400", description = "Les données envoyées sont invalides"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + 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}") + @Produces(MediaType.TEXT_PLAIN) + @Operation(summary = "Supprime un membre en fonction de son identifiant", description = "Supprime un membre en " + + "fonction de son identifiant, ainsi que toutes les informations associées") + @APIResponses(value = { + @APIResponse(responseCode = "204", description = "Le membre a été supprimé avec succès"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "Le membre n'existe pas"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Uni deleteAdminMembre( + @Parameter(description = "Identifiant de membre") @PathParam("id") long id) { + return membreService.delete(id); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/MembreClubEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/MembreClubEndpoints.java new file mode 100644 index 0000000..aa9b49e --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/MembreClubEndpoints.java @@ -0,0 +1,126 @@ +package fr.titionfire.ffsaf.rest; + +import fr.titionfire.ffsaf.domain.service.MembreService; +import fr.titionfire.ffsaf.rest.data.SimpleMembre; +import fr.titionfire.ffsaf.rest.exception.DInternalError; +import fr.titionfire.ffsaf.rest.from.ClubMemberForm; +import fr.titionfire.ffsaf.rest.from.FullMemberForm; +import fr.titionfire.ffsaf.utils.PageResult; +import fr.titionfire.ffsaf.utils.SecurityCtx; +import fr.titionfire.ffsaf.utils.Utils; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.unchecked.Unchecked; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.openapi.annotations.Operation; +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; + +@Tag(name = "Membre club", description = "Gestion des membres (pour les clubs)") +@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"}) +@Path("api/member") +public class MembreClubEndpoints { + + @Inject + MembreService membreService; + + @ConfigProperty(name = "upload_dir") + String media; + + @Inject + SecurityCtx securityCtx; + + @GET + @Path("/find/club") + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Recherche des membres par critères", description = "Recherche des membres en " + + "fonction de critères tels que le nom, le prénom, etc.") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "La liste des membres correspondant aux critères de recherche"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Uni> getFindClub( + @Parameter(description = "Nombre max de résulta (max 50)") @QueryParam("limit") Integer limit, + @Parameter(description = "Page à consulter") @QueryParam("page") Integer page, + @Parameter(description = "Text à rechercher") @QueryParam("search") String search) { + if (limit == null) + limit = 50; + if (page == null || page < 1) + page = 1; + return membreService.search(limit, page - 1, search, securityCtx.getSubject()); + } + + @PUT + @Path("club/{id}") + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Operation(summary = "Met à jour les informations d'un membre en fonction de son identifiant", + description = "Met à jour les informations d'un membre en fonction de son identifiant") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Le membre a été mis à jour avec succès"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "Le membre n'existe pas"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Uni setMembre( + @Parameter(description = "Identifiant de membre") @PathParam("id") long id, ClubMemberForm input) { + 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(); + }); + } + + @POST + @Path("club") + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Operation(summary = "Ajoute un nouveau membre", description = "Ajoute un nouveau membre avec les informations " + + "fournies dans le formulaire") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Le membre a été ajouté avec succès"), + @APIResponse(responseCode = "400", description = "Les données envoyées sont invalides"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Uni addMembre(FullMemberForm input) { + 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(); + }); + } + + @DELETE + @Path("club/{id}") + @Produces(MediaType.TEXT_PLAIN) + @Operation(summary = "Supprime un membre en fonction de son identifiant", description = "Supprime " + + "un membre en fonction de son identifiant, ainsi que toutes les informations associées") + @APIResponses(value = { + @APIResponse(responseCode = "204", description = "Le membre a été supprimé avec succès"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "Le membre n'existe pas"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Uni deleteMembre( + @Parameter(description = "Identifiant de membre") @PathParam("id") long id) { + return membreService.delete(id, securityCtx); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/MembreEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/MembreEndpoints.java new file mode 100644 index 0000000..46ef260 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/MembreEndpoints.java @@ -0,0 +1,138 @@ +package fr.titionfire.ffsaf.rest; + +import fr.titionfire.ffsaf.data.model.MembreModel; +import fr.titionfire.ffsaf.domain.service.MembreService; +import fr.titionfire.ffsaf.rest.data.MeData; +import fr.titionfire.ffsaf.rest.data.SimpleMembre; +import fr.titionfire.ffsaf.rest.exception.DForbiddenException; +import fr.titionfire.ffsaf.utils.SecurityCtx; +import fr.titionfire.ffsaf.utils.Utils; +import io.quarkus.security.Authenticated; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.unchecked.Unchecked; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.openapi.annotations.Operation; +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 java.net.URISyntaxException; +import java.util.function.Consumer; + +@Tag(name = "Membre", description = "Gestion des membres") +@Authenticated +@Path("api/member") +public class MembreEndpoints { + + @Inject + MembreService membreService; + + @ConfigProperty(name = "upload_dir") + String media; + + @Inject + SecurityCtx securityCtx; + + Consumer checkPerm = Unchecked.consumer(membreModel -> { + if (!securityCtx.roleHas("federation_admin") && !securityCtx.isInClubGroup(membreModel.getClub().getId())) + throw new DForbiddenException(); + }); + + @GET + @Path("{id}") + @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Renvoie les détails d'un membre en fonction de son identifiant", description = "Renvoie les " + + "détails d'un membre en fonction de son identifiant") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Les détails du membre"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "Le membre n'existe pas"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Uni getById( + @Parameter(description = "Identifiant de membre") @PathParam("id") long id) { + return membreService.getById(id).onItem().invoke(checkPerm).map(SimpleMembre::fromModel); + } + + @GET + @Path("/find/licence") + @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) + @Operation(summary = "Renvoie les détails d'un membre en fonction de son numéro de licence", description = "Renvoie " + + "les détails d'un membre en fonction de son numéro de licence") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Les détails du membre"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "Le membre n'existe pas"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + @Produces(MediaType.APPLICATION_JSON) + public Uni getByLicence(@QueryParam("id") long id) { + return membreService.getByLicence(id).onItem().invoke(checkPerm).map(SimpleMembre::fromModel); + } + + @GET + @Path("me") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Renvoie les informations du membre connecté", description = "Renvoie les informations du " + + "membre connecté, y compris le club et les licences") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Les informations du membre connecté"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Uni getMe() { + return membreService.getMembre(securityCtx.getSubject()); + } + + @GET + @Path("me/licence") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Renvoie l'attestation d'adhesion du membre connecté", description = "Renvoie l'attestation d'adhesion du " + + "membre connecté, y compris le club et les licences") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "L'attestation d'adhesion"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "Le membre n'a pas de licence active"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Uni getMeLicence() { + return membreService.getLicencePdf(securityCtx.getSubject()); + } + + @GET + @Path("{id}/photo") + @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) + @Operation(summary = "Renvoie la photo d'un membre", description = "Renvoie la photo d'un membre en fonction de son identifiant") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "La photo du membre"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "Le membre n'existe pas ou n'a pas de photo"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Uni getPhoto(@PathParam("id") long id) throws URISyntaxException { + return Utils.getMediaFile(id, media, "ppMembre", membreService.getById(id).onItem().invoke(checkPerm)); + } + + @GET + @Path("{id}/licence") + @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) + @Operation(summary = "Renvoie le pdf de la licence d'un membre", description = "Renvoie le pdf de la licence d'un membre en fonction de son identifiant") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Le pdf de la licence"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "Le membre n'existe pas ou n'a pas de licence active"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Uni getLicencePDF(@PathParam("id") long id) { + return membreService.getLicencePdf(membreService.getByIdWithLicence(id).onItem().invoke(checkPerm)); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/PouleEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/PouleEndpoints.java new file mode 100644 index 0000000..8bc9dc1 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/PouleEndpoints.java @@ -0,0 +1,63 @@ +package fr.titionfire.ffsaf.rest; + +import fr.titionfire.ffsaf.domain.service.PouleService; +import fr.titionfire.ffsaf.rest.data.PouleData; +import fr.titionfire.ffsaf.rest.data.PouleFullData; +import fr.titionfire.ffsaf.utils.CompetitionSystem; +import fr.titionfire.ffsaf.utils.SecurityCtx; +import io.quarkus.security.Authenticated; +import io.smallrye.mutiny.Uni; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; + +import java.util.List; + +@Authenticated +@Path("api/poule/{system}/") +public class PouleEndpoints { + + @PathParam("system") + private CompetitionSystem system; + + @Inject + PouleService service; + + @Inject + SecurityCtx securityCtx; + + + @GET + @Path("{id}") + @Produces(MediaType.APPLICATION_JSON) + public Uni getById(@PathParam("id") Long id) { + return service.getById(securityCtx, system, id); + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + public Uni> getAll() { + return service.getAll(securityCtx, system); + } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Uni addOrUpdate(PouleData data) { + return service.addOrUpdate(securityCtx, system, data); + } + + @POST + @Path("sync") + @Consumes(MediaType.APPLICATION_JSON) + public Uni syncPoule(PouleFullData data) { + return service.syncPoule(securityCtx, system, data); + } + + @DELETE + @Path("{id}") + @Produces(MediaType.APPLICATION_JSON) + public Uni delete(@PathParam("id") Long id) { + return service.delete(securityCtx, system, id); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/CompetitionData.java b/src/main/java/fr/titionfire/ffsaf/rest/data/CompetitionData.java new file mode 100644 index 0000000..eae4c16 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/CompetitionData.java @@ -0,0 +1,31 @@ +package fr.titionfire.ffsaf.rest.data; + +import fr.titionfire.ffsaf.data.model.CompetitionModel; +import fr.titionfire.ffsaf.utils.CompetitionSystem; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.Date; + +@Data +@AllArgsConstructor +@RegisterForReflection +public class CompetitionData { + private Long id; + private String name; + private String uuid; + private Date date; + private CompetitionSystem system; + private Long club; + private String clubName; + private String owner; + + public static CompetitionData fromModel(CompetitionModel model) { + if (model == null) + return null; + + return new CompetitionData(model.getId(), model.getName(), model.getUuid(), model.getDate(), model.getSystem(), + model.getClub().getId(), model.getClub().getName(), model.getOwner()); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/DeskMember.java b/src/main/java/fr/titionfire/ffsaf/rest/data/DeskMember.java new file mode 100644 index 0000000..7bffcfc --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/DeskMember.java @@ -0,0 +1,35 @@ +package fr.titionfire.ffsaf.rest.data; + +import fr.titionfire.ffsaf.data.model.MembreModel; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Data +@NoArgsConstructor +@RegisterForReflection +@Schema(name = "BureauMembre") +public class DeskMember { + @Schema(description = "Identifiant du membre", example = "1") + private Long id; + @Schema(description = "Nom du membre", example = "Doe") + private String lname; + @Schema(description = "Prénom du membre", example = "John") + private String fname; + @Schema(description = "Rôle du membre", example = "Président") + private String role; + + public static DeskMember fromModel(MembreModel membreModel) { + if (membreModel == null) + return null; + + DeskMember deskMember = new DeskMember(); + deskMember.setId(membreModel.getId()); + deskMember.setLname(membreModel.getLname()); + deskMember.setFname(membreModel.getFname()); + deskMember.setRole(membreModel.getRole().toString()); + + return deskMember; + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/MatchData.java b/src/main/java/fr/titionfire/ffsaf/rest/data/MatchData.java new file mode 100644 index 0000000..b7c942b --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/MatchData.java @@ -0,0 +1,36 @@ +package fr.titionfire.ffsaf.rest.data; + +import fr.titionfire.ffsaf.data.model.MatchModel; +import fr.titionfire.ffsaf.utils.ScoreEmbeddable; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.List; + +@Data +@AllArgsConstructor +@RegisterForReflection +public class MatchData { + private Long id; + private Long c1_id; + private String c1_str; + private Long c2_id; + private String c2_str; + private Long poule; + private long poule_ord; + private boolean isEnd = true; + private char groupe; + private List scores; + + public static MatchData fromModel(MatchModel model) { + if (model == null) + return null; + + return new MatchData(model.getSystemId(), + (model.getC1_id() == null) ? null : model.getC1_id().getId(), model.getC1_str(), + (model.getC2_id() == null) ? null : model.getC2_id().getId(), model.getC2_str(), + model.getPoule().getId(), model.getPoule_ord(), model.isEnd(), model.getGroupe(), + model.getScores()); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/MeData.java b/src/main/java/fr/titionfire/ffsaf/rest/data/MeData.java new file mode 100644 index 0000000..a65e097 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/MeData.java @@ -0,0 +1,59 @@ +package fr.titionfire.ffsaf.rest.data; + +import fr.titionfire.ffsaf.data.model.MembreModel; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +import java.util.Date; +import java.util.List; + +@Data +@ToString +@NoArgsConstructor +@RegisterForReflection +public class MeData { + @Schema(description = "L'identifiant du membre.", example = "1") + private long id; + @Schema(description = "Le nom du membre.", example = "Dupont") + private String lname = ""; + @Schema(description = "Le prénom du membre.", example = "Jean") + private String fname = ""; + @Schema(description = "La catégorie du membre.", example = "SENIOR") + private String categorie; + @Schema(description = "Le nom du club du membre.", example = "Association sportive") + private String club; + @Schema(description = "Le genre du membre.", example = "Homme") + private String genre; + @Schema(description = "Le numéro de licence du membre.", example = "12345") + private int licence; + @Schema(description = "Le pays du membre.", example = "FR") + private String country; + @Schema(description = "La date de naissance du membre.") + private Date birth_date; + @Schema(description = "L'adresse e-mail du membre.", example = "jean.dupont@example.com") + private String email; + @Schema(description = "Le rôle du membre dans l'association.", example = "MEMBRE") + private String role; + @Schema(description = "Le grade d'arbitrage du membre.", example = "N/A") + private String grade_arbitrage; + @Schema(description = "La liste des licences du membre.") + private List licences; + + public void setMembre(MembreModel membreModel) { + this.id = membreModel.getId(); + this.lname = membreModel.getLname(); + this.fname = membreModel.getFname(); + this.categorie = membreModel.getCategorie().getName(); + this.club = membreModel.getClub().getName(); + this.genre = membreModel.getGenre().str; + this.licence = membreModel.getLicence(); + this.country = membreModel.getCountry(); + this.birth_date = membreModel.getBirth_date(); + this.email = membreModel.getEmail(); + this.role = membreModel.getRole().str; + this.grade_arbitrage = membreModel.getGrade_arbitrage().str; + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/PouleData.java b/src/main/java/fr/titionfire/ffsaf/rest/data/PouleData.java new file mode 100644 index 0000000..f89da27 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/PouleData.java @@ -0,0 +1,23 @@ +package fr.titionfire.ffsaf.rest.data; + +import fr.titionfire.ffsaf.data.model.PouleModel; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +@RegisterForReflection +public class PouleData { + private Long id; + private String name; + private Long compet; + private Integer type; + + public static PouleData fromModel(PouleModel model) { + if (model == null) + return null; + + return new PouleData(model.getSystemId(), model.getName(), model.getCompet().getId(), model.getType()); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/PouleFullData.java b/src/main/java/fr/titionfire/ffsaf/rest/data/PouleFullData.java new file mode 100644 index 0000000..b949111 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/PouleFullData.java @@ -0,0 +1,28 @@ +package fr.titionfire.ffsaf.rest.data; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.List; + +@Data +@AllArgsConstructor +public class PouleFullData { + private Long id; + private String name; + private Long compet; + private Integer type; + private List matches; + private List trees; + + /*public static PouleFullData fromModel(PouleModel pouleModel) { + if (pouleModel == null) + return null; + + PouleEntity pouleEntity = PouleEntity.fromModel(pouleModel); + + return new PouleFullData(pouleEntity.getId(), pouleEntity.getName(), pouleEntity.getCompet().getId(), + pouleEntity.getType(), pouleModel.getMatchs().stream().map(MatchData::fromModel).toList(), + pouleEntity.getTrees().stream().map(TreeData::fromEntity).toList()); + }*/ +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/RenewAffData.java b/src/main/java/fr/titionfire/ffsaf/rest/data/RenewAffData.java new file mode 100644 index 0000000..02ce7cc --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/RenewAffData.java @@ -0,0 +1,45 @@ +package fr.titionfire.ffsaf.rest.data; + +import fr.titionfire.ffsaf.data.model.MembreModel; +import fr.titionfire.ffsaf.utils.RoleAsso; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@RegisterForReflection +public class RenewAffData { + String name; + Long siret; + String rna; + String address; + int saison; + List members; + + + @Data + @AllArgsConstructor + @RegisterForReflection + public static class RenewMember { + String lname; + String fname; + String email; + int licence; + RoleAsso role; + + public RenewMember(MembreModel o) { + this.lname = o.getLname(); + this.fname = o.getFname(); + this.email = o.getEmail(); + this.licence = o.getLicence(); + this.role = o.getRole(); + } + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleAffiliation.java b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleAffiliation.java new file mode 100644 index 0000000..2631783 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleAffiliation.java @@ -0,0 +1,35 @@ +package fr.titionfire.ffsaf.rest.data; + +import fr.titionfire.ffsaf.data.model.AffiliationModel; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Data +@Builder +@AllArgsConstructor +@RegisterForReflection +public class SimpleAffiliation { + @Schema(description = "L'identifiant de l'affiliation.", example = "1") + private Long id; + @Schema(description = "L'identifiant du club associé à l'affiliation.", example = "123") + private Long club; + @Schema(description = "La saison de l'affiliation.", example = "2022") + private int saison; + @Schema(description = "Indique si l'affiliation est validée ou non.", example = "true") + private boolean validate; + + public static SimpleAffiliation fromModel(AffiliationModel model) { + if (model == null) + return null; + + return new SimpleAffiliationBuilder() + .id(model.getId()) + .club(model.getClub().getId()) + .saison(model.getSaison()) + .validate(true) + .build(); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleClub.java b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleClub.java new file mode 100644 index 0000000..6229aa6 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleClub.java @@ -0,0 +1,70 @@ +package fr.titionfire.ffsaf.rest.data; + +import fr.titionfire.ffsaf.data.model.ClubModel; +import fr.titionfire.ffsaf.utils.Contact; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.ToString; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +import java.util.HashMap; +import java.util.Map; + +@Data +@Builder +@ToString +@AllArgsConstructor +@RegisterForReflection +public class SimpleClub { + @Schema(description = "L'identifiant unique du club.", example = "1") + private Long id; + @Schema(description = "Identifiant long du club (UUID)", example = "b94f3167-3f6a-449c-a73b-ec84202bf07e") + private String clubId; + @Schema(description = "Le nom du club.", example = "Association sportive") + private String name; + @Schema(description = "Le pays du club.", example = "FR") + private String country; + @Schema(description = "Les contacts du club", example = "{\"SITE\": \"www.test.com\", \"COURRIEL\": \"test@test.com\"}") + private Map contact; + @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}]") + private String training_location; + @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}]") + private String training_day_time; + @Schema(description = "Contact interne du club", example = "john.doe@test.com") + private String contact_intern; + @Schema(description = "Adresse postale du club", example = "1 rue de l'exemple, 75000 Paris") + private String address; + @Schema(description = "RNA du club", example = "W123456789") + private String RNA; + @Schema(description = "Numéro SIRET du club", example = "12345678901234") + private Long SIRET; + @Schema(description = "Numéro d'affiliation du club", example = "12345") + private Long no_affiliation; + @Schema(description = "Club international", example = "false") + private boolean international; + @Schema(description = "Une map contenant les contacts possible pout un club.") + private HashMap contactMap = null; + + public static SimpleClub fromModel(ClubModel model) { + if (model == null) + return null; + + return new SimpleClubBuilder() + .id(model.getId()) + .clubId(model.getClubId()) + .name(model.getName()) + .country(model.getCountry()) + .contact(model.getContact()) + .training_location(model.getTraining_location()) + .training_day_time(model.getTraining_day_time()) + .contact_intern(model.getContact_intern()) + .RNA(model.getRNA()) + .SIRET(model.getSIRET()) + .no_affiliation(model.getNo_affiliation()) + .international(model.isInternational()) + .address(model.getAddress()) + .build(); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleClubList.java b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleClubList.java new file mode 100644 index 0000000..b42edb6 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleClubList.java @@ -0,0 +1,35 @@ +package fr.titionfire.ffsaf.rest.data; + +import fr.titionfire.ffsaf.data.model.ClubModel; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@RegisterForReflection +public class SimpleClubList { + @Schema(description = "Identifiant du club", example = "1") + Long id; + @Schema(description = "Nom du club", example = "Club de test") + String name; + @Schema(description = "Pays du club", example = "FR") + String country; + @Schema(description = "Numéro SIRET du club", example = "12345678901234") + Long siret; + @Schema(description = "Numéro d'affiliation du club", example = "12345") + Long no_affiliation; + + public static SimpleClubList fromModel(ClubModel model) { + if (model == null) + return null; + + return new SimpleClubList(model.getId(), model.getName(), model.getCountry(), model.getSIRET(), + model.getNo_affiliation()); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleCompetData.java b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleCompetData.java new file mode 100644 index 0000000..3adaac7 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleCompetData.java @@ -0,0 +1,28 @@ +package fr.titionfire.ffsaf.rest.data; + +import fr.titionfire.ffsaf.net2.data.SimpleCompet; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +@AllArgsConstructor +@RegisterForReflection +public class SimpleCompetData { + private long id; + private boolean show_blason; + private boolean show_flag; + private List admin; + private List table; + + public static SimpleCompetData fromModel(SimpleCompet compet) { + if (compet == null) + return null; + + return new SimpleCompetData(compet.id(), compet.show_blason(), compet.show_flag(), + new ArrayList<>(), new ArrayList<>()); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleLicence.java b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleLicence.java index c9fedce..fad08b3 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleLicence.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleLicence.java @@ -5,16 +5,22 @@ import io.quarkus.runtime.annotations.RegisterForReflection; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @Data @Builder @AllArgsConstructor @RegisterForReflection public class SimpleLicence { + @Schema(description = "ID de la licence", example = "1") Long id; + @Schema(description = "ID du membre", example = "1") Long membre; + @Schema(description = "Saison de la licence", example = "2024") int saison; - boolean certificate; + @Schema(description = "Nom du médecin sur certificat médical.", example = "M. Jean") + String certificate; + @Schema(description = "Validation de la licence", example = "true") boolean validate; public static SimpleLicence fromModel(LicenceModel model) { @@ -25,7 +31,7 @@ public class SimpleLicence { .id(model.getId()) .membre(model.getMembre().getId()) .saison(model.getSaison()) - .certificate(model.isCertificate()) + .certificate(model.getCertificate()) .validate(model.isValidate()) .build(); } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleMembre.java b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleMembre.java index dc51d65..a7e228b 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleMembre.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleMembre.java @@ -10,6 +10,7 @@ import io.quarkus.runtime.annotations.RegisterForReflection; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; import java.util.Date; @@ -18,19 +19,33 @@ import java.util.Date; @AllArgsConstructor @RegisterForReflection public class SimpleMembre { + @Schema(description = "L'identifiant du membre.", example = "1") private long id; + @Schema(description = "L'identifiant long du membre (userID).", example = "e81d1d35-d897-421e-8086-6c5e74d13c6e") private String userId; + @Schema(description = "Le nom du membre.", example = "Dupont") private String lname = ""; + @Schema(description = "Le prénom du membre.", example = "Jean") private String fname = ""; + @Schema(description = "La catégorie du membre.", example = "SENIOR") private Categorie categorie; + @Schema(description = "Le club du membre.") private SimpleClubModel club; + @Schema(description = "Le genre du membre.", example = "H") private Genre genre; + @Schema(description = "Le numéro de licence du membre.", example = "12345") private int licence; + @Schema(description = "Le pays du membre.", example = "FR") private String country; + @Schema(description = "La date de naissance du membre.") private Date birth_date; + @Schema(description = "L'adresse e-mail du membre.", example = "jean.dupont@example.com") private String email; + @Schema(description = "Le rôle du membre dans l'association.", example = "MEMBRE") private RoleAsso role; + @Schema(description = "Le grade d'arbitrage du membre.", example = "N/A") private GradeArbitrage grade_arbitrage; + @Schema(hidden = true) private String url_photo; public static SimpleMembre fromModel(MembreModel model) { diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleReqAffiliation.java b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleReqAffiliation.java new file mode 100644 index 0000000..30d683f --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleReqAffiliation.java @@ -0,0 +1,76 @@ +package fr.titionfire.ffsaf.rest.data; + +import fr.titionfire.ffsaf.data.model.AffiliationRequestModel; +import fr.titionfire.ffsaf.utils.RoleAsso; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +import java.util.List; + +@Data +@Builder +@AllArgsConstructor +@RegisterForReflection +public class SimpleReqAffiliation { + @Schema(description = "Identifiant de la demande d'affiliation", example = "1") + Long id; + @Schema(description = "Identifiant du club", example = "1") + Long club; + @Schema(description = "Nom du club si club similar trouver (même siret)", example = "Association sportive") + String club_name; + @Schema(description = "Identifiant du club affilié", example = "1") + Long club_no_aff; + @Schema(description = "Nom du club demander", example = "Association sportive") + String name; + @Schema(description = "Numéro SIRET de l'association", example = "12345678901234") + long siret; + @Schema(description = "Numéro RNA de l'association", example = "W123456789") + String RNA; + @Schema(description = "Adresse de l'association", example = "1 rue de l'exemple, 75000 Paris") + String address; + @Schema(description = "Liste des membres pour la demande d'affiliation") + List members; + @Schema(description = "Saison de l'affiliation", example = "2025") + int saison; + + public static SimpleReqAffiliation fromModel(AffiliationRequestModel model) { + if (model == null) + return null; + + return new SimpleReqAffiliation.SimpleReqAffiliationBuilder() + .id(model.getId()) + .name(model.getName()) + .siret(model.getSiret()) + .RNA(model.getRNA()) + .address(model.getAddress()) + .saison(model.getSaison()) + .members(List.of( + new AffiliationMember(model.getM1_lname(), model.getM1_fname(), model.getM1_email(), + model.getM1_lincence(), model.getM1_role()), + new AffiliationMember(model.getM2_lname(), model.getM2_fname(), model.getM2_email(), + model.getM2_lincence(), model.getM2_role()), + new AffiliationMember(model.getM3_lname(), model.getM3_fname(), model.getM3_email(), + model.getM3_lincence(), model.getM3_role()) + )) + .build(); + } + + @Data + @AllArgsConstructor + @RegisterForReflection + public static class AffiliationMember { + @Schema(description = "Nom du membre", example = "Doe") + String lname; + @Schema(description = "Prénom du membre", example = "John") + String fname; + @Schema(description = "Email du membre", example = "john.doe@test.com") + String email; + @Schema(description = "Numéro de licence du membre", example = "12345") + int licence; + @Schema(description = "Rôle du membre", example = "MEMBRE") + RoleAsso role; + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleReqAffiliationResume.java b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleReqAffiliationResume.java new file mode 100644 index 0000000..f99ace0 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleReqAffiliationResume.java @@ -0,0 +1,35 @@ +package fr.titionfire.ffsaf.rest.data; + +import fr.titionfire.ffsaf.data.model.AffiliationRequestModel; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Data +@Builder +@AllArgsConstructor +@RegisterForReflection +public class SimpleReqAffiliationResume { + @Schema(description = "L'identifiant de la demande d'affiliation.", example = "1") + Long id; + @Schema(description = "Le nom de l'association.", example = "Association sportive") + String name; + @Schema(description = "Le numéro SIRET de l'association.", example = "12345678901234") + long siret; + @Schema(description = "La saison de l'affiliation.", example = "2025") + int saison; + + public static SimpleReqAffiliationResume fromModel(AffiliationRequestModel model) { + if (model == null) + return null; + + return new SimpleReqAffiliationResume.SimpleReqAffiliationResumeBuilder() + .id(model.getId()) + .name(model.getName()) + .siret(model.getSiret()) + .saison(model.getSaison()) + .build(); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/TreeData.java b/src/main/java/fr/titionfire/ffsaf/rest/data/TreeData.java new file mode 100644 index 0000000..b4cff47 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/TreeData.java @@ -0,0 +1,26 @@ +package fr.titionfire.ffsaf.rest.data; + +import fr.titionfire.ffsaf.data.model.TreeModel; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +@RegisterForReflection +public class TreeData { + private Long id; + private Long poule; + private Integer level; + private Long match; + private TreeData left; + private TreeData right; + + public static TreeData fromModel(TreeModel model) { + if (model == null) + return null; + + return new TreeData(model.getId(), model.getPoule(), model.getLevel(), model.getMatch().getId(), + fromModel(model.getLeft()), fromModel(model.getRight())); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/UserInfo.java b/src/main/java/fr/titionfire/ffsaf/rest/data/UserInfo.java index cc8dc72..7ed707d 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/UserInfo.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/UserInfo.java @@ -5,6 +5,7 @@ import io.quarkus.security.identity.SecurityIdentity; import lombok.Builder; import lombok.Data; import org.eclipse.microprofile.jwt.JsonWebToken; +import org.eclipse.microprofile.openapi.annotations.media.Schema; import java.util.ArrayList; import java.util.List; @@ -14,14 +15,23 @@ import java.util.Set; @Builder @RegisterForReflection public class UserInfo { + @Schema(description = "L'identifiant de l'utilisateur.", example = "1234567890") String id; + @Schema(description = "Le nom complet de l'utilisateur.", example = "John Doe") String name; + @Schema(description = "Le prénom de l'utilisateur.", example = "John") String givenName; + @Schema(description = "Le nom de famille de l'utilisateur.", example = "Doe") String familyName; + @Schema(description = "L'adresse e-mail de l'utilisateur.", example = "jihn.doe@test.fr") String email; + @Schema(description = "L'adresse e-mail de l'utilisateur a été vérifiée.", example = "true") boolean emailVerified; + @Schema(description = "La date d'expiration du token d'accès.") long expiration; + @Schema(description = "La liste des groupes de l'utilisateur.") List groups; + @Schema(description = "La liste des rôles de l'utilisateur.") Set roles; public static UserInfo makeUserInfo(JsonWebToken accessToken, SecurityIdentity securityIdentity) { diff --git a/src/main/java/fr/titionfire/ffsaf/rest/exception/DBadRequestException.java b/src/main/java/fr/titionfire/ffsaf/rest/exception/DBadRequestException.java new file mode 100644 index 0000000..31a9be3 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/exception/DBadRequestException.java @@ -0,0 +1,14 @@ +package fr.titionfire.ffsaf.rest.exception; + +import jakarta.ws.rs.core.Response; + +import java.io.Serial; + +public class DBadRequestException extends DetailException { + @Serial + private static final long serialVersionUID = 7518556311032332135L; + + public DBadRequestException(String message) { + super(Response.Status.BAD_REQUEST, message); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/exception/DForbiddenException.java b/src/main/java/fr/titionfire/ffsaf/rest/exception/DForbiddenException.java new file mode 100644 index 0000000..d95bf5b --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/exception/DForbiddenException.java @@ -0,0 +1,18 @@ +package fr.titionfire.ffsaf.rest.exception; + +import jakarta.ws.rs.core.Response; + +import java.io.Serial; + +public class DForbiddenException extends DetailException { + @Serial + private static final long serialVersionUID = 8408920537659758038L; + + public DForbiddenException() { + this("Accès a la ressource interdite"); + } + + public DForbiddenException(String message) { + super(Response.Status.FORBIDDEN, message); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/exception/DInternalError.java b/src/main/java/fr/titionfire/ffsaf/rest/exception/DInternalError.java new file mode 100644 index 0000000..777fa99 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/exception/DInternalError.java @@ -0,0 +1,15 @@ +package fr.titionfire.ffsaf.rest.exception; + +import jakarta.ws.rs.core.Response; + +import java.io.Serial; + +public class DInternalError extends DetailException { + + @Serial + private static final long serialVersionUID = -3635595157694180842L; + + public DInternalError(String message) { + super(Response.Status.INTERNAL_SERVER_ERROR, message); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/exception/DNotFoundException.java b/src/main/java/fr/titionfire/ffsaf/rest/exception/DNotFoundException.java new file mode 100644 index 0000000..9baab1d --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/exception/DNotFoundException.java @@ -0,0 +1,15 @@ +package fr.titionfire.ffsaf.rest.exception; + +import jakarta.ws.rs.core.Response; + +import java.io.Serial; + +public class DNotFoundException extends DetailException{ + + @Serial + private static final long serialVersionUID = -5193524134675732376L; + + public DNotFoundException(String message) { + super(Response.Status.NOT_FOUND, message); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/exception/DetailException.java b/src/main/java/fr/titionfire/ffsaf/rest/exception/DetailException.java new file mode 100644 index 0000000..0af0f41 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/exception/DetailException.java @@ -0,0 +1,20 @@ +package fr.titionfire.ffsaf.rest.exception; + +import jakarta.ws.rs.core.Response; +import lombok.Getter; + +import java.io.Serial; + +@Getter +public class DetailException extends Exception { + + @Serial + private static final long serialVersionUID = 5349921926328753676L; + + private final Response.Status status; + + public DetailException(Response.Status status, String message) { + super(message); + this.status = status; + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/exception/DetailExceptionMapper.java b/src/main/java/fr/titionfire/ffsaf/rest/exception/DetailExceptionMapper.java new file mode 100644 index 0000000..c37f711 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/exception/DetailExceptionMapper.java @@ -0,0 +1,19 @@ +package fr.titionfire.ffsaf.rest.exception; + +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +@Provider +public class DetailExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(DetailException e) { + return Response.status(e.getStatus()) + .entity(e.getMessage()) + .type(MediaType.TEXT_PLAIN_TYPE) + .build(); + } + +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/exception/KeycloakExceptionMapper.java b/src/main/java/fr/titionfire/ffsaf/rest/exception/KeycloakExceptionMapper.java new file mode 100644 index 0000000..bb40b9d --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/exception/KeycloakExceptionMapper.java @@ -0,0 +1,20 @@ +package fr.titionfire.ffsaf.rest.exception; + +import fr.titionfire.ffsaf.utils.KeycloakException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +@Provider +public class KeycloakExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(KeycloakException e) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity("Erreur du gestionnaire d'identité: " + e.getMessage()) + .type(MediaType.TEXT_PLAIN_TYPE) + .build(); + } + +} \ No newline at end of file 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 04f002d..985510c 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestForm.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestForm.java @@ -1,86 +1,140 @@ package fr.titionfire.ffsaf.rest.from; import fr.titionfire.ffsaf.data.model.AffiliationRequestModel; +import fr.titionfire.ffsaf.utils.RoleAsso; import jakarta.ws.rs.FormParam; import jakarta.ws.rs.core.MediaType; import lombok.Getter; 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; @Getter @ToString public class AffiliationRequestForm { + @Schema(description = "L'identifiant de l'affiliation. (null si nouvelle demande d'affiliation)") + @FormParam("id") + private Long id = null; + + @Schema(description = "Le nom de l'association.", example = "Association sportive", required = true) @FormParam("name") private String name = null; - @FormParam("siren") - private String siren = null; + @Schema(description = "Le numéro SIRET de l'association.", example = "12345678901234", required = true) + @FormParam("siret") + private Long siret = null; + @Schema(description = "Le numéro RNA de l'association. (peut être null)", example = "W123456789") @FormParam("rna") private String rna = null; + @Schema(description = "L'adresse de l'association.", example = "1 rue de l'exemple, 75000 Paris", required = true) @FormParam("adresse") private String adresse = null; + @Schema(description = "La saison de l'affiliation.", example = "2025", required = true) + @FormParam("saison") + private int saison = -1; + + @Schema(description = "Le statut de l'association.", type = SchemaType.ARRAY, implementation = byte.class) @FormParam("status") @PartType(MediaType.APPLICATION_OCTET_STREAM) private byte[] status = new byte[0]; + @Schema(description = "Le logo de l'association.", type = SchemaType.ARRAY, implementation = byte.class) @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; + @Schema(description = "Le nom du premier membre de l'association.", example = "Doe", required = true) + @FormParam("m1_nom") + private String m1_lname = 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; + @Schema(description = "Le prénom du premier membre de l'association.", example = "John", required = true) + @FormParam("m1_prenom") + private String m1_fname = 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; + @Schema(description = "L'adresse e-mail du premier membre de l'association.", example = "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") + @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) + @FormParam("m1_role") + private RoleAsso m1_role = null; + + @Schema(description = "Le nom du deuxième membre de l'association.", example = "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) + @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) + @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") + @FormParam("m2_licence") + private String m2_lincence = null; + + @Schema(description = "Le rôle du deuxième membre de l'association.", example = "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) + @FormParam("m3_nom") + private String m3_lname = null; + + @Schema(description = "Le prénom du troisième membre de l'association.", example = "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) + @FormParam("m3_mail") + private String m3_email = null; + + @Schema(description = "Le numéro de licence du troisième membre de l'association. (null si non licencié)") + @FormParam("m3_licence") + private String m3_lincence = null; + + @Schema(description = "Le rôle du troisième membre de l'association.", example = "MEMBREBUREAU", required = true) + @FormParam("m3_role") + private RoleAsso m3_role = null; public AffiliationRequestModel toModel() { AffiliationRequestModel model = new AffiliationRequestModel(); model.setName(this.getName()); - model.setSiren(this.getSiren()); + model.setSiret(this.getSiret()); model.setRNA(this.getRna()); model.setAddress(this.getAdresse()); + model.setSaison(this.getSaison()); - 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.setM1_lname(this.getM1_lname()); + model.setM1_fname(this.getM1_fname()); + model.setM1_email(this.getM1_email()); + model.setM1_lincence((this.getM1_lincence() == null || this.getM1_lincence().isBlank()) + ? -1 : Integer.parseInt(this.getM1_lincence())); + model.setM1_role(this.getM1_role()); - 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.setM2_lname(this.getM2_lname()); + model.setM2_fname(this.getM2_fname()); + model.setM2_email(this.getM2_email()); + model.setM2_lincence((this.getM1_lincence() == null || this.getM1_lincence().isBlank()) + ? -1 : Integer.parseInt(this.getM2_lincence())); + model.setM2_role(this.getM2_role()); - 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())); + model.setM3_lname(this.getM3_lname()); + model.setM3_fname(this.getM3_fname()); + model.setM3_email(this.getM3_email()); + model.setM3_lincence((this.getM1_lincence() == null || this.getM1_lincence().isBlank()) + ? -1 : Integer.parseInt(this.getM3_lincence())); + model.setM3_role(this.getM3_role()); return model; } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestSaveForm.java b/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestSaveForm.java new file mode 100644 index 0000000..ac3012b --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestSaveForm.java @@ -0,0 +1,170 @@ +package fr.titionfire.ffsaf.rest.from; + +import fr.titionfire.ffsaf.utils.RoleAsso; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.core.MediaType; +import lombok.Getter; +import lombok.ToString; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.jboss.resteasy.reactive.PartType; + +@Getter +@ToString +public class AffiliationRequestSaveForm { + @Schema(description = "L'identifiant de l'affiliation.", example = "1", required = true) + @FormParam("id") + private Long id = null; + + @Schema(description = "Le nom de l'association.", example = "Association sportive", required = true) + @FormParam("name") + private String name = null; + + @Schema(description = "Le numéro SIRET de l'association.", example = "12345678901234", required = true) + @FormParam("siret") + private Long siret = null; + + @Schema(description = "Le numéro RNA de l'association. (peut être null)", example = "W123456789") + @FormParam("rna") + private String rna = null; + + @Schema(description = "L'adresse de l'association.", example = "1 rue de l'exemple, 75000 Paris", required = true) + @FormParam("address") + private String address = null; + + @Schema(description = "Le statut de l'association.") + @FormParam("status") + @PartType(MediaType.APPLICATION_OCTET_STREAM) + private byte[] status = new byte[0]; + + @Schema(description = "Le logo de l'association.") + @FormParam("logo") + @PartType(MediaType.APPLICATION_OCTET_STREAM) + private byte[] logo = new byte[0]; + + @Schema(description = "Mode utiliser pour la sauvegarde du membre 1 (0 = licence mode, 2 = nom, prénom)", example = "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) + @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) + @FormParam("m1_licence") + private String m1_lincence = null; + + @Schema(description = "Le nom du premier membre de l'association.", example = "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) + @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) + @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) + @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) + @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) + @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) + @FormParam("m2_licence") + private String m2_lincence = null; + + @Schema(description = "Le nom du deuxième membre de l'association.", example = "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) + @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) + @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) + @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) + @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) + @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) + @FormParam("m3_licence") + private String m3_lincence = null; + + @Schema(description = "Le nom du troisième membre de l'association.", example = "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) + @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) + @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) + @FormParam("m3_email_mode") + private Integer m3_email_mode = null; + + + @Getter + public class Member { + private Integer mode; + private RoleAsso role; + private String licence; + private String lname; + private String fname; + private String email; + private Integer email_mode; + + public Member(int n) { + if (n == 1) { + mode = m1_mode; + role = m1_role; + licence = m1_lincence; + lname = m1_lname; + fname = m1_fname; + email = m1_email; + email_mode = m1_email_mode; + } else if (n == 2) { + mode = m2_mode; + role = m2_role; + licence = m2_lincence; + lname = m2_lname; + fname = m2_fname; + email = m2_email; + email_mode = m2_email_mode; + } else if (n == 3) { + mode = m3_mode; + role = m3_role; + licence = m3_lincence; + lname = m3_lname; + fname = m3_fname; + email = m3_email; + email_mode = m3_email_mode; + } + } + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/from/ClubMemberForm.java b/src/main/java/fr/titionfire/ffsaf/rest/from/ClubMemberForm.java index ac752d3..d65f4bc 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/from/ClubMemberForm.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/from/ClubMemberForm.java @@ -6,39 +6,50 @@ import fr.titionfire.ffsaf.utils.RoleAsso; import jakarta.ws.rs.FormParam; import jakarta.ws.rs.core.MediaType; import lombok.Getter; +import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.jboss.resteasy.reactive.PartType; import java.util.Date; @Getter public class ClubMemberForm { + @Schema(description = "L'identifiant du membre.", example = "1234567", required = true) @FormParam("id") private String id = null; + @Schema(description = "Le nom du membre.", example = "Dupont", required = true) @FormParam("lname") private String lname = null; + @Schema(description = "Le prénom du membre.", example = "Jean", required = true) @FormParam("fname") private String fname = null; + @Schema(description = "La catégorie du membre.", example = "SENIOR", required = true) @FormParam("categorie") private Categorie categorie = null; + @Schema(description = "Le genre du membre.", example = "H", required = true) @FormParam("genre") private Genre genre; + @Schema(description = "Le pays du membre.", example = "FR", required = true) @FormParam("country") private String country; + @Schema(description = "La date de naissance du membre.", required = true) @FormParam("birth_date") private Date birth_date; + @Schema(description = "L'adresse e-mail du membre.", example = "jean.dupont@example.com", required = true) @FormParam("email") private String email; + @Schema(description = "Le rôle du membre dans l'association.", example = "MEMBRE", required = true) @FormParam("role") private RoleAsso role; + @Schema(description = "La photo du membre.") @FormParam("photo_data") @PartType(MediaType.APPLICATION_OCTET_STREAM) private byte[] photo_data = new byte[0]; diff --git a/src/main/java/fr/titionfire/ffsaf/rest/from/FullClubForm.java b/src/main/java/fr/titionfire/ffsaf/rest/from/FullClubForm.java new file mode 100644 index 0000000..26f81f1 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/from/FullClubForm.java @@ -0,0 +1,67 @@ +package fr.titionfire.ffsaf.rest.from; + +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.core.MediaType; +import lombok.Getter; +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; + +@ToString +@Getter +public class FullClubForm { + @FormParam("id") + @Schema(description = "Identifiant du club", example = "1", required = true) + private String id = null; + + @FormParam("name") + @Schema(description = "Nom du club", example = "Association sportive", required = true) + private String name = null; + + @FormParam("country") + @Schema(description = "Pays du club", example = "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) + 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) + 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) + private String training_day_time = null; + + @FormParam("contact_intern") + @Schema(description = "Contact interne du club", example = "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) + private String address = null; + + @FormParam("rna") + @Schema(description = "RNA du club", example = "W123456789") + private String rna = null; + + @FormParam("siret") + @Schema(description = "Numéro SIRET du club", example = "12345678901234", required = true) + private String siret = null; + + @FormParam("international") + @Schema(description = "Club international", example = "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]; + + @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]; +} 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 f164c9e..e08aa2d 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/from/FullMemberForm.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/from/FullMemberForm.java @@ -7,48 +7,62 @@ import fr.titionfire.ffsaf.utils.RoleAsso; import jakarta.ws.rs.FormParam; import jakarta.ws.rs.core.MediaType; import lombok.Getter; +import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.jboss.resteasy.reactive.PartType; import java.util.Date; @Getter public class FullMemberForm { + @Schema(description = "L'identifiant du membre.", example = "1") @FormParam("id") private String id = null; + @Schema(description = "Le nom du membre.", example = "Dupont") @FormParam("lname") private String lname = null; + @Schema(description = "Le prénom du membre.", example = "Jean") @FormParam("fname") private String fname = null; + @Schema(description = "La catégorie du membre.", example = "SENIOR") @FormParam("categorie") private Categorie categorie = null; + @Schema(description = "L'identifiant du club du membre.", example = "1") @FormParam("club") private Long club = null; + @Schema(description = "Le genre du membre.", example = "H") @FormParam("genre") private Genre genre; + @Schema(description = "Le numéro de licence du membre.", example = "12345") @FormParam("licence") private int licence; + @Schema(description = "Le pays du membre.", example = "FR") @FormParam("country") private String country; + @Schema(description = "La date de naissance du membre.") @FormParam("birth_date") - private Date birth_date; + private Date birth_date = null; + @Schema(description = "L'adresse e-mail du membre.", example = "jean.dupont@example.com") @FormParam("email") private String email; + @Schema(description = "Le rôle du membre dans l'association.", example = "MEMBRE") @FormParam("role") private RoleAsso role; + @Schema(description = "Le grade d'arbitrage du membre.", example = "ASSESSEUR") @FormParam("grade_arbitrage") private GradeArbitrage grade_arbitrage; + @Schema(description = "La photo du membre.") @FormParam("photo_data") @PartType(MediaType.APPLICATION_OCTET_STREAM) private byte[] photo_data = new byte[0]; diff --git a/src/main/java/fr/titionfire/ffsaf/rest/from/LicenceForm.java b/src/main/java/fr/titionfire/ffsaf/rest/from/LicenceForm.java index 678acc7..f6d8cc3 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/from/LicenceForm.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/from/LicenceForm.java @@ -3,22 +3,28 @@ package fr.titionfire.ffsaf.rest.from; import jakarta.ws.rs.FormParam; import lombok.Getter; import lombok.ToString; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @Getter @ToString public class LicenceForm { @FormParam("id") + @Schema(description = "L'identifiant de la licence. (-1 si nouvelle demande de licence)", required = true) private long id; @FormParam("membre") + @Schema(description = "L'identifiant du membre.", example = "1", required = true) private long membre; @FormParam("saison") + @Schema(description = "La saison de la licence.", example = "2025", required = true) private int saison; @FormParam("certificate") - private boolean certificate; + @Schema(description = "Nom du médecin sur certificat médical.", example = "M. Jean", required = true) + private String certificate = null; @FormParam("validate") + @Schema(description = "Licence validée (seuls les admin pourrons enregistrer cette valeur)", example = "true", required = true) private boolean validate; } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/from/MemberPermForm.java b/src/main/java/fr/titionfire/ffsaf/rest/from/MemberPermForm.java index a99832d..5b24b51 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/from/MemberPermForm.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/from/MemberPermForm.java @@ -3,19 +3,24 @@ package fr.titionfire.ffsaf.rest.from; import jakarta.ws.rs.FormParam; import lombok.Getter; import lombok.ToString; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @Getter @ToString public class MemberPermForm { + @Schema(description = "Indique si le membre est un administrateur de la fédération.", example = "false", required = true) @FormParam("federation_admin") private boolean federation_admin; + @Schema(description = "Indique si le membre est un utilisateur SAFCA.", example = "false", required = true) @FormParam("safca_user") private boolean safca_user; + @Schema(description = "Indique si le membre peut créer des compétitions sur SAFCA.", example = "false", required = true) @FormParam("safca_create_compet") private boolean safca_create_compet; + @Schema(description = "Indique si le membre est un super administrateur SAFCA.", example = "false", required = true) @FormParam("safca_super_admin") private boolean safca_super_admin; } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/from/PartClubForm.java b/src/main/java/fr/titionfire/ffsaf/rest/from/PartClubForm.java new file mode 100644 index 0000000..ac68cfb --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/from/PartClubForm.java @@ -0,0 +1,34 @@ +package fr.titionfire.ffsaf.rest.from; + +import jakarta.ws.rs.FormParam; +import lombok.Getter; +import lombok.ToString; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@ToString +@Getter +public class PartClubForm { + @FormParam("id") + @Schema(description = "Identifiant du club", example = "1", required = true) + private String id = null; + + @FormParam("contact") + @Schema(description = "Les contacts du club", example = "{\"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) + 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) + private String training_day_time = null; + + @FormParam("contact_intern") + @Schema(description = "Contact interne du club", example = "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) + private String address = null; +} diff --git a/src/main/java/fr/titionfire/ffsaf/utils/Categorie.java b/src/main/java/fr/titionfire/ffsaf/utils/Categorie.java index eca6c0e..a9c82be 100644 --- a/src/main/java/fr/titionfire/ffsaf/utils/Categorie.java +++ b/src/main/java/fr/titionfire/ffsaf/utils/Categorie.java @@ -1,7 +1,10 @@ package fr.titionfire.ffsaf.utils; +import io.quarkus.runtime.annotations.RegisterForReflection; + import java.util.ResourceBundle; +@RegisterForReflection public enum Categorie { SUPER_MINI, MINI_POUSSIN, @@ -30,4 +33,20 @@ public enum Categorie { case VETERAN2 -> BUNDLE.getString("Cat.VETERAN2"); }; } + + public String getName() { + return switch (this){ + case SUPER_MINI -> "Super Mini"; + case MINI_POUSSIN -> "Mini Poussin"; + case POUSSIN -> "Poussin"; + case BENJAMIN -> "Benjamin"; + case MINIME -> "Minime"; + case CADET -> "Cadet"; + case JUNIOR -> "Junior"; + case SENIOR1 -> "Senior 1"; + case SENIOR2 -> "Senior 2"; + case VETERAN1 -> "Vétéran 1"; + case VETERAN2 -> "Vétéran 2"; + }; + } } diff --git a/src/main/java/fr/titionfire/ffsaf/utils/CompetitionSystem.java b/src/main/java/fr/titionfire/ffsaf/utils/CompetitionSystem.java new file mode 100644 index 0000000..79dbeeb --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/utils/CompetitionSystem.java @@ -0,0 +1,5 @@ +package fr.titionfire.ffsaf.utils; + +public enum CompetitionSystem { + SAFCA, +} diff --git a/src/main/java/fr/titionfire/ffsaf/utils/Contact.java b/src/main/java/fr/titionfire/ffsaf/utils/Contact.java index 884e6d6..ea49228 100644 --- a/src/main/java/fr/titionfire/ffsaf/utils/Contact.java +++ b/src/main/java/fr/titionfire/ffsaf/utils/Contact.java @@ -2,6 +2,9 @@ package fr.titionfire.ffsaf.utils; import io.quarkus.runtime.annotations.RegisterForReflection; +import javax.naming.ldap.HasControls; +import java.util.HashMap; + @RegisterForReflection public enum Contact { COURRIEL("Courriel"), @@ -20,8 +23,11 @@ public enum Contact { this.name = name; } - @Override - public String toString() { - return name; + public static HashMap toSite() { + HashMap map = new HashMap<>(); + for (Contact contact : Contact.values()) { + map.put(contact.toString(), contact.name); + } + return map; } } diff --git a/src/main/java/fr/titionfire/ffsaf/utils/Genre.java b/src/main/java/fr/titionfire/ffsaf/utils/Genre.java index b5815c3..7b87357 100644 --- a/src/main/java/fr/titionfire/ffsaf/utils/Genre.java +++ b/src/main/java/fr/titionfire/ffsaf/utils/Genre.java @@ -1,5 +1,21 @@ package fr.titionfire.ffsaf.utils; +import io.quarkus.runtime.annotations.RegisterForReflection; + +@RegisterForReflection public enum Genre { - H, F, NA + H("Homme"), + F("Femme"), + NA("Non définie"); + + public final String str; + + Genre(String name) { + this.str = name; + } + + @Override + public String toString() { + return str; + } } diff --git a/src/main/java/fr/titionfire/ffsaf/utils/GradeArbitrage.java b/src/main/java/fr/titionfire/ffsaf/utils/GradeArbitrage.java index 800dca5..df9b927 100644 --- a/src/main/java/fr/titionfire/ffsaf/utils/GradeArbitrage.java +++ b/src/main/java/fr/titionfire/ffsaf/utils/GradeArbitrage.java @@ -1,18 +1,21 @@ package fr.titionfire.ffsaf.utils; +import io.quarkus.runtime.annotations.RegisterForReflection; + +@RegisterForReflection public enum GradeArbitrage { NA("N/A"), ASSESSEUR("Assesseur"), ARBITRE("Arbitre"); - public final String name; + public final String str; GradeArbitrage(String name) { - this.name = name; + this.str = name; } @Override public String toString() { - return name; + return str; } } diff --git a/src/main/java/fr/titionfire/ffsaf/utils/GroupeUtils.java b/src/main/java/fr/titionfire/ffsaf/utils/GroupeUtils.java deleted file mode 100644 index 3910ea9..0000000 --- a/src/main/java/fr/titionfire/ffsaf/utils/GroupeUtils.java +++ /dev/null @@ -1,25 +0,0 @@ -package fr.titionfire.ffsaf.utils; - -import org.eclipse.microprofile.jwt.JsonWebToken; - -public class GroupeUtils { - public static boolean isInClubGroup(long id, JsonWebToken accessToken) { - if (accessToken.getClaim("user_groups") instanceof Iterable) { - for (Object str : (Iterable) accessToken.getClaim("user_groups")) { - if (str.toString().substring(1, str.toString().length() - 1).startsWith("/club/" + id + "-")) - return true; - } - } - return false; - } - - public static boolean contains(String string, JsonWebToken accessToken) { - if (accessToken.getClaim("user_groups") instanceof Iterable) { - for (Object str : (Iterable) accessToken.getClaim("user_groups")) { - if (str.toString().substring(1, str.toString().length() - 1).contains(string)) - return true; - } - } - return false; - } -} diff --git a/src/main/java/fr/titionfire/ffsaf/utils/PageResult.java b/src/main/java/fr/titionfire/ffsaf/utils/PageResult.java index e8d2629..5d86ed1 100644 --- a/src/main/java/fr/titionfire/ffsaf/utils/PageResult.java +++ b/src/main/java/fr/titionfire/ffsaf/utils/PageResult.java @@ -2,6 +2,7 @@ package fr.titionfire.ffsaf.utils; import io.quarkus.runtime.annotations.RegisterForReflection; import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; import java.util.ArrayList; import java.util.List; @@ -9,9 +10,13 @@ import java.util.List; @Data @RegisterForReflection public class PageResult { + @Schema(description = "Le numéro de la page courante.", example = "1") private int page; + @Schema(description = "Le nombre d'éléments par page.", example = "10") private int page_size; + @Schema(description = "Le nombre total de pages.", example = "5") private int page_count; + @Schema(description = "Le nombre total d'éléments.", example = "47") private long result_count; private List result = new ArrayList<>(); } diff --git a/src/main/java/fr/titionfire/ffsaf/utils/RoleAsso.java b/src/main/java/fr/titionfire/ffsaf/utils/RoleAsso.java index 84991b1..b498495 100644 --- a/src/main/java/fr/titionfire/ffsaf/utils/RoleAsso.java +++ b/src/main/java/fr/titionfire/ffsaf/utils/RoleAsso.java @@ -5,20 +5,24 @@ import io.quarkus.runtime.annotations.RegisterForReflection; @RegisterForReflection public enum RoleAsso { MEMBRE("Membre", 0), - PRESIDENT("Président", 3), - TRESORIER("Trésorier", 1), - SECRETAIRE("Secrétaire", 2); + PRESIDENT("Président", 7), + VPRESIDENT("Vise-Président", 6), + SECRETAIRE("Secrétaire", 5), + VSECRETAIRE("Vise-Secrétaire", 4), + TRESORIER("Trésorier", 3), + VTRESORIER("Vise-Trésorier", 2), + MEMBREBUREAU("Membre bureau", 1); - public final String name; + public final String str; public final int level; RoleAsso(String name, int level) { - this.name = name; + this.str = name; this.level = level; } @Override public String toString() { - return name; + return str; } } diff --git a/src/main/java/fr/titionfire/ffsaf/utils/ScoreEmbeddable.java b/src/main/java/fr/titionfire/ffsaf/utils/ScoreEmbeddable.java new file mode 100644 index 0000000..53ce863 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/utils/ScoreEmbeddable.java @@ -0,0 +1,22 @@ +package fr.titionfire.ffsaf.utils; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import jakarta.persistence.*; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@RegisterForReflection + +@Embeddable +public class ScoreEmbeddable { + int n_round; + int s1; + int s2; +} diff --git a/src/main/java/fr/titionfire/ffsaf/utils/SecurityCtx.java b/src/main/java/fr/titionfire/ffsaf/utils/SecurityCtx.java new file mode 100644 index 0000000..348e1b3 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/utils/SecurityCtx.java @@ -0,0 +1,56 @@ +package fr.titionfire.ffsaf.utils; + +import io.quarkus.security.identity.SecurityIdentity; +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import org.eclipse.microprofile.jwt.JsonWebToken; + +import java.util.Set; + +@RequestScoped +public class SecurityCtx { + @Inject + JsonWebToken idToken; + + @Inject + SecurityIdentity securityIdentity; + + public Set getRoles() { + return securityIdentity.getRoles(); + } + + public String getSubject() { + if (idToken == null) + return null; + return idToken.getSubject(); + } + + public boolean roleHas(String role) { + if (role == null) + return false; + return securityIdentity.getRoles().contains(role); + } + + public boolean isInClubGroup(long id) { + if (idToken == null || idToken.getClaim("user_groups") == null) + return false; + + if (idToken.getClaim("user_groups") instanceof Iterable) { + for (Object str : (Iterable) idToken.getClaim("user_groups")) { + if (str.toString().substring(1, str.toString().length() - 1).startsWith("/club/" + id + "-")) + return true; + } + } + return false; + } + + public boolean contains(String string) { + if (idToken.getClaim("user_groups") instanceof Iterable) { + for (Object str : (Iterable) idToken.getClaim("user_groups")) { + if (str.toString().substring(1, str.toString().length() - 1).contains(string)) + return true; + } + } + return false; + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/utils/SequenceType.java b/src/main/java/fr/titionfire/ffsaf/utils/SequenceType.java new file mode 100644 index 0000000..a38f496 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/utils/SequenceType.java @@ -0,0 +1,5 @@ +package fr.titionfire.ffsaf.utils; + +public enum SequenceType { + Licence, Affiliation +} diff --git a/src/main/java/fr/titionfire/ffsaf/utils/StringSimilarity.java b/src/main/java/fr/titionfire/ffsaf/utils/StringSimilarity.java new file mode 100644 index 0000000..def85ee --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/utils/StringSimilarity.java @@ -0,0 +1,65 @@ +package fr.titionfire.ffsaf.utils; + +public class StringSimilarity { + + public static int similarity(String s1, String s2) { + String longer = s1, shorter = s2; + if (s1.length() < s2.length()) { + longer = s2; + shorter = s1; + } + int longerLength = longer.length(); + if (longerLength == 0) { + return 1; + } + return editDistance(longer, shorter); + + } + + public static int editDistance(String s1, String s2) { + s1 = s1.toLowerCase(); + s2 = s2.toLowerCase(); + + int[] costs = new int[s2.length() + 1]; + for (int i = 0; i <= s1.length(); i++) { + int lastValue = i; + for (int j = 0; j <= s2.length(); j++) { + if (i == 0) + costs[j] = j; + else { + if (j > 0) { + int newValue = costs[j - 1]; + if (s1.charAt(i - 1) != s2.charAt(j - 1)) + newValue = Math.min(Math.min(newValue, lastValue), + costs[j]) + 1; + costs[j - 1] = lastValue; + lastValue = newValue; + } + } + } + if (i > 0) + costs[s2.length()] = lastValue; + } + return costs[s2.length()]; + } + + public static void printSimilarity(String s, String t) { + System.out.printf( + "%d is the similarity between \"%s\" and \"%s\"%n", similarity(s, t), s, t); + } + + /*public static void main(String[] args) { + printSimilarity("Xavier Login", "Xavier Lojin"); + printSimilarity("Xavier Login", "Xavier ogin"); + printSimilarity("Xavier Login", "avier Login"); + printSimilarity("Xavier Login", "xavier login"); + printSimilarity("Xavier Login", "Xaviér Login"); + printSimilarity("Xavier Gomme Login", "Xavier Login"); + printSimilarity("Xavier Login", "Xavier Gomme Login"); + printSimilarity("Xavier Login", "Xavier"); + printSimilarity("Xavier Login", "Login"); + printSimilarity("Jule", "Julles"); + printSimilarity("Xavier", "Xaviér"); + printSimilarity("Xavier", "xavvie"); + }*/ +} diff --git a/src/main/java/fr/titionfire/ffsaf/utils/Utils.java b/src/main/java/fr/titionfire/ffsaf/utils/Utils.java index 88c4925..f93aa36 100644 --- a/src/main/java/fr/titionfire/ffsaf/utils/Utils.java +++ b/src/main/java/fr/titionfire/ffsaf/utils/Utils.java @@ -1,8 +1,18 @@ package fr.titionfire.ffsaf.utils; +import io.smallrye.mutiny.Uni; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import jodd.net.MimeTypes; +import net.sf.jmimemagic.Magic; +import net.sf.jmimemagic.MagicException; +import net.sf.jmimemagic.MagicMatchNotFoundException; +import net.sf.jmimemagic.MagicParseException; +import org.jboss.logging.Logger; import java.io.*; +import java.net.URISyntaxException; import java.net.URLConnection; import java.nio.file.Files; import java.util.Calendar; @@ -11,6 +21,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; public class Utils { + private static final org.jboss.logging.Logger LOGGER = Logger.getLogger(Utils.class); public static int getSaison() { return getSaison(new Date()); @@ -27,20 +38,63 @@ public class Utils { } } + public static Uni moveMedia(long idSrc, long idDest, String media, String dirSrc, String dirDst) { + return Uni.createFrom().nullItem().map(__ -> { + File dirFile = new File(media, dirSrc); + if (!dirFile.exists()) + return "Not found"; + + File dirDestFile = new File(media, dirDst); + if (!dirDestFile.exists()) + if (!dirDestFile.mkdirs()) + return "Fail to create directory " + dirDestFile; + + FilenameFilter filter = (directory, filename) -> filename.startsWith(idSrc + "."); + File[] files = dirFile.listFiles(filter); + if (files == null || files.length == 0) + return "Not found"; + + FilenameFilter filter2 = (directory, filename) -> filename.startsWith(idDest + "."); + File[] files2 = dirDestFile.listFiles(filter2); + if (files2 != null) { + for (File file : files2) { + //noinspection ResultOfMethodCallIgnored + file.delete(); + } + } + + for (File file : files) { + //noinspection ResultOfMethodCallIgnored + file.renameTo(new File(dirDestFile, + file.getName().replaceFirst(String.valueOf(idSrc), String.valueOf(idDest)))); + } + + return "Ok"; + }); + } + public static Future replacePhoto(long id, byte[] input, String media, String dir) { return CompletableFuture.supplyAsync(() -> { + if (input == null || input.length == 0) + return "OK"; + try (InputStream is = new BufferedInputStream(new ByteArrayInputStream(input))) { - String mimeType = URLConnection.guessContentTypeFromStream(is); + String mimeType; + try { + mimeType = Magic.getMagicMatch(input, false).getMimeType(); + } catch (MagicParseException | MagicMatchNotFoundException | MagicException 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()) + if (!dirFile.mkdirs()) throw new IOException("Fail to create directory " + dir); - FilenameFilter filter = (directory, filename) -> filename.startsWith(String.valueOf(id)); + FilenameFilter filter = (directory, filename) -> filename.startsWith(id + "."); File[] files = dirFile.listFiles(filter); if (files != null) { for (File file : files) { @@ -57,4 +111,85 @@ public class Utils { } }); } + + public static Uni getMediaFile(long id, String media, String dirname, + Uni uniBase) throws URISyntaxException { + return getMediaFile(id, media, dirname, null, uniBase); + } + + public static Uni getMediaFile(long id, String media, String dirname, String out_filename, + Uni uniBase) throws URISyntaxException { + Future> future = CompletableFuture.supplyAsync(() -> { + FilenameFilter filter = (directory, filename) -> filename.startsWith(id + "."); + File[] files = new File(media, dirname).listFiles(filter); + if (files != null && files.length > 0) { + File file = files[0]; + try { + byte[] data = Files.readAllBytes(file.toPath()); + return new Pair<>(file, data); + } catch (IOException ignored) { + } + } + return null; + }); + + Future future2 = CompletableFuture.supplyAsync(() -> { + try (InputStream st = Utils.class.getClassLoader().getResourceAsStream("asset/blank-profile-picture.png")) { + if (st == null) + return null; + return st.readAllBytes(); + } catch (IOException ignored) { + } + return null; + }); + + return uniBase.chain(__ -> Uni.createFrom().future(future) + .chain(filePair -> { + if (filePair == null) { + return Uni.createFrom().future(future2).map(data -> { + if (data == null) + return Response.noContent().build(); + + String mimeType = "image/apng"; + Response.ResponseBuilder resp = Response.ok(data); + resp.type(MediaType.APPLICATION_OCTET_STREAM); + resp.header(HttpHeaders.CONTENT_LENGTH, data.length); + resp.header(HttpHeaders.CONTENT_TYPE, mimeType); + resp.header(HttpHeaders.CONTENT_DISPOSITION, + "inline; " + ((out_filename == null) ? "" : "filename=\"" + out_filename + "\"")); + return resp.build(); + }); + } else { + return Uni.createFrom().item(() -> { + String mimeType = URLConnection.guessContentTypeFromName(filePair.getKey().getName()); + + Response.ResponseBuilder resp = Response.ok(filePair.getValue()); + resp.type(MediaType.APPLICATION_OCTET_STREAM); + resp.header(HttpHeaders.CONTENT_LENGTH, filePair.getValue().length); + resp.header(HttpHeaders.CONTENT_TYPE, mimeType); + resp.header(HttpHeaders.CONTENT_DISPOSITION, + "inline; " + ((out_filename == null) ? "" : "filename=\"" + out_filename + "\"")); + return resp.build(); + }); + } + })); + } + + public static Uni deleteMedia(long id, String media, String dir) { + return Uni.createFrom().nullItem().map(__ -> { + File dirFile = new File(media, dir); + if (!dirFile.exists()) + return "OK"; + + FilenameFilter filter = (directory, filename) -> filename.startsWith(id + "."); + File[] files = dirFile.listFiles(filter); + if (files != null) { + for (File file : files) { + //noinspection ResultOfMethodCallIgnored + file.delete(); + } + } + return "Ok"; + }); + } } diff --git a/src/main/java/fr/titionfire/ffsaf/ws/FileSocket.java b/src/main/java/fr/titionfire/ffsaf/ws/FileSocket.java deleted file mode 100644 index 283a09b..0000000 --- a/src/main/java/fr/titionfire/ffsaf/ws/FileSocket.java +++ /dev/null @@ -1,165 +0,0 @@ -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 org.eclipse.microprofile.config.inject.ConfigProperty; -import org.jboss.logging.Logger; - -import java.io.*; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -@ServerEndpoint("/api/ws/file/{code}") -@ApplicationScoped -public class FileSocket { - private static final Logger logger = Logger.getLogger(FileSocket.class); - public static Map sessions = new ConcurrentHashMap<>(); - - @ConfigProperty(name = "upload_dir") - String media; - - /*@Scheduled(every = "10s") - void increment() { - sessions.forEach((key, value) -> { - if (System.currentTimeMillis() - value.time > 60000) { - closeAndDelete(value); - if (value.session != null && value.session.isOpen()) { - try { - value.session.close(new CloseReason(CloseReason.CloseCodes.VIOLATED_POLICY, "Timeout")); - } catch (IOException e) { - StringWriter errors = new StringWriter(); - e.printStackTrace(new PrintWriter(errors)); - logger.error(errors.toString()); - } - } - sessions.remove(key); - } - }); - }*/ - - @OnOpen - public void onOpen(Session session, @PathParam("code") String code) { - try { - if (sessions.containsKey(code)) { - FileRecv fileRecv = sessions.get(code); - fileRecv.session = session; - fileRecv.file = new File(media + "-ext", "record/" + fileRecv.name); - fileRecv.fos = new FileOutputStream(fileRecv.file, false); - logger.info("Start reception of file: " + fileRecv.file.getAbsolutePath()); - } else { - session.close(new CloseReason(CloseReason.CloseCodes.VIOLATED_POLICY, "File not found")); - } - } catch (IOException e) { - StringWriter errors = new StringWriter(); - e.printStackTrace(new PrintWriter(errors)); - logger.error(errors.toString()); - } - } - - @OnClose - public void onClose(Session session, @PathParam("code") String code) { - if (sessions.containsKey(code)) { - FileRecv fileRecv = sessions.get(code); - if (fileRecv.fos != null) { - try { - fileRecv.fos.close(); - } catch (IOException e) { - StringWriter errors = new StringWriter(); - e.printStackTrace(new PrintWriter(errors)); - logger.error(errors.toString()); - } - } - logger.info("File received: " + fileRecv.file.getAbsolutePath()); - sessions.remove(code); - } - } - - @OnError - public void onError(Session session, @PathParam("code") String code, Throwable throwable) { - if (sessions.containsKey(code)) { - closeAndDelete(sessions.get(code)); - sessions.remove(code); - } - logger.error("Error on file reception: " + throwable.getMessage()); - } - - - @OnMessage - public void onMessage(String message, @PathParam("code") String code) { - if (message.equals("cancel")) { - if (sessions.containsKey(code)) { - closeAndDelete(sessions.get(code)); - sessions.remove(code); - } - logger.error("Error file " + code + " are cancel by the client"); - } - } - - private void closeAndDelete(FileRecv fileRecv) { - if (fileRecv.fos != null) { - try { - fileRecv.fos.close(); - } catch (IOException e) { - StringWriter errors = new StringWriter(); - e.printStackTrace(new PrintWriter(errors)); - logger.error(errors.toString()); - } - } - if (fileRecv.file.exists()) { - //noinspection ResultOfMethodCallIgnored - fileRecv.file.delete(); - } - } - - @OnMessage - public void onMessage(byte[] data, @PathParam("code") String code) { - int length = (data[1] << 7) | data[2]; - - byte check_sum = 0; - for (int j = 3; j < length + 3; j++) { - check_sum = (byte) (check_sum ^ data[j]); - } - // System.out.println(length + " - " + data[1] + " - " + data[0] + " - " + check_sum); - - if (sessions.containsKey(code)) { - FileRecv fileRecv = sessions.get(code); - - if (check_sum != data[0]) { - fileRecv.session.getAsyncRemote().sendText("Error: Checksum error", result -> { - if (result.getException() != null) { - logger.error("Unable to send message: " + result.getException()); - } - }); - return; - } - - try { - fileRecv.fos.write(data, 3, length); - } catch (IOException e) { - StringWriter errors = new StringWriter(); - e.printStackTrace(new PrintWriter(errors)); - logger.error(errors.toString()); - } - - fileRecv.session.getAsyncRemote().sendText("ok", result -> { - if (result.getException() != null) { - logger.error("Unable to send message: " + result.getException()); - } - }); - } - } - - @AllArgsConstructor - @RegisterForReflection - public static class FileRecv { - Session session; - String name; - File file; - FileOutputStream fos; - long time; - } -} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 92aa416..c0346aa 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -17,12 +17,11 @@ quarkus.quartz.start-mode=forced %dev.quarkus.log.min-level=ALL %dev.quarkus.log.category."fr.titionfire.ffsaf".level=ALL - quarkus.oidc.auth-server-url=https://auth.safca.fr/realms/safca quarkus.oidc.client-id=backend quarkus.oidc.credentials.secret=secret quarkus.oidc.tls.verification=required -quarkus.oidc.application-type=web-app +quarkus.oidc.application-type=hybrid quarkus.oidc.roles.source=accesstoken quarkus.http.limits.max-body-size=10M diff --git a/src/main/resources/asset/DMSans-Regular.ttf b/src/main/resources/asset/DMSans-Regular.ttf new file mode 100644 index 0000000..07266ae Binary files /dev/null and b/src/main/resources/asset/DMSans-Regular.ttf differ diff --git a/src/main/resources/asset/FFSSAF-bord-blanc-fond-transparent.png b/src/main/resources/asset/FFSSAF-bord-blanc-fond-transparent.png new file mode 100644 index 0000000..e4d6e6d Binary files /dev/null and b/src/main/resources/asset/FFSSAF-bord-blanc-fond-transparent.png differ diff --git a/src/main/resources/asset/blank-profile-picture.png b/src/main/resources/asset/blank-profile-picture.png new file mode 100644 index 0000000..9e34293 Binary files /dev/null and b/src/main/resources/asset/blank-profile-picture.png differ diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html index 97bbfb1..95678f6 100644 --- a/src/main/webapp/index.html +++ b/src/main/webapp/index.html @@ -14,6 +14,10 @@ integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" /> + + diff --git a/src/main/webapp/package-lock.json b/src/main/webapp/package-lock.json index 37c5149..78674e5 100644 --- a/src/main/webapp/package-lock.json +++ b/src/main/webapp/package-lock.json @@ -15,8 +15,11 @@ "@fortawesome/react-fontawesome": "^0.2.0", "axios": "^1.6.5", "browser-image-compression": "^2.0.2", + "leaflet": "^1.9.4", + "proj4": "^2.11.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-leaflet": "^4.2.1", "react-loader-spinner": "^6.1.6", "react-router-dom": "^6.21.2", "react-toastify": "^10.0.4" @@ -1023,6 +1026,16 @@ "node": ">= 8" } }, + "node_modules/@react-leaflet/core": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz", + "integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/@remix-run/router": { "version": "1.14.2", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.14.2.tgz", @@ -3113,6 +3126,11 @@ "json-buffer": "3.0.1" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3167,6 +3185,11 @@ "yallist": "^3.0.2" } }, + "node_modules/mgrs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mgrs/-/mgrs-1.0.0.tgz", + "integrity": "sha512-awNbTOqCxK1DBGjalK3xqWIstBZgN6fxsMSiXLs9/spqWkF2pAhb2rrYCFSsr1/tT7PhcDGjZndG8SWYn0byYA==" + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -3486,6 +3509,15 @@ "node": ">= 0.8.0" } }, + "node_modules/proj4": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/proj4/-/proj4-2.11.0.tgz", + "integrity": "sha512-SasuTkAx8HnWQHfIyhkdUNJorSJqINHAN3EyMWYiQRVorftz9DHz650YraFgczwgtHOxqnfuDxSNv3C8MUnHeg==", + "dependencies": { + "mgrs": "1.0.0", + "wkt-parser": "^1.3.3" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -3558,6 +3590,19 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-leaflet": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz", + "integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==", + "dependencies": { + "@react-leaflet/core": "^2.1.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/react-loader-spinner": { "version": "6.1.6", "resolved": "https://registry.npmjs.org/react-loader-spinner/-/react-loader-spinner-6.1.6.tgz", @@ -4393,6 +4438,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wkt-parser": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/wkt-parser/-/wkt-parser-1.3.3.tgz", + "integrity": "sha512-ZnV3yH8/k58ZPACOXeiHaMuXIiaTk1t0hSUVisbO0t4RjA5wPpUytcxeyiN2h+LZRrmuHIh/1UlrR9e7DHDvTw==" + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/src/main/webapp/package.json b/src/main/webapp/package.json index 534a96f..32fbdf3 100644 --- a/src/main/webapp/package.json +++ b/src/main/webapp/package.json @@ -17,8 +17,11 @@ "@fortawesome/react-fontawesome": "^0.2.0", "axios": "^1.6.5", "browser-image-compression": "^2.0.2", + "leaflet": "^1.9.4", + "proj4": "^2.11.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-leaflet": "^4.2.1", "react-loader-spinner": "^6.1.6", "react-router-dom": "^6.21.2", "react-toastify": "^10.0.4" diff --git a/src/main/webapp/public/blank-profile-picture.png b/src/main/webapp/public/blank-profile-picture.png new file mode 100644 index 0000000..9e34293 Binary files /dev/null and b/src/main/webapp/public/blank-profile-picture.png differ diff --git a/src/main/webapp/public/flags/flags_ad.png b/src/main/webapp/public/flags/flags_ad.png new file mode 100644 index 0000000..b750895 Binary files /dev/null and b/src/main/webapp/public/flags/flags_ad.png differ diff --git a/src/main/webapp/public/flags/flags_ae.png b/src/main/webapp/public/flags/flags_ae.png new file mode 100644 index 0000000..b5d25f6 Binary files /dev/null and b/src/main/webapp/public/flags/flags_ae.png differ diff --git a/src/main/webapp/public/flags/flags_af.png b/src/main/webapp/public/flags/flags_af.png new file mode 100644 index 0000000..f38b17a Binary files /dev/null and b/src/main/webapp/public/flags/flags_af.png differ diff --git a/src/main/webapp/public/flags/flags_ag.png b/src/main/webapp/public/flags/flags_ag.png new file mode 100644 index 0000000..669e34e Binary files /dev/null and b/src/main/webapp/public/flags/flags_ag.png differ diff --git a/src/main/webapp/public/flags/flags_ai.png b/src/main/webapp/public/flags/flags_ai.png new file mode 100644 index 0000000..19c4823 Binary files /dev/null and b/src/main/webapp/public/flags/flags_ai.png differ diff --git a/src/main/webapp/public/flags/flags_al.png b/src/main/webapp/public/flags/flags_al.png new file mode 100644 index 0000000..79770bd Binary files /dev/null and b/src/main/webapp/public/flags/flags_al.png differ diff --git a/src/main/webapp/public/flags/flags_am.png b/src/main/webapp/public/flags/flags_am.png new file mode 100644 index 0000000..36a2cc2 Binary files /dev/null and b/src/main/webapp/public/flags/flags_am.png differ diff --git a/src/main/webapp/public/flags/flags_ao.png b/src/main/webapp/public/flags/flags_ao.png new file mode 100644 index 0000000..301a92d Binary files /dev/null and b/src/main/webapp/public/flags/flags_ao.png differ diff --git a/src/main/webapp/public/flags/flags_aq.png b/src/main/webapp/public/flags/flags_aq.png new file mode 100644 index 0000000..accccd7 Binary files /dev/null and b/src/main/webapp/public/flags/flags_aq.png differ diff --git a/src/main/webapp/public/flags/flags_ar.png b/src/main/webapp/public/flags/flags_ar.png new file mode 100644 index 0000000..795042c Binary files /dev/null and b/src/main/webapp/public/flags/flags_ar.png differ diff --git a/src/main/webapp/public/flags/flags_as.png b/src/main/webapp/public/flags/flags_as.png new file mode 100644 index 0000000..464e36b Binary files /dev/null and b/src/main/webapp/public/flags/flags_as.png differ diff --git a/src/main/webapp/public/flags/flags_at.png b/src/main/webapp/public/flags/flags_at.png new file mode 100644 index 0000000..0848b2a Binary files /dev/null and b/src/main/webapp/public/flags/flags_at.png differ diff --git a/src/main/webapp/public/flags/flags_au.png b/src/main/webapp/public/flags/flags_au.png new file mode 100644 index 0000000..9e4033c Binary files /dev/null and b/src/main/webapp/public/flags/flags_au.png differ diff --git a/src/main/webapp/public/flags/flags_aw.png b/src/main/webapp/public/flags/flags_aw.png new file mode 100644 index 0000000..c59c9ed Binary files /dev/null and b/src/main/webapp/public/flags/flags_aw.png differ diff --git a/src/main/webapp/public/flags/flags_ax.png b/src/main/webapp/public/flags/flags_ax.png new file mode 100644 index 0000000..5ce6bda Binary files /dev/null and b/src/main/webapp/public/flags/flags_ax.png differ diff --git a/src/main/webapp/public/flags/flags_az.png b/src/main/webapp/public/flags/flags_az.png new file mode 100644 index 0000000..d45b6eb Binary files /dev/null and b/src/main/webapp/public/flags/flags_az.png differ diff --git a/src/main/webapp/public/flags/flags_ba.png b/src/main/webapp/public/flags/flags_ba.png new file mode 100644 index 0000000..dc47995 Binary files /dev/null and b/src/main/webapp/public/flags/flags_ba.png differ diff --git a/src/main/webapp/public/flags/flags_bb.png b/src/main/webapp/public/flags/flags_bb.png new file mode 100644 index 0000000..6b42943 Binary files /dev/null and b/src/main/webapp/public/flags/flags_bb.png differ diff --git a/src/main/webapp/public/flags/flags_bd.png b/src/main/webapp/public/flags/flags_bd.png new file mode 100644 index 0000000..571f0a4 Binary files /dev/null and b/src/main/webapp/public/flags/flags_bd.png differ diff --git a/src/main/webapp/public/flags/flags_be.png b/src/main/webapp/public/flags/flags_be.png new file mode 100644 index 0000000..f4270f2 Binary files /dev/null and b/src/main/webapp/public/flags/flags_be.png differ diff --git a/src/main/webapp/public/flags/flags_bf.png b/src/main/webapp/public/flags/flags_bf.png new file mode 100644 index 0000000..1dffc19 Binary files /dev/null and b/src/main/webapp/public/flags/flags_bf.png differ diff --git a/src/main/webapp/public/flags/flags_bg.png b/src/main/webapp/public/flags/flags_bg.png new file mode 100644 index 0000000..8cd991c Binary files /dev/null and b/src/main/webapp/public/flags/flags_bg.png differ diff --git a/src/main/webapp/public/flags/flags_bh.png b/src/main/webapp/public/flags/flags_bh.png new file mode 100644 index 0000000..dcc9a0c Binary files /dev/null and b/src/main/webapp/public/flags/flags_bh.png differ diff --git a/src/main/webapp/public/flags/flags_bi.png b/src/main/webapp/public/flags/flags_bi.png new file mode 100644 index 0000000..2b82b52 Binary files /dev/null and b/src/main/webapp/public/flags/flags_bi.png differ diff --git a/src/main/webapp/public/flags/flags_bj.png b/src/main/webapp/public/flags/flags_bj.png new file mode 100644 index 0000000..ab6f4b9 Binary files /dev/null and b/src/main/webapp/public/flags/flags_bj.png differ diff --git a/src/main/webapp/public/flags/flags_bl.png b/src/main/webapp/public/flags/flags_bl.png new file mode 100644 index 0000000..fdc0baa Binary files /dev/null and b/src/main/webapp/public/flags/flags_bl.png differ diff --git a/src/main/webapp/public/flags/flags_bm.png b/src/main/webapp/public/flags/flags_bm.png new file mode 100644 index 0000000..fd23054 Binary files /dev/null and b/src/main/webapp/public/flags/flags_bm.png differ diff --git a/src/main/webapp/public/flags/flags_bn.png b/src/main/webapp/public/flags/flags_bn.png new file mode 100644 index 0000000..bcd4bb9 Binary files /dev/null and b/src/main/webapp/public/flags/flags_bn.png differ diff --git a/src/main/webapp/public/flags/flags_bo.png b/src/main/webapp/public/flags/flags_bo.png new file mode 100644 index 0000000..1c613a3 Binary files /dev/null and b/src/main/webapp/public/flags/flags_bo.png differ diff --git a/src/main/webapp/public/flags/flags_bq.png b/src/main/webapp/public/flags/flags_bq.png new file mode 100644 index 0000000..7dfbb59 Binary files /dev/null and b/src/main/webapp/public/flags/flags_bq.png differ diff --git a/src/main/webapp/public/flags/flags_br.png b/src/main/webapp/public/flags/flags_br.png new file mode 100644 index 0000000..39cf3e7 Binary files /dev/null and b/src/main/webapp/public/flags/flags_br.png differ diff --git a/src/main/webapp/public/flags/flags_bs.png b/src/main/webapp/public/flags/flags_bs.png new file mode 100644 index 0000000..30d4907 Binary files /dev/null and b/src/main/webapp/public/flags/flags_bs.png differ diff --git a/src/main/webapp/public/flags/flags_bt.png b/src/main/webapp/public/flags/flags_bt.png new file mode 100644 index 0000000..6aaf2eb Binary files /dev/null and b/src/main/webapp/public/flags/flags_bt.png differ diff --git a/src/main/webapp/public/flags/flags_bv.png b/src/main/webapp/public/flags/flags_bv.png new file mode 100644 index 0000000..bfbd46d Binary files /dev/null and b/src/main/webapp/public/flags/flags_bv.png differ diff --git a/src/main/webapp/public/flags/flags_bw.png b/src/main/webapp/public/flags/flags_bw.png new file mode 100644 index 0000000..08934e7 Binary files /dev/null and b/src/main/webapp/public/flags/flags_bw.png differ diff --git a/src/main/webapp/public/flags/flags_by.png b/src/main/webapp/public/flags/flags_by.png new file mode 100644 index 0000000..e2eed0c Binary files /dev/null and b/src/main/webapp/public/flags/flags_by.png differ diff --git a/src/main/webapp/public/flags/flags_bz.png b/src/main/webapp/public/flags/flags_bz.png new file mode 100644 index 0000000..5166529 Binary files /dev/null and b/src/main/webapp/public/flags/flags_bz.png differ diff --git a/src/main/webapp/public/flags/flags_ca.png b/src/main/webapp/public/flags/flags_ca.png new file mode 100644 index 0000000..1670997 Binary files /dev/null and b/src/main/webapp/public/flags/flags_ca.png differ diff --git a/src/main/webapp/public/flags/flags_cc.png b/src/main/webapp/public/flags/flags_cc.png new file mode 100644 index 0000000..58c90ff Binary files /dev/null and b/src/main/webapp/public/flags/flags_cc.png differ diff --git a/src/main/webapp/public/flags/flags_cd.png b/src/main/webapp/public/flags/flags_cd.png new file mode 100644 index 0000000..db2e24b Binary files /dev/null and b/src/main/webapp/public/flags/flags_cd.png differ diff --git a/src/main/webapp/public/flags/flags_cf.png b/src/main/webapp/public/flags/flags_cf.png new file mode 100644 index 0000000..0c73cc1 Binary files /dev/null and b/src/main/webapp/public/flags/flags_cf.png differ diff --git a/src/main/webapp/public/flags/flags_cg.png b/src/main/webapp/public/flags/flags_cg.png new file mode 100644 index 0000000..5c87941 Binary files /dev/null and b/src/main/webapp/public/flags/flags_cg.png differ diff --git a/src/main/webapp/public/flags/flags_ch.png b/src/main/webapp/public/flags/flags_ch.png new file mode 100644 index 0000000..aef5f5f Binary files /dev/null and b/src/main/webapp/public/flags/flags_ch.png differ diff --git a/src/main/webapp/public/flags/flags_ci.png b/src/main/webapp/public/flags/flags_ci.png new file mode 100644 index 0000000..c9f1db6 Binary files /dev/null and b/src/main/webapp/public/flags/flags_ci.png differ diff --git a/src/main/webapp/public/flags/flags_ck.png b/src/main/webapp/public/flags/flags_ck.png new file mode 100644 index 0000000..b339940 Binary files /dev/null and b/src/main/webapp/public/flags/flags_ck.png differ diff --git a/src/main/webapp/public/flags/flags_cl.png b/src/main/webapp/public/flags/flags_cl.png new file mode 100644 index 0000000..7015e88 Binary files /dev/null and b/src/main/webapp/public/flags/flags_cl.png differ diff --git a/src/main/webapp/public/flags/flags_cm.png b/src/main/webapp/public/flags/flags_cm.png new file mode 100644 index 0000000..1d089f2 Binary files /dev/null and b/src/main/webapp/public/flags/flags_cm.png differ diff --git a/src/main/webapp/public/flags/flags_cn.png b/src/main/webapp/public/flags/flags_cn.png new file mode 100644 index 0000000..5fe7e5f Binary files /dev/null and b/src/main/webapp/public/flags/flags_cn.png differ diff --git a/src/main/webapp/public/flags/flags_co.png b/src/main/webapp/public/flags/flags_co.png new file mode 100644 index 0000000..d03450a Binary files /dev/null and b/src/main/webapp/public/flags/flags_co.png differ diff --git a/src/main/webapp/public/flags/flags_cr.png b/src/main/webapp/public/flags/flags_cr.png new file mode 100644 index 0000000..84d4aba Binary files /dev/null and b/src/main/webapp/public/flags/flags_cr.png differ diff --git a/src/main/webapp/public/flags/flags_cu.png b/src/main/webapp/public/flags/flags_cu.png new file mode 100644 index 0000000..2285564 Binary files /dev/null and b/src/main/webapp/public/flags/flags_cu.png differ diff --git a/src/main/webapp/public/flags/flags_cv.png b/src/main/webapp/public/flags/flags_cv.png new file mode 100644 index 0000000..b27e125 Binary files /dev/null and b/src/main/webapp/public/flags/flags_cv.png differ diff --git a/src/main/webapp/public/flags/flags_cw.png b/src/main/webapp/public/flags/flags_cw.png new file mode 100644 index 0000000..459d4db Binary files /dev/null and b/src/main/webapp/public/flags/flags_cw.png differ diff --git a/src/main/webapp/public/flags/flags_cx.png b/src/main/webapp/public/flags/flags_cx.png new file mode 100644 index 0000000..b70ce5f Binary files /dev/null and b/src/main/webapp/public/flags/flags_cx.png differ diff --git a/src/main/webapp/public/flags/flags_cy.png b/src/main/webapp/public/flags/flags_cy.png new file mode 100644 index 0000000..984a03e Binary files /dev/null and b/src/main/webapp/public/flags/flags_cy.png differ diff --git a/src/main/webapp/public/flags/flags_cz.png b/src/main/webapp/public/flags/flags_cz.png new file mode 100644 index 0000000..5040542 Binary files /dev/null and b/src/main/webapp/public/flags/flags_cz.png differ diff --git a/src/main/webapp/public/flags/flags_de.png b/src/main/webapp/public/flags/flags_de.png new file mode 100644 index 0000000..f57ee83 Binary files /dev/null and b/src/main/webapp/public/flags/flags_de.png differ diff --git a/src/main/webapp/public/flags/flags_dj.png b/src/main/webapp/public/flags/flags_dj.png new file mode 100644 index 0000000..366eb1e Binary files /dev/null and b/src/main/webapp/public/flags/flags_dj.png differ diff --git a/src/main/webapp/public/flags/flags_dk.png b/src/main/webapp/public/flags/flags_dk.png new file mode 100644 index 0000000..80dac23 Binary files /dev/null and b/src/main/webapp/public/flags/flags_dk.png differ diff --git a/src/main/webapp/public/flags/flags_dm.png b/src/main/webapp/public/flags/flags_dm.png new file mode 100644 index 0000000..89c01b8 Binary files /dev/null and b/src/main/webapp/public/flags/flags_dm.png differ diff --git a/src/main/webapp/public/flags/flags_do.png b/src/main/webapp/public/flags/flags_do.png new file mode 100644 index 0000000..8384003 Binary files /dev/null and b/src/main/webapp/public/flags/flags_do.png differ diff --git a/src/main/webapp/public/flags/flags_dz.png b/src/main/webapp/public/flags/flags_dz.png new file mode 100644 index 0000000..1e75b36 Binary files /dev/null and b/src/main/webapp/public/flags/flags_dz.png differ diff --git a/src/main/webapp/public/flags/flags_ec.png b/src/main/webapp/public/flags/flags_ec.png new file mode 100644 index 0000000..3abdceb Binary files /dev/null and b/src/main/webapp/public/flags/flags_ec.png differ diff --git a/src/main/webapp/public/flags/flags_ee.png b/src/main/webapp/public/flags/flags_ee.png new file mode 100644 index 0000000..ed175b5 Binary files /dev/null and b/src/main/webapp/public/flags/flags_ee.png differ diff --git a/src/main/webapp/public/flags/flags_eg.png b/src/main/webapp/public/flags/flags_eg.png new file mode 100644 index 0000000..572b899 Binary files /dev/null and b/src/main/webapp/public/flags/flags_eg.png differ diff --git a/src/main/webapp/public/flags/flags_eh.png b/src/main/webapp/public/flags/flags_eh.png new file mode 100644 index 0000000..af9b2d0 Binary files /dev/null and b/src/main/webapp/public/flags/flags_eh.png differ diff --git a/src/main/webapp/public/flags/flags_er.png b/src/main/webapp/public/flags/flags_er.png new file mode 100644 index 0000000..01377ae Binary files /dev/null and b/src/main/webapp/public/flags/flags_er.png differ diff --git a/src/main/webapp/public/flags/flags_es.png b/src/main/webapp/public/flags/flags_es.png new file mode 100644 index 0000000..d0b393b Binary files /dev/null and b/src/main/webapp/public/flags/flags_es.png differ diff --git a/src/main/webapp/public/flags/flags_et.png b/src/main/webapp/public/flags/flags_et.png new file mode 100644 index 0000000..0493946 Binary files /dev/null and b/src/main/webapp/public/flags/flags_et.png differ diff --git a/src/main/webapp/public/flags/flags_fi.png b/src/main/webapp/public/flags/flags_fi.png new file mode 100644 index 0000000..c79484e Binary files /dev/null and b/src/main/webapp/public/flags/flags_fi.png differ diff --git a/src/main/webapp/public/flags/flags_fj.png b/src/main/webapp/public/flags/flags_fj.png new file mode 100644 index 0000000..645f764 Binary files /dev/null and b/src/main/webapp/public/flags/flags_fj.png differ diff --git a/src/main/webapp/public/flags/flags_fk.png b/src/main/webapp/public/flags/flags_fk.png new file mode 100644 index 0000000..a036922 Binary files /dev/null and b/src/main/webapp/public/flags/flags_fk.png differ diff --git a/src/main/webapp/public/flags/flags_fm.png b/src/main/webapp/public/flags/flags_fm.png new file mode 100644 index 0000000..8ef8008 Binary files /dev/null and b/src/main/webapp/public/flags/flags_fm.png differ diff --git a/src/main/webapp/public/flags/flags_fo.png b/src/main/webapp/public/flags/flags_fo.png new file mode 100644 index 0000000..2e42492 Binary files /dev/null and b/src/main/webapp/public/flags/flags_fo.png differ diff --git a/src/main/webapp/public/flags/flags_fr.png b/src/main/webapp/public/flags/flags_fr.png new file mode 100644 index 0000000..1cba62e Binary files /dev/null and b/src/main/webapp/public/flags/flags_fr.png differ diff --git a/src/main/webapp/public/flags/flags_ga.png b/src/main/webapp/public/flags/flags_ga.png new file mode 100644 index 0000000..9dfacfb Binary files /dev/null and b/src/main/webapp/public/flags/flags_ga.png differ diff --git a/src/main/webapp/public/flags/flags_gb.png b/src/main/webapp/public/flags/flags_gb.png new file mode 100644 index 0000000..90cd0c4 Binary files /dev/null and b/src/main/webapp/public/flags/flags_gb.png differ diff --git a/src/main/webapp/public/flags/flags_gd.png b/src/main/webapp/public/flags/flags_gd.png new file mode 100644 index 0000000..edc2374 Binary files /dev/null and b/src/main/webapp/public/flags/flags_gd.png differ diff --git a/src/main/webapp/public/flags/flags_ge.png b/src/main/webapp/public/flags/flags_ge.png new file mode 100644 index 0000000..fc702ed Binary files /dev/null and b/src/main/webapp/public/flags/flags_ge.png differ diff --git a/src/main/webapp/public/flags/flags_gf.png b/src/main/webapp/public/flags/flags_gf.png new file mode 100644 index 0000000..0646981 Binary files /dev/null and b/src/main/webapp/public/flags/flags_gf.png differ diff --git a/src/main/webapp/public/flags/flags_gg.png b/src/main/webapp/public/flags/flags_gg.png new file mode 100644 index 0000000..e81ad67 Binary files /dev/null and b/src/main/webapp/public/flags/flags_gg.png differ diff --git a/src/main/webapp/public/flags/flags_gh.png b/src/main/webapp/public/flags/flags_gh.png new file mode 100644 index 0000000..f60438e Binary files /dev/null and b/src/main/webapp/public/flags/flags_gh.png differ diff --git a/src/main/webapp/public/flags/flags_gi.png b/src/main/webapp/public/flags/flags_gi.png new file mode 100644 index 0000000..f5c613c Binary files /dev/null and b/src/main/webapp/public/flags/flags_gi.png differ diff --git a/src/main/webapp/public/flags/flags_gl.png b/src/main/webapp/public/flags/flags_gl.png new file mode 100644 index 0000000..5d72262 Binary files /dev/null and b/src/main/webapp/public/flags/flags_gl.png differ diff --git a/src/main/webapp/public/flags/flags_gm.png b/src/main/webapp/public/flags/flags_gm.png new file mode 100644 index 0000000..b7e639f Binary files /dev/null and b/src/main/webapp/public/flags/flags_gm.png differ diff --git a/src/main/webapp/public/flags/flags_gn.png b/src/main/webapp/public/flags/flags_gn.png new file mode 100644 index 0000000..5ec8902 Binary files /dev/null and b/src/main/webapp/public/flags/flags_gn.png differ diff --git a/src/main/webapp/public/flags/flags_gp.png b/src/main/webapp/public/flags/flags_gp.png new file mode 100644 index 0000000..519b7ca Binary files /dev/null and b/src/main/webapp/public/flags/flags_gp.png differ diff --git a/src/main/webapp/public/flags/flags_gq.png b/src/main/webapp/public/flags/flags_gq.png new file mode 100644 index 0000000..d49b6d1 Binary files /dev/null and b/src/main/webapp/public/flags/flags_gq.png differ diff --git a/src/main/webapp/public/flags/flags_gr.png b/src/main/webapp/public/flags/flags_gr.png new file mode 100644 index 0000000..3b20721 Binary files /dev/null and b/src/main/webapp/public/flags/flags_gr.png differ diff --git a/src/main/webapp/public/flags/flags_gs.png b/src/main/webapp/public/flags/flags_gs.png new file mode 100644 index 0000000..5cf5a1b Binary files /dev/null and b/src/main/webapp/public/flags/flags_gs.png differ diff --git a/src/main/webapp/public/flags/flags_gt.png b/src/main/webapp/public/flags/flags_gt.png new file mode 100644 index 0000000..b2664f7 Binary files /dev/null and b/src/main/webapp/public/flags/flags_gt.png differ diff --git a/src/main/webapp/public/flags/flags_gu.png b/src/main/webapp/public/flags/flags_gu.png new file mode 100644 index 0000000..2221ff8 Binary files /dev/null and b/src/main/webapp/public/flags/flags_gu.png differ diff --git a/src/main/webapp/public/flags/flags_gw.png b/src/main/webapp/public/flags/flags_gw.png new file mode 100644 index 0000000..2707720 Binary files /dev/null and b/src/main/webapp/public/flags/flags_gw.png differ diff --git a/src/main/webapp/public/flags/flags_gy.png b/src/main/webapp/public/flags/flags_gy.png new file mode 100644 index 0000000..12e66a5 Binary files /dev/null and b/src/main/webapp/public/flags/flags_gy.png differ diff --git a/src/main/webapp/public/flags/flags_hk.png b/src/main/webapp/public/flags/flags_hk.png new file mode 100644 index 0000000..49c4f66 Binary files /dev/null and b/src/main/webapp/public/flags/flags_hk.png differ diff --git a/src/main/webapp/public/flags/flags_hm.png b/src/main/webapp/public/flags/flags_hm.png new file mode 100644 index 0000000..d372e40 Binary files /dev/null and b/src/main/webapp/public/flags/flags_hm.png differ diff --git a/src/main/webapp/public/flags/flags_hn.png b/src/main/webapp/public/flags/flags_hn.png new file mode 100644 index 0000000..5fac3cc Binary files /dev/null and b/src/main/webapp/public/flags/flags_hn.png differ diff --git a/src/main/webapp/public/flags/flags_hr.png b/src/main/webapp/public/flags/flags_hr.png new file mode 100644 index 0000000..372b89e Binary files /dev/null and b/src/main/webapp/public/flags/flags_hr.png differ diff --git a/src/main/webapp/public/flags/flags_ht.png b/src/main/webapp/public/flags/flags_ht.png new file mode 100644 index 0000000..6480536 Binary files /dev/null and b/src/main/webapp/public/flags/flags_ht.png differ diff --git a/src/main/webapp/public/flags/flags_hu.png b/src/main/webapp/public/flags/flags_hu.png new file mode 100644 index 0000000..2b7d26d Binary files /dev/null and b/src/main/webapp/public/flags/flags_hu.png differ diff --git a/src/main/webapp/public/flags/flags_id.png b/src/main/webapp/public/flags/flags_id.png new file mode 100644 index 0000000..03fdc56 Binary files /dev/null and b/src/main/webapp/public/flags/flags_id.png differ diff --git a/src/main/webapp/public/flags/flags_ie.png b/src/main/webapp/public/flags/flags_ie.png new file mode 100644 index 0000000..68b8704 Binary files /dev/null and b/src/main/webapp/public/flags/flags_ie.png differ diff --git a/src/main/webapp/public/flags/flags_il.png b/src/main/webapp/public/flags/flags_il.png new file mode 100644 index 0000000..617bcf6 Binary files /dev/null and b/src/main/webapp/public/flags/flags_il.png differ diff --git a/src/main/webapp/public/flags/flags_im.png b/src/main/webapp/public/flags/flags_im.png new file mode 100644 index 0000000..5fe2926 Binary files /dev/null and b/src/main/webapp/public/flags/flags_im.png differ diff --git a/src/main/webapp/public/flags/flags_in.png b/src/main/webapp/public/flags/flags_in.png new file mode 100644 index 0000000..e234bfd Binary files /dev/null and b/src/main/webapp/public/flags/flags_in.png differ diff --git a/src/main/webapp/public/flags/flags_io.png b/src/main/webapp/public/flags/flags_io.png new file mode 100644 index 0000000..4f05166 Binary files /dev/null and b/src/main/webapp/public/flags/flags_io.png differ diff --git a/src/main/webapp/public/flags/flags_iq.png b/src/main/webapp/public/flags/flags_iq.png new file mode 100644 index 0000000..47903a4 Binary files /dev/null and b/src/main/webapp/public/flags/flags_iq.png differ diff --git a/src/main/webapp/public/flags/flags_ir.png b/src/main/webapp/public/flags/flags_ir.png new file mode 100644 index 0000000..958d398 Binary files /dev/null and b/src/main/webapp/public/flags/flags_ir.png differ diff --git a/src/main/webapp/public/flags/flags_is.png b/src/main/webapp/public/flags/flags_is.png new file mode 100644 index 0000000..4ebc845 Binary files /dev/null and b/src/main/webapp/public/flags/flags_is.png differ diff --git a/src/main/webapp/public/flags/flags_it.png b/src/main/webapp/public/flags/flags_it.png new file mode 100644 index 0000000..f342a33 Binary files /dev/null and b/src/main/webapp/public/flags/flags_it.png differ diff --git a/src/main/webapp/public/flags/flags_je.png b/src/main/webapp/public/flags/flags_je.png new file mode 100644 index 0000000..3d7f194 Binary files /dev/null and b/src/main/webapp/public/flags/flags_je.png differ diff --git a/src/main/webapp/public/flags/flags_jm.png b/src/main/webapp/public/flags/flags_jm.png new file mode 100644 index 0000000..a380e17 Binary files /dev/null and b/src/main/webapp/public/flags/flags_jm.png differ diff --git a/src/main/webapp/public/flags/flags_jo.png b/src/main/webapp/public/flags/flags_jo.png new file mode 100644 index 0000000..cc5422b Binary files /dev/null and b/src/main/webapp/public/flags/flags_jo.png differ diff --git a/src/main/webapp/public/flags/flags_jp.png b/src/main/webapp/public/flags/flags_jp.png new file mode 100644 index 0000000..4130459 Binary files /dev/null and b/src/main/webapp/public/flags/flags_jp.png differ diff --git a/src/main/webapp/public/flags/flags_ke.png b/src/main/webapp/public/flags/flags_ke.png new file mode 100644 index 0000000..88cd07f Binary files /dev/null and b/src/main/webapp/public/flags/flags_ke.png differ diff --git a/src/main/webapp/public/flags/flags_kg.png b/src/main/webapp/public/flags/flags_kg.png new file mode 100644 index 0000000..1e49b97 Binary files /dev/null and b/src/main/webapp/public/flags/flags_kg.png differ diff --git a/src/main/webapp/public/flags/flags_kh.png b/src/main/webapp/public/flags/flags_kh.png new file mode 100644 index 0000000..414f0ec Binary files /dev/null and b/src/main/webapp/public/flags/flags_kh.png differ diff --git a/src/main/webapp/public/flags/flags_ki.png b/src/main/webapp/public/flags/flags_ki.png new file mode 100644 index 0000000..1034b3d Binary files /dev/null and b/src/main/webapp/public/flags/flags_ki.png differ diff --git a/src/main/webapp/public/flags/flags_km.png b/src/main/webapp/public/flags/flags_km.png new file mode 100644 index 0000000..e886085 Binary files /dev/null and b/src/main/webapp/public/flags/flags_km.png differ diff --git a/src/main/webapp/public/flags/flags_kn.png b/src/main/webapp/public/flags/flags_kn.png new file mode 100644 index 0000000..133f40d Binary files /dev/null and b/src/main/webapp/public/flags/flags_kn.png differ diff --git a/src/main/webapp/public/flags/flags_kp.png b/src/main/webapp/public/flags/flags_kp.png new file mode 100644 index 0000000..2aa96f0 Binary files /dev/null and b/src/main/webapp/public/flags/flags_kp.png differ diff --git a/src/main/webapp/public/flags/flags_kr.png b/src/main/webapp/public/flags/flags_kr.png new file mode 100644 index 0000000..4a737a2 Binary files /dev/null and b/src/main/webapp/public/flags/flags_kr.png differ diff --git a/src/main/webapp/public/flags/flags_kw.png b/src/main/webapp/public/flags/flags_kw.png new file mode 100644 index 0000000..67f127c Binary files /dev/null and b/src/main/webapp/public/flags/flags_kw.png differ diff --git a/src/main/webapp/public/flags/flags_ky.png b/src/main/webapp/public/flags/flags_ky.png new file mode 100644 index 0000000..33c4799 Binary files /dev/null and b/src/main/webapp/public/flags/flags_ky.png differ diff --git a/src/main/webapp/public/flags/flags_kz.png b/src/main/webapp/public/flags/flags_kz.png new file mode 100644 index 0000000..4258969 Binary files /dev/null and b/src/main/webapp/public/flags/flags_kz.png differ diff --git a/src/main/webapp/public/flags/flags_la.png b/src/main/webapp/public/flags/flags_la.png new file mode 100644 index 0000000..3202a8a Binary files /dev/null and b/src/main/webapp/public/flags/flags_la.png differ diff --git a/src/main/webapp/public/flags/flags_lb.png b/src/main/webapp/public/flags/flags_lb.png new file mode 100644 index 0000000..09a452f Binary files /dev/null and b/src/main/webapp/public/flags/flags_lb.png differ diff --git a/src/main/webapp/public/flags/flags_lc.png b/src/main/webapp/public/flags/flags_lc.png new file mode 100644 index 0000000..bdec5c8 Binary files /dev/null and b/src/main/webapp/public/flags/flags_lc.png differ diff --git a/src/main/webapp/public/flags/flags_li.png b/src/main/webapp/public/flags/flags_li.png new file mode 100644 index 0000000..f881eb3 Binary files /dev/null and b/src/main/webapp/public/flags/flags_li.png differ diff --git a/src/main/webapp/public/flags/flags_lk.png b/src/main/webapp/public/flags/flags_lk.png new file mode 100644 index 0000000..8e8da85 Binary files /dev/null and b/src/main/webapp/public/flags/flags_lk.png differ diff --git a/src/main/webapp/public/flags/flags_lr.png b/src/main/webapp/public/flags/flags_lr.png new file mode 100644 index 0000000..a5bec9d Binary files /dev/null and b/src/main/webapp/public/flags/flags_lr.png differ diff --git a/src/main/webapp/public/flags/flags_ls.png b/src/main/webapp/public/flags/flags_ls.png new file mode 100644 index 0000000..16db821 Binary files /dev/null and b/src/main/webapp/public/flags/flags_ls.png differ diff --git a/src/main/webapp/public/flags/flags_lt.png b/src/main/webapp/public/flags/flags_lt.png new file mode 100644 index 0000000..13c6ea4 Binary files /dev/null and b/src/main/webapp/public/flags/flags_lt.png differ diff --git a/src/main/webapp/public/flags/flags_lu.png b/src/main/webapp/public/flags/flags_lu.png new file mode 100644 index 0000000..6a0cb7e Binary files /dev/null and b/src/main/webapp/public/flags/flags_lu.png differ diff --git a/src/main/webapp/public/flags/flags_lv.png b/src/main/webapp/public/flags/flags_lv.png new file mode 100644 index 0000000..bccb85e Binary files /dev/null and b/src/main/webapp/public/flags/flags_lv.png differ diff --git a/src/main/webapp/public/flags/flags_ly.png b/src/main/webapp/public/flags/flags_ly.png new file mode 100644 index 0000000..298393f Binary files /dev/null and b/src/main/webapp/public/flags/flags_ly.png differ diff --git a/src/main/webapp/public/flags/flags_ma.png b/src/main/webapp/public/flags/flags_ma.png new file mode 100644 index 0000000..084b197 Binary files /dev/null and b/src/main/webapp/public/flags/flags_ma.png differ diff --git a/src/main/webapp/public/flags/flags_mc.png b/src/main/webapp/public/flags/flags_mc.png new file mode 100644 index 0000000..3a141da Binary files /dev/null and b/src/main/webapp/public/flags/flags_mc.png differ diff --git a/src/main/webapp/public/flags/flags_md.png b/src/main/webapp/public/flags/flags_md.png new file mode 100644 index 0000000..3ca84e6 Binary files /dev/null and b/src/main/webapp/public/flags/flags_md.png differ diff --git a/src/main/webapp/public/flags/flags_me.png b/src/main/webapp/public/flags/flags_me.png new file mode 100644 index 0000000..e5e0445 Binary files /dev/null and b/src/main/webapp/public/flags/flags_me.png differ diff --git a/src/main/webapp/public/flags/flags_mf.png b/src/main/webapp/public/flags/flags_mf.png new file mode 100644 index 0000000..1cba62e Binary files /dev/null and b/src/main/webapp/public/flags/flags_mf.png differ diff --git a/src/main/webapp/public/flags/flags_mg.png b/src/main/webapp/public/flags/flags_mg.png new file mode 100644 index 0000000..46f0a57 Binary files /dev/null and b/src/main/webapp/public/flags/flags_mg.png differ diff --git a/src/main/webapp/public/flags/flags_mh.png b/src/main/webapp/public/flags/flags_mh.png new file mode 100644 index 0000000..9f000a5 Binary files /dev/null and b/src/main/webapp/public/flags/flags_mh.png differ diff --git a/src/main/webapp/public/flags/flags_mk.png b/src/main/webapp/public/flags/flags_mk.png new file mode 100644 index 0000000..ce71c21 Binary files /dev/null and b/src/main/webapp/public/flags/flags_mk.png differ diff --git a/src/main/webapp/public/flags/flags_ml.png b/src/main/webapp/public/flags/flags_ml.png new file mode 100644 index 0000000..6c0dcf0 Binary files /dev/null and b/src/main/webapp/public/flags/flags_ml.png differ diff --git a/src/main/webapp/public/flags/flags_mm.png b/src/main/webapp/public/flags/flags_mm.png new file mode 100644 index 0000000..650f535 Binary files /dev/null and b/src/main/webapp/public/flags/flags_mm.png differ diff --git a/src/main/webapp/public/flags/flags_mn.png b/src/main/webapp/public/flags/flags_mn.png new file mode 100644 index 0000000..2b00e7b Binary files /dev/null and b/src/main/webapp/public/flags/flags_mn.png differ diff --git a/src/main/webapp/public/flags/flags_mo.png b/src/main/webapp/public/flags/flags_mo.png new file mode 100644 index 0000000..2e69908 Binary files /dev/null and b/src/main/webapp/public/flags/flags_mo.png differ diff --git a/src/main/webapp/public/flags/flags_mp.png b/src/main/webapp/public/flags/flags_mp.png new file mode 100644 index 0000000..ee9d621 Binary files /dev/null and b/src/main/webapp/public/flags/flags_mp.png differ diff --git a/src/main/webapp/public/flags/flags_mq.png b/src/main/webapp/public/flags/flags_mq.png new file mode 100644 index 0000000..ca303e8 Binary files /dev/null and b/src/main/webapp/public/flags/flags_mq.png differ diff --git a/src/main/webapp/public/flags/flags_mr.png b/src/main/webapp/public/flags/flags_mr.png new file mode 100644 index 0000000..f15a388 Binary files /dev/null and b/src/main/webapp/public/flags/flags_mr.png differ diff --git a/src/main/webapp/public/flags/flags_ms.png b/src/main/webapp/public/flags/flags_ms.png new file mode 100644 index 0000000..31b268b Binary files /dev/null and b/src/main/webapp/public/flags/flags_ms.png differ diff --git a/src/main/webapp/public/flags/flags_mt.png b/src/main/webapp/public/flags/flags_mt.png new file mode 100644 index 0000000..b83f796 Binary files /dev/null and b/src/main/webapp/public/flags/flags_mt.png differ diff --git a/src/main/webapp/public/flags/flags_mu.png b/src/main/webapp/public/flags/flags_mu.png new file mode 100644 index 0000000..df6294b Binary files /dev/null and b/src/main/webapp/public/flags/flags_mu.png differ diff --git a/src/main/webapp/public/flags/flags_mv.png b/src/main/webapp/public/flags/flags_mv.png new file mode 100644 index 0000000..3af54a4 Binary files /dev/null and b/src/main/webapp/public/flags/flags_mv.png differ diff --git a/src/main/webapp/public/flags/flags_mw.png b/src/main/webapp/public/flags/flags_mw.png new file mode 100644 index 0000000..9f2daed Binary files /dev/null and b/src/main/webapp/public/flags/flags_mw.png differ diff --git a/src/main/webapp/public/flags/flags_mx.png b/src/main/webapp/public/flags/flags_mx.png new file mode 100644 index 0000000..0004186 Binary files /dev/null and b/src/main/webapp/public/flags/flags_mx.png differ diff --git a/src/main/webapp/public/flags/flags_my.png b/src/main/webapp/public/flags/flags_my.png new file mode 100644 index 0000000..b1c71f5 Binary files /dev/null and b/src/main/webapp/public/flags/flags_my.png differ diff --git a/src/main/webapp/public/flags/flags_mz.png b/src/main/webapp/public/flags/flags_mz.png new file mode 100644 index 0000000..6411251 Binary files /dev/null and b/src/main/webapp/public/flags/flags_mz.png differ diff --git a/src/main/webapp/public/flags/flags_na.png b/src/main/webapp/public/flags/flags_na.png new file mode 100644 index 0000000..99f7e3c Binary files /dev/null and b/src/main/webapp/public/flags/flags_na.png differ diff --git a/src/main/webapp/public/flags/flags_nc.png b/src/main/webapp/public/flags/flags_nc.png new file mode 100644 index 0000000..34a619e Binary files /dev/null and b/src/main/webapp/public/flags/flags_nc.png differ diff --git a/src/main/webapp/public/flags/flags_ne.png b/src/main/webapp/public/flags/flags_ne.png new file mode 100644 index 0000000..bdce81c Binary files /dev/null and b/src/main/webapp/public/flags/flags_ne.png differ diff --git a/src/main/webapp/public/flags/flags_nf.png b/src/main/webapp/public/flags/flags_nf.png new file mode 100644 index 0000000..e1f1b2f Binary files /dev/null and b/src/main/webapp/public/flags/flags_nf.png differ diff --git a/src/main/webapp/public/flags/flags_ng.png b/src/main/webapp/public/flags/flags_ng.png new file mode 100644 index 0000000..488068e Binary files /dev/null and b/src/main/webapp/public/flags/flags_ng.png differ diff --git a/src/main/webapp/public/flags/flags_ni.png b/src/main/webapp/public/flags/flags_ni.png new file mode 100644 index 0000000..3df6cc2 Binary files /dev/null and b/src/main/webapp/public/flags/flags_ni.png differ diff --git a/src/main/webapp/public/flags/flags_nl.png b/src/main/webapp/public/flags/flags_nl.png new file mode 100644 index 0000000..564a915 Binary files /dev/null and b/src/main/webapp/public/flags/flags_nl.png differ diff --git a/src/main/webapp/public/flags/flags_no.png b/src/main/webapp/public/flags/flags_no.png new file mode 100644 index 0000000..bfbd46d Binary files /dev/null and b/src/main/webapp/public/flags/flags_no.png differ diff --git a/src/main/webapp/public/flags/flags_np.png b/src/main/webapp/public/flags/flags_np.png new file mode 100644 index 0000000..1c3205d Binary files /dev/null and b/src/main/webapp/public/flags/flags_np.png differ diff --git a/src/main/webapp/public/flags/flags_nr.png b/src/main/webapp/public/flags/flags_nr.png new file mode 100644 index 0000000..e915a48 Binary files /dev/null and b/src/main/webapp/public/flags/flags_nr.png differ diff --git a/src/main/webapp/public/flags/flags_nu.png b/src/main/webapp/public/flags/flags_nu.png new file mode 100644 index 0000000..7cd5f52 Binary files /dev/null and b/src/main/webapp/public/flags/flags_nu.png differ diff --git a/src/main/webapp/public/flags/flags_nz.png b/src/main/webapp/public/flags/flags_nz.png new file mode 100644 index 0000000..7d9e161 Binary files /dev/null and b/src/main/webapp/public/flags/flags_nz.png differ diff --git a/src/main/webapp/public/flags/flags_om.png b/src/main/webapp/public/flags/flags_om.png new file mode 100644 index 0000000..9c658d4 Binary files /dev/null and b/src/main/webapp/public/flags/flags_om.png differ diff --git a/src/main/webapp/public/flags/flags_pa.png b/src/main/webapp/public/flags/flags_pa.png new file mode 100644 index 0000000..0c45461 Binary files /dev/null and b/src/main/webapp/public/flags/flags_pa.png differ diff --git a/src/main/webapp/public/flags/flags_pe.png b/src/main/webapp/public/flags/flags_pe.png new file mode 100644 index 0000000..7a462cc Binary files /dev/null and b/src/main/webapp/public/flags/flags_pe.png differ diff --git a/src/main/webapp/public/flags/flags_pf.png b/src/main/webapp/public/flags/flags_pf.png new file mode 100644 index 0000000..d2ccc54 Binary files /dev/null and b/src/main/webapp/public/flags/flags_pf.png differ diff --git a/src/main/webapp/public/flags/flags_pg.png b/src/main/webapp/public/flags/flags_pg.png new file mode 100644 index 0000000..d1c25b2 Binary files /dev/null and b/src/main/webapp/public/flags/flags_pg.png differ diff --git a/src/main/webapp/public/flags/flags_ph.png b/src/main/webapp/public/flags/flags_ph.png new file mode 100644 index 0000000..a173e25 Binary files /dev/null and b/src/main/webapp/public/flags/flags_ph.png differ diff --git a/src/main/webapp/public/flags/flags_pk.png b/src/main/webapp/public/flags/flags_pk.png new file mode 100644 index 0000000..442d03c Binary files /dev/null and b/src/main/webapp/public/flags/flags_pk.png differ diff --git a/src/main/webapp/public/flags/flags_pl.png b/src/main/webapp/public/flags/flags_pl.png new file mode 100644 index 0000000..f220bfe Binary files /dev/null and b/src/main/webapp/public/flags/flags_pl.png differ diff --git a/src/main/webapp/public/flags/flags_pm.png b/src/main/webapp/public/flags/flags_pm.png new file mode 100644 index 0000000..fa0f4bf Binary files /dev/null and b/src/main/webapp/public/flags/flags_pm.png differ diff --git a/src/main/webapp/public/flags/flags_pn.png b/src/main/webapp/public/flags/flags_pn.png new file mode 100644 index 0000000..e739f95 Binary files /dev/null and b/src/main/webapp/public/flags/flags_pn.png differ diff --git a/src/main/webapp/public/flags/flags_pr.png b/src/main/webapp/public/flags/flags_pr.png new file mode 100644 index 0000000..aa7bef7 Binary files /dev/null and b/src/main/webapp/public/flags/flags_pr.png differ diff --git a/src/main/webapp/public/flags/flags_ps.png b/src/main/webapp/public/flags/flags_ps.png new file mode 100644 index 0000000..c3e1a5c Binary files /dev/null and b/src/main/webapp/public/flags/flags_ps.png differ diff --git a/src/main/webapp/public/flags/flags_pt.png b/src/main/webapp/public/flags/flags_pt.png new file mode 100644 index 0000000..1be1cee Binary files /dev/null and b/src/main/webapp/public/flags/flags_pt.png differ diff --git a/src/main/webapp/public/flags/flags_pw.png b/src/main/webapp/public/flags/flags_pw.png new file mode 100644 index 0000000..2efb744 Binary files /dev/null and b/src/main/webapp/public/flags/flags_pw.png differ diff --git a/src/main/webapp/public/flags/flags_py.png b/src/main/webapp/public/flags/flags_py.png new file mode 100644 index 0000000..3ebdc49 Binary files /dev/null and b/src/main/webapp/public/flags/flags_py.png differ diff --git a/src/main/webapp/public/flags/flags_qa.png b/src/main/webapp/public/flags/flags_qa.png new file mode 100644 index 0000000..9b0e95a Binary files /dev/null and b/src/main/webapp/public/flags/flags_qa.png differ diff --git a/src/main/webapp/public/flags/flags_re.png b/src/main/webapp/public/flags/flags_re.png new file mode 100644 index 0000000..98b2ca3 Binary files /dev/null and b/src/main/webapp/public/flags/flags_re.png differ diff --git a/src/main/webapp/public/flags/flags_ro.png b/src/main/webapp/public/flags/flags_ro.png new file mode 100644 index 0000000..8ea5627 Binary files /dev/null and b/src/main/webapp/public/flags/flags_ro.png differ diff --git a/src/main/webapp/public/flags/flags_rs.png b/src/main/webapp/public/flags/flags_rs.png new file mode 100644 index 0000000..3a624f7 Binary files /dev/null and b/src/main/webapp/public/flags/flags_rs.png differ diff --git a/src/main/webapp/public/flags/flags_ru.png b/src/main/webapp/public/flags/flags_ru.png new file mode 100644 index 0000000..6821d59 Binary files /dev/null and b/src/main/webapp/public/flags/flags_ru.png differ diff --git a/src/main/webapp/public/flags/flags_rw.png b/src/main/webapp/public/flags/flags_rw.png new file mode 100644 index 0000000..84ea77a Binary files /dev/null and b/src/main/webapp/public/flags/flags_rw.png differ diff --git a/src/main/webapp/public/flags/flags_sa.png b/src/main/webapp/public/flags/flags_sa.png new file mode 100644 index 0000000..bcf6779 Binary files /dev/null and b/src/main/webapp/public/flags/flags_sa.png differ diff --git a/src/main/webapp/public/flags/flags_sb.png b/src/main/webapp/public/flags/flags_sb.png new file mode 100644 index 0000000..0caccaa Binary files /dev/null and b/src/main/webapp/public/flags/flags_sb.png differ diff --git a/src/main/webapp/public/flags/flags_sc.png b/src/main/webapp/public/flags/flags_sc.png new file mode 100644 index 0000000..2ccd568 Binary files /dev/null and b/src/main/webapp/public/flags/flags_sc.png differ diff --git a/src/main/webapp/public/flags/flags_sd.png b/src/main/webapp/public/flags/flags_sd.png new file mode 100644 index 0000000..80517e6 Binary files /dev/null and b/src/main/webapp/public/flags/flags_sd.png differ diff --git a/src/main/webapp/public/flags/flags_se.png b/src/main/webapp/public/flags/flags_se.png new file mode 100644 index 0000000..f937cb0 Binary files /dev/null and b/src/main/webapp/public/flags/flags_se.png differ diff --git a/src/main/webapp/public/flags/flags_sg.png b/src/main/webapp/public/flags/flags_sg.png new file mode 100644 index 0000000..2151da4 Binary files /dev/null and b/src/main/webapp/public/flags/flags_sg.png differ diff --git a/src/main/webapp/public/flags/flags_sh.png b/src/main/webapp/public/flags/flags_sh.png new file mode 100644 index 0000000..1648f34 Binary files /dev/null and b/src/main/webapp/public/flags/flags_sh.png differ diff --git a/src/main/webapp/public/flags/flags_si.png b/src/main/webapp/public/flags/flags_si.png new file mode 100644 index 0000000..ebfa53e Binary files /dev/null and b/src/main/webapp/public/flags/flags_si.png differ diff --git a/src/main/webapp/public/flags/flags_sj.png b/src/main/webapp/public/flags/flags_sj.png new file mode 100644 index 0000000..bfbd46d Binary files /dev/null and b/src/main/webapp/public/flags/flags_sj.png differ diff --git a/src/main/webapp/public/flags/flags_sk.png b/src/main/webapp/public/flags/flags_sk.png new file mode 100644 index 0000000..01e2c89 Binary files /dev/null and b/src/main/webapp/public/flags/flags_sk.png differ diff --git a/src/main/webapp/public/flags/flags_sl.png b/src/main/webapp/public/flags/flags_sl.png new file mode 100644 index 0000000..a7d36d7 Binary files /dev/null and b/src/main/webapp/public/flags/flags_sl.png differ diff --git a/src/main/webapp/public/flags/flags_sm.png b/src/main/webapp/public/flags/flags_sm.png new file mode 100644 index 0000000..482dfcf Binary files /dev/null and b/src/main/webapp/public/flags/flags_sm.png differ diff --git a/src/main/webapp/public/flags/flags_sn.png b/src/main/webapp/public/flags/flags_sn.png new file mode 100644 index 0000000..3ecd166 Binary files /dev/null and b/src/main/webapp/public/flags/flags_sn.png differ diff --git a/src/main/webapp/public/flags/flags_so.png b/src/main/webapp/public/flags/flags_so.png new file mode 100644 index 0000000..70f94ec Binary files /dev/null and b/src/main/webapp/public/flags/flags_so.png differ diff --git a/src/main/webapp/public/flags/flags_sr.png b/src/main/webapp/public/flags/flags_sr.png new file mode 100644 index 0000000..26c2681 Binary files /dev/null and b/src/main/webapp/public/flags/flags_sr.png differ diff --git a/src/main/webapp/public/flags/flags_ss.png b/src/main/webapp/public/flags/flags_ss.png new file mode 100644 index 0000000..6bf3251 Binary files /dev/null and b/src/main/webapp/public/flags/flags_ss.png differ diff --git a/src/main/webapp/public/flags/flags_st.png b/src/main/webapp/public/flags/flags_st.png new file mode 100644 index 0000000..a029334 Binary files /dev/null and b/src/main/webapp/public/flags/flags_st.png differ diff --git a/src/main/webapp/public/flags/flags_sv.png b/src/main/webapp/public/flags/flags_sv.png new file mode 100644 index 0000000..17965f4 Binary files /dev/null and b/src/main/webapp/public/flags/flags_sv.png differ diff --git a/src/main/webapp/public/flags/flags_sx.png b/src/main/webapp/public/flags/flags_sx.png new file mode 100644 index 0000000..bdeff0c Binary files /dev/null and b/src/main/webapp/public/flags/flags_sx.png differ diff --git a/src/main/webapp/public/flags/flags_sy.png b/src/main/webapp/public/flags/flags_sy.png new file mode 100644 index 0000000..fe03842 Binary files /dev/null and b/src/main/webapp/public/flags/flags_sy.png differ diff --git a/src/main/webapp/public/flags/flags_sz.png b/src/main/webapp/public/flags/flags_sz.png new file mode 100644 index 0000000..2cd6beb Binary files /dev/null and b/src/main/webapp/public/flags/flags_sz.png differ diff --git a/src/main/webapp/public/flags/flags_tc.png b/src/main/webapp/public/flags/flags_tc.png new file mode 100644 index 0000000..2d3a901 Binary files /dev/null and b/src/main/webapp/public/flags/flags_tc.png differ diff --git a/src/main/webapp/public/flags/flags_td.png b/src/main/webapp/public/flags/flags_td.png new file mode 100644 index 0000000..c59f4e9 Binary files /dev/null and b/src/main/webapp/public/flags/flags_td.png differ diff --git a/src/main/webapp/public/flags/flags_tf.png b/src/main/webapp/public/flags/flags_tf.png new file mode 100644 index 0000000..95df01a Binary files /dev/null and b/src/main/webapp/public/flags/flags_tf.png differ diff --git a/src/main/webapp/public/flags/flags_tg.png b/src/main/webapp/public/flags/flags_tg.png new file mode 100644 index 0000000..de1a7ec Binary files /dev/null and b/src/main/webapp/public/flags/flags_tg.png differ diff --git a/src/main/webapp/public/flags/flags_th.png b/src/main/webapp/public/flags/flags_th.png new file mode 100644 index 0000000..76836d8 Binary files /dev/null and b/src/main/webapp/public/flags/flags_th.png differ diff --git a/src/main/webapp/public/flags/flags_tj.png b/src/main/webapp/public/flags/flags_tj.png new file mode 100644 index 0000000..cf3384f Binary files /dev/null and b/src/main/webapp/public/flags/flags_tj.png differ diff --git a/src/main/webapp/public/flags/flags_tk.png b/src/main/webapp/public/flags/flags_tk.png new file mode 100644 index 0000000..2eb355f Binary files /dev/null and b/src/main/webapp/public/flags/flags_tk.png differ diff --git a/src/main/webapp/public/flags/flags_tl.png b/src/main/webapp/public/flags/flags_tl.png new file mode 100644 index 0000000..248c109 Binary files /dev/null and b/src/main/webapp/public/flags/flags_tl.png differ diff --git a/src/main/webapp/public/flags/flags_tm.png b/src/main/webapp/public/flags/flags_tm.png new file mode 100644 index 0000000..747f9b2 Binary files /dev/null and b/src/main/webapp/public/flags/flags_tm.png differ diff --git a/src/main/webapp/public/flags/flags_tn.png b/src/main/webapp/public/flags/flags_tn.png new file mode 100644 index 0000000..93d7228 Binary files /dev/null and b/src/main/webapp/public/flags/flags_tn.png differ diff --git a/src/main/webapp/public/flags/flags_to.png b/src/main/webapp/public/flags/flags_to.png new file mode 100644 index 0000000..e933b35 Binary files /dev/null and b/src/main/webapp/public/flags/flags_to.png differ diff --git a/src/main/webapp/public/flags/flags_tr.png b/src/main/webapp/public/flags/flags_tr.png new file mode 100644 index 0000000..69a32d4 Binary files /dev/null and b/src/main/webapp/public/flags/flags_tr.png differ diff --git a/src/main/webapp/public/flags/flags_tt.png b/src/main/webapp/public/flags/flags_tt.png new file mode 100644 index 0000000..e9c92f9 Binary files /dev/null and b/src/main/webapp/public/flags/flags_tt.png differ diff --git a/src/main/webapp/public/flags/flags_tv.png b/src/main/webapp/public/flags/flags_tv.png new file mode 100644 index 0000000..9a46257 Binary files /dev/null and b/src/main/webapp/public/flags/flags_tv.png differ diff --git a/src/main/webapp/public/flags/flags_tw.png b/src/main/webapp/public/flags/flags_tw.png new file mode 100644 index 0000000..8245128 Binary files /dev/null and b/src/main/webapp/public/flags/flags_tw.png differ diff --git a/src/main/webapp/public/flags/flags_tz.png b/src/main/webapp/public/flags/flags_tz.png new file mode 100644 index 0000000..0a6184f Binary files /dev/null and b/src/main/webapp/public/flags/flags_tz.png differ diff --git a/src/main/webapp/public/flags/flags_ua.png b/src/main/webapp/public/flags/flags_ua.png new file mode 100644 index 0000000..2779e92 Binary files /dev/null and b/src/main/webapp/public/flags/flags_ua.png differ diff --git a/src/main/webapp/public/flags/flags_ug.png b/src/main/webapp/public/flags/flags_ug.png new file mode 100644 index 0000000..d1a5f12 Binary files /dev/null and b/src/main/webapp/public/flags/flags_ug.png differ diff --git a/src/main/webapp/public/flags/flags_um.png b/src/main/webapp/public/flags/flags_um.png new file mode 100644 index 0000000..09078c5 Binary files /dev/null and b/src/main/webapp/public/flags/flags_um.png differ diff --git a/src/main/webapp/public/flags/flags_us.png b/src/main/webapp/public/flags/flags_us.png new file mode 100644 index 0000000..09078c5 Binary files /dev/null and b/src/main/webapp/public/flags/flags_us.png differ diff --git a/src/main/webapp/public/flags/flags_uy.png b/src/main/webapp/public/flags/flags_uy.png new file mode 100644 index 0000000..6e79d1a Binary files /dev/null and b/src/main/webapp/public/flags/flags_uy.png differ diff --git a/src/main/webapp/public/flags/flags_uz.png b/src/main/webapp/public/flags/flags_uz.png new file mode 100644 index 0000000..2b14e5b Binary files /dev/null and b/src/main/webapp/public/flags/flags_uz.png differ diff --git a/src/main/webapp/public/flags/flags_va.png b/src/main/webapp/public/flags/flags_va.png new file mode 100644 index 0000000..34a4cf1 Binary files /dev/null and b/src/main/webapp/public/flags/flags_va.png differ diff --git a/src/main/webapp/public/flags/flags_vc.png b/src/main/webapp/public/flags/flags_vc.png new file mode 100644 index 0000000..477cd52 Binary files /dev/null and b/src/main/webapp/public/flags/flags_vc.png differ diff --git a/src/main/webapp/public/flags/flags_ve.png b/src/main/webapp/public/flags/flags_ve.png new file mode 100644 index 0000000..163bb31 Binary files /dev/null and b/src/main/webapp/public/flags/flags_ve.png differ diff --git a/src/main/webapp/public/flags/flags_vg.png b/src/main/webapp/public/flags/flags_vg.png new file mode 100644 index 0000000..dac7003 Binary files /dev/null and b/src/main/webapp/public/flags/flags_vg.png differ diff --git a/src/main/webapp/public/flags/flags_vi.png b/src/main/webapp/public/flags/flags_vi.png new file mode 100644 index 0000000..2e65977 Binary files /dev/null and b/src/main/webapp/public/flags/flags_vi.png differ diff --git a/src/main/webapp/public/flags/flags_vn.png b/src/main/webapp/public/flags/flags_vn.png new file mode 100644 index 0000000..2009c9e Binary files /dev/null and b/src/main/webapp/public/flags/flags_vn.png differ diff --git a/src/main/webapp/public/flags/flags_vu.png b/src/main/webapp/public/flags/flags_vu.png new file mode 100644 index 0000000..f1d731b Binary files /dev/null and b/src/main/webapp/public/flags/flags_vu.png differ diff --git a/src/main/webapp/public/flags/flags_wf.png b/src/main/webapp/public/flags/flags_wf.png new file mode 100644 index 0000000..8a87712 Binary files /dev/null and b/src/main/webapp/public/flags/flags_wf.png differ diff --git a/src/main/webapp/public/flags/flags_ws.png b/src/main/webapp/public/flags/flags_ws.png new file mode 100644 index 0000000..7892b7f Binary files /dev/null and b/src/main/webapp/public/flags/flags_ws.png differ diff --git a/src/main/webapp/public/flags/flags_ye.png b/src/main/webapp/public/flags/flags_ye.png new file mode 100644 index 0000000..9f3c0f1 Binary files /dev/null and b/src/main/webapp/public/flags/flags_ye.png differ diff --git a/src/main/webapp/public/flags/flags_yt.png b/src/main/webapp/public/flags/flags_yt.png new file mode 100644 index 0000000..b40a523 Binary files /dev/null and b/src/main/webapp/public/flags/flags_yt.png differ diff --git a/src/main/webapp/public/flags/flags_za.png b/src/main/webapp/public/flags/flags_za.png new file mode 100644 index 0000000..5b929be Binary files /dev/null and b/src/main/webapp/public/flags/flags_za.png differ diff --git a/src/main/webapp/public/flags/flags_zm.png b/src/main/webapp/public/flags/flags_zm.png new file mode 100644 index 0000000..3274c5a Binary files /dev/null and b/src/main/webapp/public/flags/flags_zm.png differ diff --git a/src/main/webapp/public/flags/flags_zw.png b/src/main/webapp/public/flags/flags_zw.png new file mode 100644 index 0000000..fe2b396 Binary files /dev/null and b/src/main/webapp/public/flags/flags_zw.png differ diff --git a/src/main/webapp/src/App.jsx b/src/main/webapp/src/App.jsx index e5e6d24..bc53786 100644 --- a/src/main/webapp/src/App.jsx +++ b/src/main/webapp/src/App.jsx @@ -12,6 +12,8 @@ import './App.css' import 'react-toastify/dist/ReactToastify.css'; import {ClubRoot, getClubChildren} from "./pages/club/ClubRoot.jsx"; import {DemandeAff, DemandeAffOk} from "./pages/DemandeAff.jsx"; +import {MePage} from "./pages/MePage.jsx"; +import {CompetitionRoot, getCompetitionChildren} from "./pages/competition/CompetitionRoot.jsx"; const router = createBrowserRouter([ { @@ -45,6 +47,15 @@ const router = createBrowserRouter([ element: } ] + }, + { + path: 'competition', + element: , + children: getCompetitionChildren() + }, + { + path: 'me', + element: } ] }, @@ -75,6 +86,14 @@ function Root() { check_validity(data => dispatch({type: 'init', val: data})) }, []); + + useEffect(() => { + const interval = setInterval(() => { + check_validity(data => dispatch({type: 'update', val: data})) + }, 1000 * 60 * 9) + return () => clearInterval(interval) + }, []); + return <>