package fr.titionfire.ffsaf.domain.service; import fr.titionfire.ffsaf.data.model.ClubModel; import fr.titionfire.ffsaf.data.model.MembreModel; import fr.titionfire.ffsaf.utils.KeycloakException; import fr.titionfire.ffsaf.utils.RequiredAction; 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.List; import java.util.Optional; @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; @Inject Vertx vertx; public Uni 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 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().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"; }))); } public Uni fetchCompte(String id) { return vertx.getOrCreateContext().executeBlocking(() -> { UserResource user = keycloak.realm(realm).users().get(id); UserRepresentation user2 = user.toRepresentation(); return new UserCompteState(user2.isEnabled(), user2.getUsername(), user2.isEmailVerified(), user.roles().realmLevel().listEffective().stream().map(RoleRepresentation::getName).toList(), user.groups().stream().map(GroupRepresentation::getName).toList()); }); } public Uni> 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 toAdd, List toRemove) { return vertx.getOrCreateContext().executeBlocking(() -> { RoleScopeResource resource = keycloak.realm(realm).users().get(id).roles().realmLevel(); List 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 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(__ -> "OK"); } private Uni creatUser(MembreModel membreModel) { String login = makeLogin(membreModel); LOGGER.infof("Creation of user %s...", login); return vertx.getOrCreateContext().executeBlocking(() -> { UserRepresentation user = new UserRepresentation(); user.setUsername(login); user.setFirstName(membreModel.getFname()); user.setLastName(membreModel.getLname()); user.setEmail(membreModel.getEmail()); user.setEnabled(true); user.setRequiredActions(List.of(RequiredAction.VERIFY_EMAIL.name(), 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)) throw new KeycloakException("Fail to creat user %s (reason=%s)".formatted(login, response.getStatusInfo().getReasonPhrase())); } return getUser(login).orElseThrow(() -> new KeycloakException("Fail to fetch user %s".formatted(login))); }) .invoke(user -> membreModel.setUserId(user.getId())) .call(user -> membreService.setUserId(membreModel.getId(), user.getId())) .call(user -> setClubGroupMembre(membreModel, membreModel.getClub())); } private Optional getUser(String username) { List users = keycloak.realm(realm).users().searchByUsername(username, true); if (users.isEmpty()) return Optional.empty(); else return Optional.of(users.get(0)); } private String makeLogin(MembreModel model) { return Normalizer.normalize((model.getFname().toLowerCase() + "." + model.getLname().toLowerCase()).replace(' ', '_'), Normalizer.Form.NFD) .replaceAll("\\p{M}", ""); } public record UserCompteState(Boolean enabled, String login, Boolean emailVerified, List realmRoles, List groups) { } }