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

View File

@ -37,20 +37,32 @@ public class RegistrationController extends CommonController {
bean.setFkCamp(fkCamp);
bean.setRegisterInKeycloak(true); // we want people to register generally
model.addAttribute("bean", bean);
return "/registration";
return "/registration/registration";
}
@PostMapping("/registration/register")
public String register(@Valid @ModelAttribute RegistrationBean bean, final BindingResult bindingResult, Model model) {
if (bindingResult.hasErrors()) {
public String register(@Valid @ModelAttribute("bean") RegistrationBean bean, final BindingResult bindingResult, Model model) {
super.setupSession(model, request);
if (bindingResult.hasErrors()) {
CampBean campBean = service.getCamp(bean.getFkCamp());
model.addAttribute("camp", campBean);
model.addAttribute("bean", bean);
return "/registration";
return "/registration/registration";
}
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 index(bean.getFkCamp(), model);
return result ? "/registration/success" : "/error";
}
@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_PERSON;
import static de.jottyfan.camporganizer.db.jooq.Tables.T_PERSONDOCUMENT;
import static de.jottyfan.camporganizer.db.jooq.Tables.T_PROFILE;
import java.time.LocalDate;
@ -13,9 +14,11 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jasypt.util.password.StrongPasswordEncryptor;
import org.jooq.DSLContext;
import org.jooq.DeleteConditionStep;
import org.jooq.InsertResultStep;
import org.jooq.InsertValuesStep12;
import org.jooq.InsertValuesStep13;
import org.jooq.Record7;
import org.jooq.SelectConditionStep;
import org.jooq.exception.DataAccessException;
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.tables.records.TCampRecord;
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.module.common.BookingBean;
import de.jottyfan.camporganizer.module.common.LambdaResultWrapper;
/**
@ -163,4 +168,61 @@ public class RegistrationGateway {
});
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;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import de.jottyfan.camporganizer.module.common.BookingBean;
/**
*
* @author jotty
@ -10,6 +14,7 @@ import org.springframework.stereotype.Service;
*/
@Service
public class RegistrationService {
private final static Logger LOGGER = LogManager.getLogger(RegistrationService.class);
@Autowired
private RegistrationGateway gateway;
@ -36,8 +41,34 @@ public class RegistrationService {
public Boolean register(RegistrationBean bean) {
Boolean result = gateway.register(bean);
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;
}
/**
* 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

@ -1,22 +1,22 @@
# database credentials from defined config file
spring.config.import=/etc/CampOrganizer2/prod.properties
spring.config.import = /etc/CampOrganizer2/prod.properties
# define overwriteable arguments
spring.datasource.driver-class-name=${spring.datasource.driver-class-name:org.postgresql.Driver}
spring.datasource.url=${spring.datasource.url}
spring.datasource.username=${spring.datasource.username}
spring.datasource.password=${spring.datasource.password}
spring.datasource.driver-class-name = ${spring.datasource.driver-class-name:org.postgresql.Driver}
spring.datasource.url = ${spring.datasource.url}
spring.datasource.username = ${spring.datasource.username}
spring.datasource.password = ${spring.datasource.password}
server.servlet.context-path=${server.servlet.context-path:/CampOrganizer2}
server.servlet.context-path = ${server.servlet.context-path:/CampOrganizer2}
keycloak.auth-server-url=${keycloak.auth-server-url}
keycloak.realm=${keycloak.realm:ow}
keycloak.resource=${keycloak.resource:biblecamp}
keycloak.public-client=${keycloak.public-client}
keycloak.use-resource-role-mappings=${keycloak.use-resource-role-mappings}
keycloak.auth-server-url = ${keycloak.auth-server-url}
keycloak.realm = ${keycloak.realm:ow}
keycloak.resource = ${keycloak.resource:biblecamp}
keycloak.public-client = ${keycloak.public-client}
keycloak.use-resource-role-mappings = ${keycloak.use-resource-role-mappings}
ow.keycloak.admin.name=${ow.keycloak.admin.name}
ow.keycloak.admin.password=${ow.keycloak.admin.password}
ow.keycloak.admin.name = ${ow.keycloak.admin.name}
ow.keycloak.admin.password = ${ow.keycloak.admin.password}
# for development only
server.port = 8081

View File

@ -28,7 +28,7 @@
</script>
<div class="accordion" id="acc">
<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"
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
@ -37,7 +37,7 @@
th:text="${b.locationName}"></span>
</button>
</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="card">
<div class="card-header">Freizeitdaten</div>
@ -112,7 +112,7 @@
</div>
<div class="row mb-2">
<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 class="row mb-2">
<div class="col-sm-2">Kommentar:</div>
@ -120,9 +120,12 @@
</div>
<div class="row mb-2">
<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" />
</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>
</form>

View File

@ -13,7 +13,7 @@
</ul>
<ul class="navbar-nav mb-2 mb-lg-0">
<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>
</ul>
</th:block>
@ -56,7 +56,7 @@
<div class="row">
<div class="col-sm-3"></div>
<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>

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>