feat: club remove

This commit is contained in:
Thibaut Valentin 2024-07-16 22:08:02 +02:00
parent 6407bf44bc
commit f297ae557b
11 changed files with 165 additions and 38 deletions

View File

@ -55,6 +55,6 @@ public class ClubModel {
boolean international;
@OneToMany(mappedBy = "club", fetch = FetchType.LAZY)
@OneToMany(mappedBy = "club", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
List<AffiliationModel> affiliations;
}

View File

@ -55,15 +55,29 @@ public class AffiliationService {
public Uni<String> save(AffiliationRequestForm form) {
AffiliationRequestModel affModel = form.toModel();
affModel.setSaison(Utils.getSaison());
int currentSaison = Utils.getSaison();
// noinspection ResultOfMethodCallIgnored
return repositoryRequest.count("siret = ?1 and saison = ?2", affModel.getSiret(), affModel.getSaison())
return Uni.createFrom().item(affModel)
.invoke(Unchecked.consumer(model -> {
if (model.getSaison() != currentSaison && model.getSaison() != currentSaison + 1) {
throw new IllegalArgumentException("Saison not valid");
}
}))
.chain(() -> repositoryRequest.count("siret = ?1 and saison = ?2", affModel.getSiret(),
affModel.getSaison()))
.onItem().invoke(Unchecked.consumer(count -> {
if (count != 0) {
throw new IllegalArgumentException("Affiliation request already exists");
}
}))
.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 IllegalArgumentException("Affiliation already exists");
}
}))
.map(o -> affModel)
.call(model -> ((model.getM1_lincence() != -1) ? combRepository.find("licence",
model.getM1_lincence()).count().invoke(count -> {
@ -289,7 +303,12 @@ public class AffiliationService {
public Uni<SimpleAffiliation> setAffiliation(long id, int saison) {
return clubRepository.findById(id)
.onItem().ifNull().failWith(new NotFoundException("Affiliation request not found"))
.onItem().ifNull().failWith(new NotFoundException("Club non trouver"))
.invoke(Unchecked.consumer(club -> {
if (club.getAffiliations().stream().anyMatch(affiliation -> affiliation.getSaison() == saison)) {
throw new IllegalArgumentException("Affiliation deja existante");
}
}))
.chain(club ->
Panache.withTransaction(() -> repository.persist(new AffiliationModel(null, club, saison))))
.map(SimpleAffiliation::fromModel);

View File

@ -4,12 +4,15 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import fr.titionfire.ffsaf.data.model.ClubModel;
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.from.FullClubForm;
import fr.titionfire.ffsaf.utils.Contact;
import fr.titionfire.ffsaf.utils.PageResult;
import fr.titionfire.ffsaf.utils.RoleAsso;
import fr.titionfire.ffsaf.utils.Utils;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.PanacheQuery;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
@ -21,6 +24,7 @@ import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.BadRequestException;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.hibernate.reactive.mutiny.Mutiny;
import java.util.Collection;
@ -39,6 +43,15 @@ public class ClubService {
@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)));
@ -115,6 +128,7 @@ public class ClubService {
m.setContact_intern(input.getContact_intern());
m.setRNA(input.getRna());
m.setSIRET(input.getSiret());
m.setAddress(input.getAddress());
try {
m.setContact(MAPPER.readValue(input.getContact(), typeRef));
@ -134,6 +148,22 @@ public class ClubService {
}
public Uni<?> delete(long id) {
return Uni.createFrom().nullItem();
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 -> 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"));
}
}

View File

@ -49,21 +49,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 +77,24 @@ public class KeycloakService {
public Uni<String> 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 NullPointerException("No keycloak user linked to the user id=" + membreModel.getId()));
}
return Uni.createFrom().item(membreModel::getUserId);
}
public Uni<String> 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 +112,14 @@ public class KeycloakService {
public Uni<?> setAutoRoleMembre(String id, RoleAsso role, GradeArbitrage gradeArbitrage) {
List<String> toRemove = new ArrayList<>(List.of("club_president", "club_tresorier", "club_secretaire",
"asseseur", "arbitre"));
"club_respo_intra", "asseseur", "arbitre"));
List<String> toAdd = new ArrayList<>();
switch (role) {
case PRESIDENT -> toAdd.add("club_president");
case TRESORIER -> toAdd.add("club_tresorier");
case SECRETAIRE -> toAdd.add("club_secretaire");
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 +141,8 @@ public class KeycloakService {
public Uni<List<String>> 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<String> toAdd, List<String> toRemove) {
@ -184,13 +194,15 @@ 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()) // TODO enable for production
// .executeActionsEmail(List.of(RequiredAction.VERIFY_EMAIL.name(),
@ -216,6 +228,30 @@ public class KeycloakService {
});
}
public Uni<?> removeClubGroup(String clubId) {
return vertx.getOrCreateContext().executeBlocking(() -> {
keycloak.realm(realm).groups().group(clubId).remove();
return null;
});
}
public Uni<?> clearUser(String userId) {
List<String> 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<RoleRepresentation> 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";
});
}
private Optional<UserRepresentation> getUser(String username) {
List<UserRepresentation> users = keycloak.realm(realm).users().searchByUsername(username, true);
@ -226,7 +262,9 @@ public class KeycloakService {
}
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}", "");
}

View File

@ -150,7 +150,7 @@ public class MembreService {
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;
else if (securityIdentity.getRoles().contains("club_respo_intra")) source = RoleAsso.MEMBREBUREAU;
if (!membre.getRole().equals(membreModel.getRole()) && membre.getRole().level > source.level)
throw new ForbiddenException();
}))

View File

@ -159,8 +159,15 @@ public class ClubEndpoints {
@GET
@Path("{id}/status")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire"})
public Uni<Response> getStatus(@PathParam("id") long id) throws URISyntaxException {
return Utils.getMediaFile(id, media, "clubStatus", clubService.getById(id).onItem().invoke(checkPerm));
public Uni<Response> getStatus(@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();
}
}));
}
}

View File

@ -25,6 +25,7 @@ public class SimpleClub {
private String training_location;
private String training_day_time;
private String contact_intern;
private String address;
private String RNA;
private Long SIRET;
private Long no_affiliation;
@ -48,6 +49,7 @@ public class SimpleClub {
.SIRET(model.getSIRET())
.no_affiliation(model.getNo_affiliation())
.international(model.isInternational())
.address(model.getAddress())
.build();
}
}

View File

@ -13,15 +13,14 @@ import org.jboss.resteasy.reactive.PartType;
public class AffiliationRequestForm {
@FormParam("name")
private String name = null;
@FormParam("siret")
private Long siret = null;
@FormParam("rna")
private String rna = null;
@FormParam("adresse")
private String adresse = null;
@FormParam("saison")
private int saison = -1;
@FormParam("status")
@PartType(MediaType.APPLICATION_OCTET_STREAM)
@ -70,6 +69,7 @@ public class AffiliationRequestForm {
model.setSiret(this.getSiret());
model.setRNA(this.getRna());
model.setAddress(this.getAdresse());
model.setSaison(this.getSaison());
model.setM1_lname(this.getM1_lname());
model.setM1_fname(this.getM1_fname());

View File

@ -30,6 +30,9 @@ public class FullClubForm {
@FormParam("contact_intern")
private String contact_intern = null;
@FormParam("address")
private String address = null;
@FormParam("rna")
private String rna = null;

View File

@ -1,5 +1,5 @@
import {useState} from "react";
import {apiAxios} from "../utils/Tools.js";
import {apiAxios, getSaison} from "../utils/Tools.js";
import {toast} from "react-toastify";
import {useNavigate} from "react-router-dom";
import {RoleList} from "../components/MemberCustomFiels.jsx";
@ -130,6 +130,7 @@ function AssoInfo() {
const [rna, setRna] = useState("")
const [rnaEnable, setRnaEnable] = useState(false)
const [adresse, setAdresse] = useState("")
const [saison, setSaison] = useState(getSaison())
const fetchSiret = () => {
if (siret.length < 14) {
@ -153,7 +154,26 @@ function AssoInfo() {
})
}
const currentSaison = getSaison();
return <>
<div className="input-group mb-3">
<div className="input-group-text">
<input className="form-check-input mt-0" type="radio" value={currentSaison} aria-label={currentSaison + "-" + (currentSaison + 1)}
name={"saison"} checked={saison === currentSaison}
onChange={e => setSaison(Number(e.target.value))}/>
{currentSaison + "-" + (currentSaison + 1)}
</div>
<span className="input-group-text">OU</span>
<div className="input-group-text">
<input className="form-check-input mt-0" type="radio" value={currentSaison + 1}
aria-label={(currentSaison + 1) + "-" + (currentSaison + 2)}
name={"saison"} checked={saison === currentSaison + 1}
onChange={e => setSaison(Number(e.target.value))}/>
{(currentSaison + 1) + "-" + (currentSaison + 2)}
</div>
</div>
<div className="input-group mb-3">
<span className="input-group-text" id="basic-addon1">Nom de l'association*</span>
<input type="text" className="form-control" placeholder="Nom de l'association" name="name"
@ -186,8 +206,8 @@ function AssoInfo() {
<div className="mb-3">
<div className="input-group">
<span className="input-group-text" id="basic-addon1">Adresse de contact*</span>
<input type="text" className="form-control" placeholder="Adresse de contact" aria-label="Adresse de contact"
<span className="input-group-text" id="basic-addon1">Adresse administrative*</span>
<input type="text" className="form-control" placeholder="Adresse administrative" aria-label="Adresse administrative"
aria-describedby="basic-addon1"
required value={adresse} name="adresse" onChange={e => setAdresse(e.target.value)}/>
</div>

View File

@ -12,6 +12,8 @@ import {useRef, useState} from "react";
import {LocationEditor, LocationEditorModal} from "../../../components/Club/LocationEditor.jsx";
import {ContactEditor} from "../../../components/Club/ContactEditor.jsx";
import {HoraireEditor} from "../../../components/Club/HoraireEditor.jsx";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faFilePdf} from "@fortawesome/free-solid-svg-icons";
const vite_url = import.meta.env.VITE_URL;
@ -122,11 +124,17 @@ function InformationForm({data}) {
<TextField name="rna" text="RNA" value={data.rna} required={false}/>
<TextField name="contact_intern" text="Contact interne" value={data.contact_intern} required={false}
placeholder="example@test.com"/>
<TextField name="address" text="Adresse administrative" value={data.address} placeholder="Adresse administrative"/>
<div className="mb-3">
<div className="input-group">
<label className="input-group-text" htmlFor="status">Status</label>
<input type="file" className="form-control" id="status" name="status" accept=".pdf,.txt" required/>
<a href={`${vite_url}/api/club/${data.id}/status`} target='_blank'>
<button className="btn btn-outline-secondary" type="button" id="button-addon1"
onClick={e => null}><FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon>
</button>
</a>
<input type="file" className="form-control" id="status" name="status" accept=".pdf,.txt"/>
</div>
<div className="form-text" id="status">Laissez vide pour ne rien changer.</div>
</div>