keycloak registration done

This commit is contained in:
Jottyfan 2022-12-09 22:23:54 +01:00
parent b6658655be
commit 29e8ff7798
10 changed files with 248 additions and 53 deletions

View File

@ -25,19 +25,19 @@ import org.springframework.stereotype.Repository;
public class KeycloakRepository { public class KeycloakRepository {
private final static Logger LOGGER = LogManager.getLogger(KeycloakRepository.class); private final static Logger LOGGER = LogManager.getLogger(KeycloakRepository.class);
@Value("${keycloak.resource:biblecamp}") @Value("${keycloak.resource}")
private String keycloakClientId; private String keycloakClientId;
@Value("${keycloak.auth-server-url}") @Value("${keycloak.auth-server-url}")
private String keycloakUrl; private String keycloakUrl;
@Value("${keycloak.realm:ow}") @Value("${keycloak.realm}")
private String keycloakRealm; private String keycloakRealm;
@Value("${ow.keycloak.admin.name:admin") @Value("${ow.keycloak.admin.name}")
private String keycloakAdminName; private String keycloakAdminName;
@Value("${ow.keycloak.admin.password:password") @Value("${ow.keycloak.admin.password}")
private String keycloakAdminPassword; private String keycloakAdminPassword;
/** /**
@ -54,28 +54,32 @@ public class KeycloakRepository {
/** /**
* register the login in keycloak * register the login in keycloak
* *
* @param forename the forename
* @param surname the surname
* @param login the username * @param login the username
* @param password the password * @param password the password
* @param email the email * @param email the email
* @return true or false * @return true or false
*/ */
public boolean register(String login, String password, String email) { public boolean register(String forename, String surname, String login, String password, String email) {
UserRepresentation user = getUserRepresentation(login, password, email); UserRepresentation user = getUserRepresentation(forename, surname, login, password, email);
UsersResource resource = getUsersResource(keycloakUrl, keycloakRealm, keycloakAdminName, keycloakAdminPassword, keycloakClientId); UsersResource resource = getUsersResource(keycloakUrl, keycloakRealm, keycloakAdminName, keycloakAdminPassword,
boolean result = register(resource, user); keycloakClientId);
sendVerificationLink(login, resource); return register(resource, user);
return result;
} }
/** /**
* generate a user representation * generate a user representation
* *
* @param forename the forename
* @param surname the surname
* @param login the login * @param login the login
* @param password the password * @param password the password
* @param email the email * @param email the email
* @return the user representation * @return the user representation
*/ */
protected UserRepresentation getUserRepresentation(String login, String password, String email) { protected UserRepresentation getUserRepresentation(String forename, String surname, String login, String password,
String email) {
CredentialRepresentation passwordCredentials = new CredentialRepresentation(); CredentialRepresentation passwordCredentials = new CredentialRepresentation();
passwordCredentials.setTemporary(false); passwordCredentials.setTemporary(false);
passwordCredentials.setType(CredentialRepresentation.PASSWORD); passwordCredentials.setType(CredentialRepresentation.PASSWORD);
@ -83,6 +87,8 @@ public class KeycloakRepository {
UserRepresentation user = new UserRepresentation(); UserRepresentation user = new UserRepresentation();
user.setUsername(login); user.setUsername(login);
user.setFirstName(forename);
user.setLastName(surname);
user.setEmail(email); user.setEmail(email);
user.setCredentials(Collections.singletonList(passwordCredentials)); user.setCredentials(Collections.singletonList(passwordCredentials));
user.setEnabled(true); user.setEnabled(true);
@ -99,8 +105,13 @@ public class KeycloakRepository {
*/ */
protected boolean register(UsersResource resource, UserRepresentation user) { protected boolean register(UsersResource resource, UserRepresentation user) {
Response response = resource.create(user); Response response = resource.create(user);
Boolean success = Status.CREATED.equals(response.getStatusInfo());
if (success) {
LOGGER.info("created new keycloak user {}", user.getUsername()); LOGGER.info("created new keycloak user {}", user.getUsername());
return Status.OK.equals(response.getStatusInfo()); } else {
LOGGER.error("error on creating keycloak user {}: {}", user.getUsername(), response.getStatus());
}
return success;
} }
/** /**
@ -114,8 +125,8 @@ public class KeycloakRepository {
* @return the keycloak object * @return the keycloak object
*/ */
public KeycloakBuilder getKeycloak(String url, String realm, String admin, String password, String clientId) { public KeycloakBuilder getKeycloak(String url, String realm, String admin, String password, String clientId) {
return KeycloakBuilder.builder().serverUrl(url).realm(realm).grantType(OAuth2Constants.PASSWORD) return KeycloakBuilder.builder().serverUrl(url).realm(realm).grantType(OAuth2Constants.PASSWORD).username(admin)
.username(admin).password(password).clientId(clientId) .password(password).clientId(clientId)
.resteasyClient(new ResteasyClientBuilderImpl().connectionPoolSize(10).build()); .resteasyClient(new ResteasyClientBuilderImpl().connectionPoolSize(10).build());
} }
@ -138,8 +149,9 @@ public class KeycloakRepository {
* *
* @param userId the ID of the user * @param userId the ID of the user
*/ */
public void sendVerificationLink(String userId, UsersResource usersResource) { public void sendVerificationLink(String userId) {
usersResource.get(userId).sendVerifyEmail(); getUsersResource(keycloakUrl, keycloakRealm, keycloakAdminName, keycloakAdminPassword, keycloakClientId).get(userId)
.sendVerifyEmail();
} }
} }

View File

@ -37,20 +37,32 @@ public class RegistrationController extends CommonController {
bean.setFkCamp(fkCamp); bean.setFkCamp(fkCamp);
bean.setRegisterInKeycloak(true); // we want people to register generally bean.setRegisterInKeycloak(true); // we want people to register generally
model.addAttribute("bean", bean); model.addAttribute("bean", bean);
return "/registration"; return "/registration/registration";
} }
@PostMapping("/registration/register") @PostMapping("/registration/register")
public String register(@Valid @ModelAttribute RegistrationBean bean, final BindingResult bindingResult, Model model) { public String register(@Valid @ModelAttribute("bean") RegistrationBean bean, final BindingResult bindingResult, Model model) {
if (bindingResult.hasErrors()) {
super.setupSession(model, request); super.setupSession(model, request);
if (bindingResult.hasErrors()) {
CampBean campBean = service.getCamp(bean.getFkCamp()); CampBean campBean = service.getCamp(bean.getFkCamp());
model.addAttribute("camp", campBean); model.addAttribute("camp", campBean);
model.addAttribute("bean", bean); return "/registration/registration";
return "/registration";
} }
Boolean result = service.register(bean); Boolean result = service.register(bean);
// TODO: give the user a message about success or error and, if registered in keycloak, a note about how to login return result ? "/registration/success" : "/error";
return index(bean.getFkCamp(), model); }
@GetMapping("/registration/cancel/{id}")
public String cancellation(@PathVariable Integer id, final Model model) {
super.setupSession(model, request);
model.addAttribute("bean", service.getBooking(id));
return "/registration/cancellation";
}
@GetMapping("/registration/remove/{id}")
public String remove(@PathVariable Integer id, final Model model) {
super.setupSession(model, request);
service.removeBooking(id);
return "redirect:/dashboard";
} }
} }

View File

@ -2,6 +2,7 @@ package de.jottyfan.camporganizer.module.registration;
import static de.jottyfan.camporganizer.db.jooq.Tables.T_CAMP; import static de.jottyfan.camporganizer.db.jooq.Tables.T_CAMP;
import static de.jottyfan.camporganizer.db.jooq.Tables.T_PERSON; import static de.jottyfan.camporganizer.db.jooq.Tables.T_PERSON;
import static de.jottyfan.camporganizer.db.jooq.Tables.T_PERSONDOCUMENT;
import static de.jottyfan.camporganizer.db.jooq.Tables.T_PROFILE; import static de.jottyfan.camporganizer.db.jooq.Tables.T_PROFILE;
import java.time.LocalDate; import java.time.LocalDate;
@ -13,9 +14,11 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.jasypt.util.password.StrongPasswordEncryptor; import org.jasypt.util.password.StrongPasswordEncryptor;
import org.jooq.DSLContext; import org.jooq.DSLContext;
import org.jooq.DeleteConditionStep;
import org.jooq.InsertResultStep; import org.jooq.InsertResultStep;
import org.jooq.InsertValuesStep12; import org.jooq.InsertValuesStep12;
import org.jooq.InsertValuesStep13; import org.jooq.InsertValuesStep13;
import org.jooq.Record7;
import org.jooq.SelectConditionStep; import org.jooq.SelectConditionStep;
import org.jooq.exception.DataAccessException; import org.jooq.exception.DataAccessException;
import org.jooq.impl.DSL; import org.jooq.impl.DSL;
@ -27,7 +30,9 @@ import de.jottyfan.camporganizer.db.jooq.enums.EnumCamprole;
import de.jottyfan.camporganizer.db.jooq.enums.EnumSex; import de.jottyfan.camporganizer.db.jooq.enums.EnumSex;
import de.jottyfan.camporganizer.db.jooq.tables.records.TCampRecord; import de.jottyfan.camporganizer.db.jooq.tables.records.TCampRecord;
import de.jottyfan.camporganizer.db.jooq.tables.records.TPersonRecord; import de.jottyfan.camporganizer.db.jooq.tables.records.TPersonRecord;
import de.jottyfan.camporganizer.db.jooq.tables.records.TPersondocumentRecord;
import de.jottyfan.camporganizer.db.jooq.tables.records.TProfileRecord; import de.jottyfan.camporganizer.db.jooq.tables.records.TProfileRecord;
import de.jottyfan.camporganizer.module.common.BookingBean;
import de.jottyfan.camporganizer.module.common.LambdaResultWrapper; import de.jottyfan.camporganizer.module.common.LambdaResultWrapper;
/** /**
@ -163,4 +168,61 @@ public class RegistrationGateway {
}); });
return lrw.getCounter() > 0; return lrw.getCounter() > 0;
} }
/**
* get the booking from the database
*
* @param id the id of the booking
* @return the booking bean or null
*/
public BookingBean getBooking(Integer id) {
SelectConditionStep<Record7<Integer, String, String, EnumCamprole, String, LocalDateTime, LocalDateTime>> sql = jooq
// @formatter:off
.select(T_PERSON.PK,
T_PERSON.FORENAME,
T_PERSON.SURNAME,
T_PERSON.CAMPROLE,
T_CAMP.NAME,
T_CAMP.ARRIVE,
T_CAMP.DEPART)
.from(T_PERSON)
.leftJoin(T_CAMP).on(T_CAMP.PK.eq(T_PERSON.FK_CAMP))
.where(T_PERSON.PK.eq(id));
// @formatter:on
LOGGER.debug(sql.toString());
Record7<Integer, String, String, EnumCamprole, String, LocalDateTime, LocalDateTime> r = sql.fetchOne();
if (r != null) {
BookingBean bean = new BookingBean();
bean.setPk(r.get(T_PERSON.PK));
bean.setForename(r.get(T_PERSON.FORENAME));
bean.setSurname(r.get(T_PERSON.SURNAME));
bean.setCampName(r.get(T_CAMP.NAME));
bean.setArrive(r.get(T_CAMP.ARRIVE));
bean.setDepart(r.get(T_CAMP.DEPART));
bean.setCamprole(r.get(T_PERSON.CAMPROLE));
return bean;
}
return null;
}
/**
* remove the booking and all of its dependencies
*
* @param id the pk of t_person
* @return number of affected database rows, should be 1
*/
public Integer removeBooking(Integer id) {
LambdaResultWrapper lrw = new LambdaResultWrapper();
jooq.transaction(t -> {
DeleteConditionStep<TPersondocumentRecord> sql1 = DSL.using(t).deleteFrom(T_PERSONDOCUMENT)
.where(T_PERSONDOCUMENT.FK_PERSON.eq(id));
LOGGER.debug(sql1.toString());
sql1.execute();
DeleteConditionStep<TPersonRecord> sql2 = DSL.using(t).deleteFrom(T_PERSON).where(T_PERSON.PK.eq(id));
LOGGER.debug(sql2.toString());
lrw.add(sql2.execute());
});
return lrw.getCounter();
}
} }

View File

@ -1,8 +1,12 @@
package de.jottyfan.camporganizer.module.registration; package de.jottyfan.camporganizer.module.registration;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import de.jottyfan.camporganizer.module.common.BookingBean;
/** /**
* *
* @author jotty * @author jotty
@ -10,6 +14,7 @@ import org.springframework.stereotype.Service;
*/ */
@Service @Service
public class RegistrationService { public class RegistrationService {
private final static Logger LOGGER = LogManager.getLogger(RegistrationService.class);
@Autowired @Autowired
private RegistrationGateway gateway; private RegistrationGateway gateway;
@ -36,8 +41,34 @@ public class RegistrationService {
public Boolean register(RegistrationBean bean) { public Boolean register(RegistrationBean bean) {
Boolean result = gateway.register(bean); Boolean result = gateway.register(bean);
if (result && bean.getRegisterInKeycloak()) { if (result && bean.getRegisterInKeycloak()) {
keycloak.register(bean.getLogin(), bean.getPassword(), bean.getEmail()); keycloak.register(bean.getForename(), bean.getSurname(), bean.getLogin(), bean.getPassword(), bean.getEmail());
if (bean.getEmail() != null && !bean.getEmail().isBlank()) {
try {
keycloak.sendVerificationLink(bean.getLogin());
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
}
}
} }
return result; return result;
} }
/**
* get the booking bean
*
* @param id the id of the registration (t_person.pk)
* @return the booking bean or null
*/
public BookingBean getBooking(Integer id) {
return gateway.getBooking(id);
}
/**
* remove the booking and all of its dependencies
*
* @param id the id of the booking (t_person.pk)
*/
public Boolean removeBooking(Integer id) {
return gateway.removeBooking(id) > 0;
}
} }

View File

@ -28,7 +28,7 @@
</script> </script>
<div class="accordion" id="acc"> <div class="accordion" id="acc">
<div class="accordion-item" th:each="b : ${mybookings}"> <div class="accordion-item" th:each="b : ${mybookings}">
<h2 class="accordion-header" th:id="'acc-head-' + ${b.pk}"> <h2 class="accordion-header" th:id="'acc-head-' + ${b.pk}" th:if="${b.pk}">
<button th:class="'accordion-button collapsed acc_' + ${b.accept}" type="button" data-bs-toggle="collapse" th:data-bs-target="'#acc-body-' + ${b.pk}" aria-expanded="true" <button th:class="'accordion-button collapsed acc_' + ${b.accept}" type="button" data-bs-toggle="collapse" th:data-bs-target="'#acc-body-' + ${b.pk}" aria-expanded="true"
th:aria-controls="'#acc-body-' + ${b.pk}"> th:aria-controls="'#acc-body-' + ${b.pk}">
<i class="fas fa-check framed framed-green" th:if="${b.accept}"></i> <i class="fas fa-ban framed framed-red" th:if="${b.accept} == false"></i> <i <i class="fas fa-check framed framed-green" th:if="${b.accept}"></i> <i class="fas fa-ban framed framed-red" th:if="${b.accept} == false"></i> <i
@ -37,7 +37,7 @@
th:text="${b.locationName}"></span> th:text="${b.locationName}"></span>
</button> </button>
</h2> </h2>
<div th:id="'acc-body-' + ${b.pk}" class="accordion-collapse collapse" th:aria-labelledby="'acc-head-' + ${b.pk}"> <div th:id="'acc-body-' + ${b.pk}" class="accordion-collapse collapse" th:aria-labelledby="'acc-head-' + ${b.pk}" th:if="${b.pk}">
<div class="accordion-body"> <div class="accordion-body">
<div class="card"> <div class="card">
<div class="card-header">Freizeitdaten</div> <div class="card-header">Freizeitdaten</div>
@ -112,7 +112,7 @@
</div> </div>
<div class="row mb-2"> <div class="row mb-2">
<div class="col-sm-2">Foto-Einverständnis:</div> <div class="col-sm-2">Foto-Einverständnis:</div>
<span class="col-sm-10" th:text="${b.consentCatalogPhoto ? 'ja' : 'nein'}"></span> <span class="col-sm-10"><span th:text="${b.consentCatalogPhoto ? 'ja' : 'nein'}" th:if="${b.consentCatalogPhoto}"></span></span>
</div> </div>
<div class="row mb-2"> <div class="row mb-2">
<div class="col-sm-2">Kommentar:</div> <div class="col-sm-2">Kommentar:</div>
@ -120,9 +120,12 @@
</div> </div>
<div class="row mb-2"> <div class="row mb-2">
<div class="col-sm-2"></div> <div class="col-sm-2"></div>
<div class="col-sm-10"> <div class="col-sm-8">
<input type="submit" class="btn btn-primary" value="Änderungen übernehmen" /> <input type="submit" class="btn btn-primary" value="Änderungen übernehmen" />
</div> </div>
<div class="col-sm-2">
<a th:href="@{/registration/cancel/{id}(id=${b.pk})}" class="btn btn-outline-danger">stornieren</a>
</div>
</div> </div>
</div> </div>
</form> </form>

View File

@ -13,7 +13,7 @@
</ul> </ul>
<ul class="navbar-nav mb-2 mb-lg-0"> <ul class="navbar-nav mb-2 mb-lg-0">
<li class="nav-item"> <li class="nav-item">
<a class="btn btn-icon-silent menufont" href="http://anmeldung.onkelwernerfreizeiten.de">Unsere Freizeiten</a> <a class="btn btn-icon-silent menufont" th:href="@{/}">Unsere Freizeiten</a>
</li> </li>
</ul> </ul>
</th:block> </th:block>
@ -56,7 +56,7 @@
<div class="row"> <div class="row">
<div class="col-sm-3"></div> <div class="col-sm-3"></div>
<div class="col-sm-9"> <div class="col-sm-9">
<a class="btn btn-primary buttonfont" href="http://anmeldung.onkelwernerfreizeiten.de">jetzt anmelden</a> <a class="btn btn-primary buttonfont" th:href="@{/registration/{id}(id=${c.pk})}">jetzt anmelden</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" layout:decorate="~{template}" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<title>Camp Organizer 2</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<th:block layout:fragment="header">
<ul class="navbar-nav mb-2 mb-lg-0">
<li class="nav-item"><a th:href="@{${keycloakProfileUrl}}" class="btn btn-secondary btn-icon-silent" target="_blank">Profil</a></li>
</ul>
<ul class="navbar-nav mb-2 mb-lg-0">
<li class="nav-item"><a th:href="@{/business}" class="btn btn-secondary btn-icon-silent" sec:authorize="hasRole('business')">Abrechnung</a></li>
</ul>
<ul class="navbar-nav mb-2 mb-lg-0">
<li class="nav-item"><a th:href="@{/confirmation}" class="btn btn-secondary btn-icon-silent" sec:authorize="hasRole('registrator')">Bestätigung</a></li>
</ul>
<ul class="navbar-nav mb-2 mb-lg-0">
<li class="nav-item"><a href="https://www.onkelwernerfreizeiten.de/cloud" class="btn btn-secondary btn-icon-silent" target="_blank">Nextcloud</a></li>
</ul>
</th:block>
<th:block layout:fragment="content">
<div class="mainpage">
<div class="alert alert-warning">
<span th:if="${bean.isStudent}">Willst du deine Anmeldung wirklich stornieren und damit den Freizeitplatz für einen anderen Teilnehmer freigeben? Diese Stornierung kann nicht
rückgängig gemacht werden.</span> <span th:if="${!bean.isStudent}">Willst du deine Anmeldung wirklich stornieren?</span>
</div>
<div class="card">
<div class="card-header">
<span th:text="${bean.campName + ' ' + #temporals.format(bean.arrive, 'dd.MM.') + ' - ' + #temporals.format(bean.depart, 'dd.MM.yyyy')}"
th:if="${bean.arrive != null and bean.depart != null}"></span>
</div>
<div class="card-body">
<span th:text="${bean.forename + ' ' + bean.surname}"></span>,&nbsp;<span th:if="${bean.isStudent}">Teilnehmer</span><span th:if="${bean.isTeacher}">Mitarbeiter</span> <span
th:if="${bean.isDirector}">Leiter</span><span th:if="${bean.isFeeder}">Küchenhilfe</span>
</div>
<div class="card-footer">
<a th:href="@{/registration/remove/{id}(id=${bean.pk})}" class="btn btn-danger">Ja, stornieren</a> &nbsp;<a th:href="@{/dashboard}" class="btn btn-outline-success">Stornierung abbrechen</a>
</div>
</div>
</div>
</th:block>
</body>
</html>

View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" layout:decorate="~{template}" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<title>Camp Organizer 2</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<th:block layout:fragment="header">
<ul class="navbar-nav mb-2 mb-lg-0">
<li class="nav-item"><a th:href="@{${keycloakProfileUrl}}" class="btn btn-secondary btn-icon-silent" target="_blank">Profil</a></li>
</ul>
<ul class="navbar-nav mb-2 mb-lg-0">
<li class="nav-item"><a th:href="@{/business}" class="btn btn-secondary btn-icon-silent" sec:authorize="hasRole('business')">Abrechnung</a></li>
</ul>
<ul class="navbar-nav mb-2 mb-lg-0">
<li class="nav-item"><a th:href="@{/confirmation}" class="btn btn-secondary btn-icon-silent" sec:authorize="hasRole('registrator')">Bestätigung</a></li>
</ul>
<ul class="navbar-nav mb-2 mb-lg-0">
<li class="nav-item"><a href="https://www.onkelwernerfreizeiten.de/cloud" class="btn btn-secondary btn-icon-silent" target="_blank">Nextcloud</a></li>
</ul>
</th:block>
<th:block layout:fragment="content">
<div class="mainpage">
<div class="alert alert-success">
Deine Anmeldung wurde entgegengenommen. Falls du dir auch ein Login eingerichtet hast, kannst du dich jetzt oben rechts einloggen und deine Anmeldung bearbeiten.
</div>
<div><a th:href="@{/}" class="btn btn-outline-secondary">zur Hauptseite</a></div>
</div>
</th:block>
</body>
</html>