Thibaut Valentin 22d742ab63
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 4m54s
fix: re-enable email kc feature
2025-01-22 20:22:10 +01:00

354 lines
18 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> 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(__ -> "OK");
}
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);
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()));
}
String finalLogin = login;
return getUser(login).orElseThrow(
() -> new KeycloakException("Fail to fetch user %s".formatted(finalLogin)));
})
.call(user -> enabled_email ?
vertx.getOrCreateContext().executeBlocking(() -> {
keycloak.realm(realm).users().get(user.getId())
.executeActionsEmail(List.of(RequiredAction.VERIFY_EMAIL.name(),
RequiredAction.UPDATE_PASSWORD.name()));
return null;
}) : Uni.createFrom().nullItem())
.invoke(user -> membreModel.setUserId(user.getId()))
.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 à la Fédération Française de Soft Armored Fighting (FFSAF), votre compte pour accéder à l'intranet a été créé.
Ce compte vous permettra de consulter vos informations, de vous inscrire aux compétitions et de consulter vos résultats.
Vous allez recevoir dans les prochaines minutes un email vous demandant de vérifier votre email et de définir un mot de passe.
L'intranet est accessible à l'adresse suivante : https://intra.ffsaf.fr
Votre nom d'utilisateur est : %s
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
""", 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) {
}
}