feat: add membre

This commit is contained in:
Thibaut Valentin 2024-03-06 19:57:17 +01:00
parent 9615660101
commit bbc8c470c5
6 changed files with 148 additions and 1 deletions

View File

@ -121,7 +121,7 @@ public class KeycloakService {
return vertx.getOrCreateContext().executeBlocking(() -> { return vertx.getOrCreateContext().executeBlocking(() -> {
UserResource user = keycloak.realm(realm).users().get(id); UserResource user = keycloak.realm(realm).users().get(id);
UserRepresentation user2 = user.toRepresentation(); 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"); 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<UserRepresentation> getUser(String username) { private Optional<UserRepresentation> getUser(String username) {
List<UserRepresentation> users = keycloak.realm(realm).users().searchByUsername(username, true); List<UserRepresentation> users = keycloak.realm(realm).users().searchByUsername(username, true);

View File

@ -4,6 +4,7 @@ import fr.titionfire.ffsaf.data.model.ClubModel;
import fr.titionfire.ffsaf.data.model.MembreModel; import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.data.repository.ClubRepository; import fr.titionfire.ffsaf.data.repository.ClubRepository;
import fr.titionfire.ffsaf.data.repository.CombRepository; 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.ServerCustom;
import fr.titionfire.ffsaf.net2.data.SimpleCombModel; import fr.titionfire.ffsaf.net2.data.SimpleCombModel;
import fr.titionfire.ffsaf.net2.request.SReqComb; import fr.titionfire.ffsaf.net2.request.SReqComb;
@ -35,6 +36,9 @@ public class MembreService {
CombRepository repository; CombRepository repository;
@Inject @Inject
ClubRepository clubRepository; ClubRepository clubRepository;
@Inject
LicenceRepository licenceRepository;
@Inject @Inject
ServerCustom serverCustom; ServerCustom serverCustom;
@Inject @Inject
@ -183,6 +187,33 @@ public class MembreService {
.map(MembreModel::getId); .map(MembreModel::getId);
} }
public Uni<String> 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<String> 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) { public Uni<?> setUserId(Long id, String id1) {
return repository.findById(id).chain(membreModel -> { return repository.findById(id).chain(membreModel -> {
membreModel.setUserId(id1); membreModel.setUserId(id1);

View File

@ -129,6 +129,14 @@ public class CombEndpoints {
}); });
} }
@DELETE
@Path("{id}")
@RolesAllowed({"federation_admin"})
@Produces(MediaType.TEXT_PLAIN)
public Uni<String> deleteAdminMembre(@PathParam("id") long id) {
return membreService.delete(id);
}
@PUT @PUT
@Path("club/{id}") @Path("club/{id}")
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"}) @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<String> deleteMembre(@PathParam("id") long id) {
return membreService.delete(id, idToken);
}
private Future<String> 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 @GET
@Path("{id}/photo") @Path("{id}/photo")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"})

View File

@ -0,0 +1,19 @@
export function ConfirmDialog({title, message, onConfirm = () => {}, onCancel = () => {}, id = "confirm-delete"}) {
return <div className="modal fade" id={id} tabIndex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
{title && <h4 className="modal-title" id="myModalLabel">{title}</h4>}
</div>
<div className="modal-body">
{message}
</div>
<div className="modal-footer">
<button type="button" className="btn btn-default" data-dismiss="modal" data-bs-dismiss="modal" onClick={onCancel}>Annuler</button>
<a className="btn btn-danger btn-ok" data-bs-dismiss="modal" onClick={onConfirm}>Confirmer</a>
</div>
</div>
</div>
</div>
}

View File

@ -6,6 +6,9 @@ import {CompteInfo} from "./CompteInfo.jsx";
import {PremForm} from "./PremForm.jsx"; import {PremForm} from "./PremForm.jsx";
import {InformationForm} from "./InformationForm.jsx"; import {InformationForm} from "./InformationForm.jsx";
import {LicenceCard} from "./LicenceCard.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; const vite_url = import.meta.env.VITE_URL;
@ -16,6 +19,19 @@ export function MemberPage() {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/member/${id}`, setLoading, 1) 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 <> return <>
<h2>Page membre</h2> <h2>Page membre</h2>
<button type="button" className="btn btn-link" onClick={() => navigate("/admin/member")}> <button type="button" className="btn btn-link" onClick={() => navigate("/admin/member")}>
@ -39,6 +55,12 @@ export function MemberPage() {
<LoadingProvider><SelectCard/></LoadingProvider> <LoadingProvider><SelectCard/></LoadingProvider>
</div> </div>
</div> </div>
<div className="col" style={{textAlign: 'right', marginTop: '1em'}}>
<button className="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#confirm-delete">Supprimer le compte
</button>
</div>
<ConfirmDialog title="Supprimer le compte" message="Êtes-vous sûr de vouloir supprimer ce compte ?"
onConfirm={handleRm}/>
</div> </div>
</div> </div>
</div> </div>

View File

@ -5,6 +5,9 @@ import {AxiosError} from "../../../components/AxiosError.jsx";
import {CompteInfo} from "./CompteInfo.jsx"; import {CompteInfo} from "./CompteInfo.jsx";
import {InformationForm} from "./InformationForm.jsx"; import {InformationForm} from "./InformationForm.jsx";
import {LicenceCard} from "./LicenceCard.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; const vite_url = import.meta.env.VITE_URL;
@ -15,6 +18,19 @@ export function MemberPage() {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/member/${id}`, setLoading, 1) 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 <> return <>
<h2>Page membre</h2> <h2>Page membre</h2>
<button type="button" className="btn btn-link" onClick={() => navigate("/club/member")}> <button type="button" className="btn btn-link" onClick={() => navigate("/club/member")}>
@ -37,6 +53,12 @@ export function MemberPage() {
<LoadingProvider><SelectCard/></LoadingProvider> <LoadingProvider><SelectCard/></LoadingProvider>
</div> </div>
</div> </div>
<div className="col" style={{textAlign: 'right', marginTop: '1em'}}>
<button className="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#confirm-delete">Supprimer le compte
</button>
</div>
<ConfirmDialog title="Supprimer le compte" message="Êtes-vous sûr de vouloir supprimer ce compte ?"
onConfirm={handleRm}/>
</div> </div>
</div> </div>
</div> </div>