basic registration functionality, still missing keycloak

This commit is contained in:
Jörg Henke 2022-11-14 10:55:20 +01:00
parent 798b62fabb
commit 4133ad9173
8 changed files with 457 additions and 17 deletions

View File

@ -54,6 +54,9 @@ dependencies {
implementation 'net.sf.biweekly:biweekly:0.6.6' implementation 'net.sf.biweekly:biweekly:0.6.6'
implementation 'org.keycloak:keycloak-spring-boot-starter' implementation 'org.keycloak:keycloak-spring-boot-starter'
// backward compatibility until the complete registration is converted to keycloak
implementation 'org.jasypt:jasypt:1.9.3'
implementation 'org.springframework.boot:spring-boot-starter-jooq' implementation 'org.springframework.boot:spring-boot-starter-jooq'
implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-security'

View File

@ -0,0 +1,22 @@
package de.jottyfan.camporganizer.module.common;
/**
*
* @author henkej
*
*/
public class LambdaResultWrapper {
private Integer counter;
public LambdaResultWrapper() {
counter = 0;
}
public Integer getCounter() {
return counter;
}
public void add(Integer i) {
counter += i;
}
}

View File

@ -0,0 +1,27 @@
package de.jottyfan.camporganizer.module.registration;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Repository;
/**
*
* @author henkej
*
*/
@Repository
public class KeycloakRepository {
private final static Logger LOGGER = LogManager.getLogger(KeycloakRepository.class);
/**
* register the login in keycloak
*
* @param login
* @param password
*/
public void register(String login, String password) {
// TODO Auto-generated method stub
LOGGER.error("not yet implemented registration of user {} in keycloak", login);
}
}

View File

@ -0,0 +1,245 @@
package de.jottyfan.camporganizer.module.registration;
import java.io.Serializable;
import java.time.LocalDate;
import org.springframework.format.annotation.DateTimeFormat;
import de.jottyfan.camporganizer.db.jooq.enums.EnumCamprole;
import de.jottyfan.camporganizer.db.jooq.enums.EnumSex;
/**
*
* @author jotty
*
*/
public class RegistrationBean implements Serializable {
private static final long serialVersionUID = 1L;
private String forename;
private String surname;
private EnumSex sex;
@DateTimeFormat(pattern="yyyy-MM-dd")
private LocalDate birthDate;
private String street;
private String zip;
private String city;
private String email;
private String phone;
private String comment;
private Integer fkCamp;
private EnumCamprole campRole;
private Boolean registerInKeycloak;
private String login;
private String password;
/**
* @return the forename
*/
public String getForename() {
return forename;
}
/**
* @param forename the forename to set
*/
public void setForename(String forename) {
this.forename = forename;
}
/**
* @return the surname
*/
public String getSurname() {
return surname;
}
/**
* @param surname the surname to set
*/
public void setSurname(String surname) {
this.surname = surname;
}
/**
* @return the sex
*/
public EnumSex getSex() {
return sex;
}
/**
* @param sex the sex to set
*/
public void setSex(EnumSex sex) {
this.sex = sex;
}
/**
* @return the birthDate
*/
public LocalDate getBirthDate() {
return birthDate;
}
/**
* @param birthDate the birthDate to set
*/
public void setBirthDate(LocalDate birthDate) {
this.birthDate = birthDate;
}
/**
* @return the street
*/
public String getStreet() {
return street;
}
/**
* @param street the street to set
*/
public void setStreet(String street) {
this.street = street;
}
/**
* @return the zip
*/
public String getZip() {
return zip;
}
/**
* @param zip the zip to set
*/
public void setZip(String zip) {
this.zip = zip;
}
/**
* @return the city
*/
public String getCity() {
return city;
}
/**
* @param city the city to set
*/
public void setCity(String city) {
this.city = city;
}
/**
* @return the email
*/
public String getEmail() {
return email;
}
/**
* @param email the email to set
*/
public void setEmail(String email) {
this.email = email;
}
/**
* @return the phone
*/
public String getPhone() {
return phone;
}
/**
* @param phone the phone to set
*/
public void setPhone(String phone) {
this.phone = phone;
}
/**
* @return the comment
*/
public String getComment() {
return comment;
}
/**
* @param comment the comment to set
*/
public void setComment(String comment) {
this.comment = comment;
}
/**
* @return the registerInKeycloak
*/
public Boolean getRegisterInKeycloak() {
return registerInKeycloak;
}
/**
* @param registerInKeycloak the registerInKeycloak to set
*/
public void setRegisterInKeycloak(Boolean registerInKeycloak) {
this.registerInKeycloak = registerInKeycloak;
}
/**
* @return the login
*/
public String getLogin() {
return login;
}
/**
* @param login the login to set
*/
public void setLogin(String login) {
this.login = login;
}
/**
* @return the password
*/
public String getPassword() {
return password;
}
/**
* @param password the password to set
*/
public void setPassword(String password) {
this.password = password;
}
/**
* @return the fkCamp
*/
public Integer getFkCamp() {
return fkCamp;
}
/**
* @param fkCamp the fkCamp to set
*/
public void setFkCamp(Integer fkCamp) {
this.fkCamp = fkCamp;
}
/**
* @return the campRole
*/
public EnumCamprole getCampRole() {
return campRole;
}
/**
* @param campRole the campRole to set
*/
public void setCampRole(EnumCamprole campRole) {
this.campRole = campRole;
}
}

View File

@ -6,7 +6,9 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import de.jottyfan.camporganizer.module.common.CommonController; import de.jottyfan.camporganizer.module.common.CommonController;
@ -27,9 +29,19 @@ public class RegistrationController extends CommonController {
@GetMapping("/registration/{fkCamp}") @GetMapping("/registration/{fkCamp}")
public String index(@PathVariable(name = "fkCamp", required = true) Integer fkCamp, Model model) { public String index(@PathVariable(name = "fkCamp", required = true) Integer fkCamp, Model model) {
super.setupSession(model, request); super.setupSession(model, request);
model.addAttribute("camp", service.getCamp(fkCamp)); CampBean campBean = service.getCamp(fkCamp);
// TODO: prepare a bean for the form variables; use model.getAttribute("camp").getPk() for the fk_camp model.addAttribute("camp", campBean);
RegistrationBean bean = new RegistrationBean();
bean.setFkCamp(fkCamp);
bean.setRegisterInKeycloak(true); // we want people to register generally
model.addAttribute("bean", bean);
return "/registration"; return "/registration";
} }
@PostMapping("/registration/register")
public String register(@ModelAttribute RegistrationBean bean, Model model) {
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);
}
} }

View File

@ -1,18 +1,34 @@
package de.jottyfan.camporganizer.module.registration; 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_PROFILE;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.UUID;
import org.apache.logging.log4j.LogManager; 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.jooq.DSLContext; import org.jooq.DSLContext;
import org.jooq.InsertResultStep;
import org.jooq.InsertValuesStep12;
import org.jooq.InsertValuesStep13;
import org.jooq.SelectConditionStep; import org.jooq.SelectConditionStep;
import org.jooq.exception.DataAccessException;
import org.jooq.impl.DSL;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
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.TCampRecord;
import de.jottyfan.camporganizer.db.jooq.tables.records.TPersonRecord;
import de.jottyfan.camporganizer.db.jooq.tables.records.TProfileRecord;
import de.jottyfan.camporganizer.module.common.LambdaResultWrapper;
/** /**
* *
@ -49,4 +65,89 @@ public class RegistrationGateway {
} }
} }
/**
* save the content in t_person; also, create a profile for the user if
* registerInKeycloak is true
*
* @param bean the bean
* @return true or false
*/
public Boolean register(RegistrationBean bean) {
LambdaResultWrapper lrw = new LambdaResultWrapper();
jooq.transaction(t -> {
if (bean.getRegisterInKeycloak()) {
SelectConditionStep<TProfileRecord> sql0 = DSL.using(t).selectFrom(T_PROFILE)
.where(T_PROFILE.USERNAME.eq(bean.getLogin()));
LOGGER.debug(sql0);
if (sql0.fetch().size() > 0) {
throw new DataAccessException("login already in use: " + bean.getLogin());
}
// TODO: check if teacher is at least 2 years older than the camp participants
String oldPassword = new StrongPasswordEncryptor().encryptPassword(bean.getPassword());
InsertResultStep<TProfileRecord> sql1 = DSL.using(t)
// @formatter:off
.insertInto(T_PROFILE,
T_PROFILE.FORENAME,
T_PROFILE.SURNAME,
T_PROFILE.USERNAME,
T_PROFILE.PASSWORD,
T_PROFILE.DUEDATE,
T_PROFILE.UUID)
.values(bean.getForename(), bean.getSurname(), bean.getLogin(), oldPassword, LocalDateTime.now().plus(356, ChronoUnit.DAYS), UUID.nameUUIDFromBytes(bean.getLogin().getBytes()).toString())
.returning(T_PROFILE.PK);
// @formatter:on
LOGGER.debug(sql1.toString());
Integer fkProfile = sql1.fetchOne().getPk();
// register the person for camp participation
InsertValuesStep13<TPersonRecord, String, String, EnumSex, LocalDate, String, String, String, String, String, EnumCamprole, Integer, String, Integer> sql2 = DSL
.using(t)
// @formatter:off
.insertInto(T_PERSON,
T_PERSON.FORENAME,
T_PERSON.SURNAME,
T_PERSON.SEX,
T_PERSON.BIRTHDATE,
T_PERSON.STREET,
T_PERSON.ZIP,
T_PERSON.CITY,
T_PERSON.EMAIL,
T_PERSON.PHONE,
T_PERSON.CAMPROLE,
T_PERSON.FK_CAMP,
T_PERSON.COMMENT,
T_PERSON.FK_PROFILE)
.values(bean.getForename(), bean.getSurname(), bean.getSex(),
bean.getBirthDate(), bean.getStreet(), bean.getZip(), bean.getCity(), bean.getEmail(),
bean.getPhone(), bean.getCampRole(), bean.getFkCamp(), bean.getComment(), fkProfile);
// @formatter:on
LOGGER.debug(sql2.toString());
lrw.add(sql2.execute());
// register the login for the portal
} else {
InsertValuesStep12<TPersonRecord, String, String, EnumSex, LocalDate, String, String, String, String, String, EnumCamprole, Integer, String> sql = DSL
.using(t)
// @formatter:off
.insertInto(T_PERSON,
T_PERSON.FORENAME,
T_PERSON.SURNAME,
T_PERSON.SEX,
T_PERSON.BIRTHDATE,
T_PERSON.STREET,
T_PERSON.ZIP,
T_PERSON.CITY,
T_PERSON.EMAIL,
T_PERSON.PHONE,
T_PERSON.CAMPROLE,
T_PERSON.FK_CAMP,
T_PERSON.COMMENT)
.values(bean.getForename(), bean.getSurname(), bean.getSex(),
bean.getBirthDate(), bean.getStreet(), bean.getZip(), bean.getCity(), bean.getEmail(),
bean.getPhone(), bean.getCampRole(), bean.getFkCamp(), bean.getComment());
// @formatter:on
LOGGER.debug(sql.toString());
lrw.add(sql.execute());
}
});
return lrw.getCounter() > 0;
}
} }

View File

@ -13,6 +13,9 @@ public class RegistrationService {
@Autowired @Autowired
private RegistrationGateway gateway; private RegistrationGateway gateway;
@Autowired
private KeycloakRepository keycloak;
/** /**
* get the camp * get the camp
@ -24,4 +27,17 @@ public class RegistrationService {
return gateway.getCamp(fkCamp); return gateway.getCamp(fkCamp);
} }
/**
* register the person for a camp; if registerInKeycloak, do so also
*
* @param bean the bean
* @return true if successful, false otherwise
*/
public Boolean register(RegistrationBean bean) {
Boolean result = gateway.register(bean);
if (result && bean.getRegisterInKeycloak()) {
keycloak.register(bean.getLogin(), bean.getPassword());
}
return result;
}
} }

View File

@ -15,66 +15,80 @@
<h3 class="centered cabin" th:text="'zur ' + ${camp.name} + ' ' + ${camp.year}"></h3> <h3 class="centered cabin" th:text="'zur ' + ${camp.name} + ' ' + ${camp.year}"></h3>
<div class="card centered-card" style="max-width: 48rem"> <div class="card centered-card" style="max-width: 48rem">
<div class="card-body"> <div class="card-body">
<form action="#" method="post"> <form th:action="@{/registration/register}" th:object="${bean}" method="post">
<input type="hidden" th:field="*{fkCamp}" />
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-sm-6 rowdist"> <div class="col-sm-6 rowdist">
<input type="text" class="form-control" placeholder="Vorname" /> <input type="text" class="form-control" placeholder="Vorname" th:field="*{forename}" />
</div> </div>
<div class="col-sm-6 rowdist"> <div class="col-sm-6 rowdist">
<input type="text" class="form-control" placeholder="Nachname" /> <input type="text" class="form-control" placeholder="Nachname" th:field="*{surname}" />
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-sm-6 rowdist"> <div class="col-sm-6 rowdist">
<select class="form-select"> <select class="form-select" th:field="*{sex}">
<option value="">Geschlecht</option> <option value="">Geschlecht</option>
<option value="female">weiblich</option> <option value="female">weiblich</option>
<option value="male">männlich</option> <option value="male">männlich</option>
</select> </select>
</div> </div>
<div class="col-sm-6 rowdist"> <div class="col-sm-6 rowdist">
<input type="date" class="form-control" placeholder="Geburtsdatum" /> <input type="date" class="form-control" placeholder="Geburtsdatum" th:field="*{birthDate}" />
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-sm-6 rowdist"> <div class="col-sm-6 rowdist">
<input type="text" class="form-control" placeholder="Straße und Hausnummer" /> <input type="text" class="form-control" placeholder="Straße und Hausnummer" th:field="*{street}" />
</div> </div>
<div class="col-sm-2 rowdist"> <div class="col-sm-2 rowdist">
<input type="text" class="form-control" placeholder="PLZ" /> <input type="text" class="form-control" placeholder="PLZ" th:field="*{zip}" />
</div> </div>
<div class="col-sm-4 rowdist"> <div class="col-sm-4 rowdist">
<input type="text" class="form-control" placeholder="Ort" /> <input type="text" class="form-control" placeholder="Ort" th:field="*{city}" />
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-sm-6 rowdist"> <div class="col-sm-6 rowdist">
<input type="text" class="form-control" placeholder="E-Mail" /> <input type="text" class="form-control" placeholder="E-Mail" th:field="*{email}" />
</div> </div>
<div class="col-sm-6 rowdist"> <div class="col-sm-6 rowdist">
<input type="text" class="form-control" placeholder="Telefonnummer" /> <input type="text" class="form-control" placeholder="Telefonnummer" th:field="*{phone}" />
</div>
</div>
<div class="row">
<div class="col-sm-6 rowdist">
<span>mitkommen als</span>
</div>
<div class="col-sm-6 rowdist">
<select class="form-select" th:field="*{campRole}">
<option value="student">Teilnehmer</option>
<option value="teacher">Mitarbeiter</option>
<option value="feeder">Küchenteam</option>
<option value="director">Leiter</option>
</select>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-sm-12 rowdist"> <div class="col-sm-12 rowdist">
<textarea class="form-control" placeholder="Sonstiges"></textarea> <textarea class="form-control" placeholder="Sonstiges" th:field="*{comment}"></textarea>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-sm-12 rowdist"> <div class="col-sm-12 rowdist">
<div class="form-check" title="Die Anmeldedaten können in den Kontoeinstellungen bearbeitet und für die nächsten Freizeitanmeldungen verwendet werden."> <div class="form-check" title="Die Anmeldedaten können in den Kontoeinstellungen bearbeitet und für die nächsten Freizeitanmeldungen verwendet werden.">
<input id="save" type="checkbox" class="form-check-input" checked="checked" onchange="$('#createlogin').toggle();" /> <input id="save" type="checkbox" class="form-check-input" th:field="*{registerInKeycloak}" onchange="$('#createlogin').toggle();" />
<label class="form-check-label" for="save">Anmeldedaten speichern</label> <label class="form-check-label" for="save">Anmeldedaten speichern</label>
</div> </div>
</div> </div>
</div> </div>
<div id="createlogin" class="row"> <div id="createlogin" class="row">
<div class="col-sm-6 rowdist"> <div class="col-sm-6 rowdist">
<input type="text" class="form-control" placeholder="Login" /> <input type="text" class="form-control" placeholder="Login" th:field="*{login}" />
</div> </div>
<div class="col-sm-6 rowdist"> <div class="col-sm-6 rowdist">
<input type="password" class="form-control" placeholder="Passwort" /> <input type="password" class="form-control" placeholder="Passwort" th:field="*{password}" />
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@ -89,4 +103,4 @@
</div> </div>
</content> </content>
</body> </body>
</html> </html>