diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/KeycloakService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/KeycloakService.java index df578b9..a70711d 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/KeycloakService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/KeycloakService.java @@ -121,7 +121,7 @@ public class KeycloakService { return vertx.getOrCreateContext().executeBlocking(() -> { UserResource user = keycloak.realm(realm).users().get(id); UserRepresentation user2 = user.toRepresentation(); - return new Pair<>(user, new UserCompteState(user2.isEnabled(), user2.getUsername(), user2.isEmailVerified())) ; + return new Pair<>(user, new UserCompteState(user2.isEnabled(), user2.getUsername(), user2.isEmailVerified())); }); } @@ -195,6 +195,17 @@ public class KeycloakService { return membreService.setUserId(id, nid).map(__ -> "OK"); } + public Uni removeAccount(String userId) { + return vertx.getOrCreateContext().executeBlocking(() -> { + try (Response response = keycloak.realm(realm).users().delete(userId)) { + System.out.println(response.getStatusInfo()); + if (!response.getStatusInfo().equals(Response.Status.NO_CONTENT)) + throw new KeycloakException("Fail to delete user %s (reason=%s)".formatted(userId, response.getStatusInfo().getReasonPhrase())); + } + return null; + }); + } + private Optional getUser(String username) { List users = keycloak.realm(realm).users().searchByUsername(username, true); diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java index 5eac2ce..cdea2aa 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java @@ -4,6 +4,7 @@ import fr.titionfire.ffsaf.data.model.ClubModel; import fr.titionfire.ffsaf.data.model.MembreModel; import fr.titionfire.ffsaf.data.repository.ClubRepository; import fr.titionfire.ffsaf.data.repository.CombRepository; +import fr.titionfire.ffsaf.data.repository.LicenceRepository; import fr.titionfire.ffsaf.net2.ServerCustom; import fr.titionfire.ffsaf.net2.data.SimpleCombModel; import fr.titionfire.ffsaf.net2.request.SReqComb; @@ -35,6 +36,9 @@ public class MembreService { CombRepository repository; @Inject ClubRepository clubRepository; + @Inject + LicenceRepository licenceRepository; + @Inject ServerCustom serverCustom; @Inject @@ -183,6 +187,33 @@ public class MembreService { .map(MembreModel::getId); } + public Uni delete(long id) { + return repository.findById(id) + .call(membreModel -> (membreModel.getUserId() != null) ? + keycloakService.removeAccount(membreModel.getUserId()) : Uni.createFrom().nullItem()) + .call(membreModel -> Panache.withTransaction(() -> repository.delete(membreModel))) + .invoke(membreModel -> SReqComb.sendRm(serverCustom.clients, id)) + .map(__ -> "Ok"); + } + + public Uni delete(long id, JsonWebToken idToken) { + return repository.findById(id) + .invoke(Unchecked.consumer(membreModel -> { + if (!GroupeUtils.isInClubGroup(membreModel.getClub().getId(), idToken)) + throw new ForbiddenException(); + })) + .call(membreModel -> licenceRepository.find("membre = ?1", membreModel).count() + .invoke(Unchecked.consumer(l -> { + if (l > 0) + throw new BadRequestException(); + }))) + .call(membreModel -> (membreModel.getUserId() != null) ? + keycloakService.removeAccount(membreModel.getUserId()) : Uni.createFrom().nullItem()) + .call(membreModel -> Panache.withTransaction(() -> repository.delete(membreModel))) + .invoke(membreModel -> SReqComb.sendRm(serverCustom.clients, id)) + .map(__ -> "Ok"); + } + public Uni setUserId(Long id, String id1) { return repository.findById(id).chain(membreModel -> { membreModel.setUserId(id1); diff --git a/src/main/java/fr/titionfire/ffsaf/rest/CombEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/CombEndpoints.java index 1c69154..d5c5dfa 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/CombEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/CombEndpoints.java @@ -129,6 +129,14 @@ public class CombEndpoints { }); } + @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"}) @@ -167,6 +175,40 @@ public class CombEndpoints { }); } + @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"}) diff --git a/src/main/webapp/src/components/ConfirmDialog.jsx b/src/main/webapp/src/components/ConfirmDialog.jsx new file mode 100644 index 0000000..b3719ad --- /dev/null +++ b/src/main/webapp/src/components/ConfirmDialog.jsx @@ -0,0 +1,19 @@ + +export function ConfirmDialog({title, message, onConfirm = () => {}, onCancel = () => {}, id = "confirm-delete"}) { + return +} \ No newline at end of file diff --git a/src/main/webapp/src/pages/admin/member/MemberPage.jsx b/src/main/webapp/src/pages/admin/member/MemberPage.jsx index badac6f..d2dd0dd 100644 --- a/src/main/webapp/src/pages/admin/member/MemberPage.jsx +++ b/src/main/webapp/src/pages/admin/member/MemberPage.jsx @@ -6,6 +6,9 @@ import {CompteInfo} from "./CompteInfo.jsx"; import {PremForm} from "./PremForm.jsx"; import {InformationForm} from "./InformationForm.jsx"; import {LicenceCard} from "./LicenceCard.jsx"; +import {toast} from "react-toastify"; +import {apiAxios} from "../../../utils/Tools.js"; +import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx"; const vite_url = import.meta.env.VITE_URL; @@ -16,6 +19,19 @@ export function MemberPage() { const setLoading = useLoadingSwitcher() const {data, error} = useFetch(`/member/${id}`, setLoading, 1) + const handleRm = () => { + toast.promise( + apiAxios.delete(`/member/${id}`), + { + pending: "Suppression du compte en cours...", + success: "Compte supprimé avec succès 🎉", + error: "Échec de la suppression du compte 😕" + } + ).then(_ => { + navigate("/admin/member") + }) + } + return <>

Page membre

+ + diff --git a/src/main/webapp/src/pages/club/member/MemberPage.jsx b/src/main/webapp/src/pages/club/member/MemberPage.jsx index 622fd5d..634fa3b 100644 --- a/src/main/webapp/src/pages/club/member/MemberPage.jsx +++ b/src/main/webapp/src/pages/club/member/MemberPage.jsx @@ -5,6 +5,9 @@ import {AxiosError} from "../../../components/AxiosError.jsx"; import {CompteInfo} from "./CompteInfo.jsx"; import {InformationForm} from "./InformationForm.jsx"; import {LicenceCard} from "./LicenceCard.jsx"; +import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx"; +import {apiAxios} from "../../../utils/Tools.js"; +import {toast} from "react-toastify"; const vite_url = import.meta.env.VITE_URL; @@ -15,6 +18,19 @@ export function MemberPage() { const setLoading = useLoadingSwitcher() const {data, error} = useFetch(`/member/${id}`, setLoading, 1) + const handleRm = () => { + toast.promise( + apiAxios.delete(`/member/club/${id}`), + { + pending: "Suppression du compte en cours...", + success: "Compte supprimé avec succès 🎉", + error: "Échec de la suppression du compte 😕" + } + ).then(_ => { + navigate("/club/member") + }) + } + return <>

Page membre

+ +