371 lines
19 KiB
Java
371 lines
19 KiB
Java
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.mailer.Mail;
|
|
import io.quarkus.mailer.reactive.ReactiveMailer;
|
|
import io.quarkus.runtime.annotations.RegisterForReflection;
|
|
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 jakarta.ws.rs.core.Response;
|
|
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
|
import org.jboss.logging.Logger;
|
|
import org.keycloak.admin.client.Keycloak;
|
|
import org.keycloak.admin.client.resource.RoleScopeResource;
|
|
import org.keycloak.admin.client.resource.UserResource;
|
|
import org.keycloak.representations.idm.GroupRepresentation;
|
|
import org.keycloak.representations.idm.RoleRepresentation;
|
|
import org.keycloak.representations.idm.UserRepresentation;
|
|
|
|
import java.text.Normalizer;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Optional;
|
|
import java.util.UUID;
|
|
|
|
@ApplicationScoped
|
|
public class KeycloakService {
|
|
private static final Logger LOGGER = Logger.getLogger(KeycloakService.class);
|
|
|
|
@Inject
|
|
Keycloak keycloak;
|
|
|
|
@Inject
|
|
ClubService clubService;
|
|
|
|
@Inject
|
|
MembreService membreService;
|
|
|
|
@ConfigProperty(name = "keycloak.realm")
|
|
String realm;
|
|
|
|
@ConfigProperty(name = "email.enabled")
|
|
boolean enabled_email;
|
|
|
|
@Inject
|
|
ReactiveMailer reactiveMailer;
|
|
|
|
@Inject
|
|
Vertx vertx;
|
|
|
|
public Uni<String> getGroupFromClub(ClubModel club) {
|
|
if (club.getClubId() == null) {
|
|
LOGGER.infof("Creation of club group %d-%s...", club.getId(), club.getName());
|
|
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")));
|
|
|
|
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()));
|
|
}
|
|
|
|
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() + "-")));
|
|
}
|
|
).call(id -> clubService.setClubId(club.getId(), id));
|
|
}
|
|
return Uni.createFrom().item(club::getClubId);
|
|
}
|
|
|
|
public Uni<String> updateGroupFromClub(ClubModel club) {
|
|
if (club.getClubId() == null) {
|
|
return getGroupFromClub(club);
|
|
} else {
|
|
LOGGER.infof("Updating name of club group %d-%s...", club.getId(), club.getName());
|
|
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")));
|
|
|
|
keycloak.realm(realm).groups().group(clubGroup.getId()).getSubGroups(0, 1000, true).stream()
|
|
.filter(g -> g.getName().startsWith(club.getId() + "-")).findAny()
|
|
.ifPresent(groupRepresentation -> {
|
|
groupRepresentation.setName(club.getId() + "-" + club.getName());
|
|
keycloak.realm(realm).groups().group(groupRepresentation.getId())
|
|
.update(groupRepresentation);
|
|
});
|
|
|
|
return club.getClubId();
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
public Uni<String> getUserFromMember(MembreModel membreModel) {
|
|
if (membreModel.getUserId() == null) {
|
|
return Uni.createFrom()
|
|
.failure(new DInternalError("No keycloak user linked to the user id=" + membreModel.getId()));
|
|
}
|
|
return Uni.createFrom().item(membreModel::getUserId);
|
|
}
|
|
|
|
public Uni<String> setClubGroupMembre(MembreModel membreModel, ClubModel club) {
|
|
if (club == null)
|
|
return 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()));
|
|
return "OK";
|
|
}));
|
|
else
|
|
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";
|
|
})));
|
|
}
|
|
|
|
public Uni<?> setEmail(String userId, String email) {
|
|
return vertx.getOrCreateContext().executeBlocking(() -> {
|
|
UserResource user = keycloak.realm(realm).users().get(userId);
|
|
UserRepresentation user2 = user.toRepresentation();
|
|
String oldEmail = user2.getEmail();
|
|
if (email.equals(user2.getEmail()))
|
|
return null;
|
|
user2.setEmail(email);
|
|
user2.setRequiredActions(List.of(RequiredAction.VERIFY_EMAIL.name()));
|
|
user.update(user2);
|
|
if (enabled_email)
|
|
user.sendVerifyEmail();
|
|
return oldEmail;
|
|
}).call(oldEmail -> oldEmail == null || !enabled_email ? Uni.createFrom().item("") :
|
|
reactiveMailer.send(
|
|
Mail.withText(oldEmail,
|
|
"FFSAF - Changement de votre adresse email",
|
|
String.format(
|
|
"""
|
|
Bonjour,
|
|
|
|
Suite à la modification de votre adresse email fournie lors de votre (ré)inscription à la FFSAF,
|
|
vous allez recevoir dans les prochaines minutes un email de vérification de votre nouvelle adresse sur celle-ci.
|
|
|
|
Ancienne adresse email : %s
|
|
Nouvelle adresse email : %s
|
|
|
|
Si vous n'avez pas demandé cette modification, veuillez contacter le support à l'adresse support@ffsaf.fr.
|
|
|
|
Cordialement,
|
|
L'équipe de la FFSAF
|
|
""", oldEmail, email)
|
|
).setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("support@ffsaf.fr")
|
|
).onFailure().invoke(e -> LOGGER.error("Fail to send email", e)));
|
|
}
|
|
|
|
public Uni<?> setAutoRoleMembre(String id, RoleAsso role, GradeArbitrage gradeArbitrage) {
|
|
List<String> toRemove = new ArrayList<>(List.of("club_president", "club_tresorier", "club_secretaire",
|
|
"club_respo_intra", "asseseur", "arbitre"));
|
|
List<String> toAdd = new ArrayList<>();
|
|
|
|
switch (role) {
|
|
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"));
|
|
case ASSESSEUR -> toAdd.add("asseseur");
|
|
}
|
|
toRemove.removeAll(toAdd);
|
|
|
|
return updateRole(id, toAdd, toRemove);
|
|
}
|
|
|
|
public Uni<Pair<UserResource, UserCompteState>> fetchCompte(String id) {
|
|
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()));
|
|
});
|
|
}
|
|
|
|
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());
|
|
}
|
|
|
|
public Uni<?> updateRole(String id, List<String> toAdd, List<String> toRemove) {
|
|
return vertx.getOrCreateContext().executeBlocking(() -> {
|
|
RoleScopeResource resource = keycloak.realm(realm).users().get(id).roles().realmLevel();
|
|
List<RoleRepresentation> roles = keycloak.realm(realm).roles().list();
|
|
resource.add(roles.stream().filter(r -> toAdd.contains(r.getName())).toList());
|
|
resource.remove(roles.stream().filter(r -> toRemove.contains(r.getName())).toList());
|
|
return "OK";
|
|
});
|
|
}
|
|
|
|
public Uni<String> initCompte(long id) {
|
|
return membreService.getById(id).invoke(Unchecked.consumer(membreModel -> {
|
|
if (membreModel.getUserId() != null)
|
|
throw new KeycloakException("User already linked to the user id=" + id);
|
|
if (membreModel.getEmail() == null)
|
|
throw new KeycloakException("User email is null");
|
|
if (membreModel.getFname() == null || membreModel.getLname() == null)
|
|
throw new KeycloakException("User name is null");
|
|
})).chain(membreModel -> creatUser(membreModel).chain(user -> {
|
|
LOGGER.infof("Set user id %s to membre %s", user.getId(), membreModel.getId());
|
|
return membreService.setUserId(membreModel.getId(), user.getId()).map(__ -> user.getId());
|
|
}));
|
|
}
|
|
|
|
private Uni<UserRepresentation> creatUser(MembreModel membreModel) {
|
|
return vertx.getOrCreateContext().executeBlocking(() -> {
|
|
String login;
|
|
int i = 1;
|
|
do {
|
|
login = makeLogin(membreModel);
|
|
if (i > 1) {
|
|
login += i;
|
|
}
|
|
i++;
|
|
} while (!keycloak.realm(realm).users().searchByUsername(login, true).isEmpty());
|
|
LOGGER.infof("Creation of user %s...", login);
|
|
|
|
UserRepresentation user = new UserRepresentation();
|
|
user.setUsername(login);
|
|
user.setFirstName(membreModel.getFname());
|
|
user.setLastName(membreModel.getLname());
|
|
user.setEmail(membreModel.getEmail());
|
|
user.setEnabled(true);
|
|
|
|
try (Response response = keycloak.realm(realm).users().create(user)) {
|
|
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)));
|
|
})
|
|
.invoke(user -> membreModel.setUserId(user.getId()))
|
|
.call(user -> updateRole(user.getId(), List.of("safca_user"), List.of()))
|
|
.call(user -> enabled_email ? reactiveMailer.send(
|
|
Mail.withText(user.getEmail(),
|
|
"FFSAF - Creation de votre compte sur l'intranet",
|
|
String.format(
|
|
"""
|
|
Bonjour,
|
|
|
|
Suite à votre première inscription %sà la Fédération Française de Soft Armored Fighting (FFSAF), votre compte intranet a été créé.
|
|
Ce compte vous permettra de consulter vos informations et, dans un futur proche, de vous inscrire aux compétitions ainsi que d'en consulter les résultats.
|
|
|
|
L'intranet est accessible à l'adresse suivante : https://intra.ffsaf.fr
|
|
Votre nom d'utilisateur est : %s
|
|
|
|
Pour définir votre mot de passe, rendez-vous sur l'intranet > "Connexion" > "Mot de passe oublié ?"
|
|
|
|
Si vous n'avez pas demandé cette inscription, veuillez contacter le support à l'adresse support@ffsaf.fr.
|
|
(Pas de panique, nous ne vous enverrons pas de message autre que ce concernant votre compte)
|
|
|
|
Cordialement,
|
|
L'équipe de la FFSAF
|
|
""",
|
|
membreModel.getRole() == RoleAsso.MEMBRE ? "par votre club (" + membreModel.getClub()
|
|
.getName() + ") " : "", user.getUsername())
|
|
).setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("support@ffsaf.fr")
|
|
) : Uni.createFrom().nullItem())
|
|
.call(user -> membreService.setUserId(membreModel.getId(), user.getId()))
|
|
.call(user -> setClubGroupMembre(membreModel, membreModel.getClub()));
|
|
}
|
|
|
|
public Uni<?> setId(long id, String nid) {
|
|
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;
|
|
});
|
|
}
|
|
|
|
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";
|
|
});
|
|
}
|
|
|
|
public Optional<UserRepresentation> getUser(String username) {
|
|
List<UserRepresentation> users = keycloak.realm(realm).users().searchByUsername(username, true);
|
|
|
|
if (users.isEmpty())
|
|
return Optional.empty();
|
|
else
|
|
return Optional.of(users.get(0));
|
|
}
|
|
|
|
|
|
public Optional<UserRepresentation> 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)
|
|
.replaceAll("\\p{M}", "");
|
|
|
|
}
|
|
|
|
@RegisterForReflection
|
|
public record UserCompteState(Boolean enabled, String login, Boolean emailVerified) {
|
|
}
|
|
}
|