basic validation

This commit is contained in:
Jottyfan 2022-12-08 23:03:50 +01:00
parent df7e4e7f0e
commit 2e376291c5
12 changed files with 174 additions and 35 deletions

View File

@ -26,6 +26,5 @@
<attribute name="org.eclipse.jst.component.dependency" value="/WEB-INF/lib"/> <attribute name="org.eclipse.jst.component.dependency" value="/WEB-INF/lib"/>
</attributes> </attributes>
</classpathentry> </classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/>
<classpathentry kind="output" path="bin/default"/> <classpathentry kind="output" path="bin/default"/>
</classpath> </classpath>

View File

@ -1,14 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?><project-modules id="moduleCoreId" project-version="1.5.0"> <?xml version="1.0" encoding="UTF-8"?>
<project-modules id="moduleCoreId" project-version="1.5.0">
<wb-module deploy-name="CampOrganizer2"> <wb-module deploy-name="CampOrganizer2">
<property name="context-root" value="CampOrganizer2"/>
<property name="context-root" value="CampOrganizer2"/> <wb-resource deploy-path="/WEB-INF/classes" source-path="src/main/resources"/>
<wb-resource deploy-path="/WEB-INF/classes" source-path="src/main/java"/>
<wb-resource deploy-path="/WEB-INF/classes" source-path="src/main/resources"/> </wb-module>
<wb-resource deploy-path="/WEB-INF/classes" source-path="src/main/java"/>
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src/test/java"/>
</wb-module>
</project-modules> </project-modules>

View File

@ -67,6 +67,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5' implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
implementation 'de.jottyfan:COJooq:2021.02' implementation 'de.jottyfan:COJooq:2021.02'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.0.0' implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.0.0'

View File

@ -34,10 +34,10 @@ public class KeycloakRepository {
@Value("${keycloak.realm:ow}") @Value("${keycloak.realm:ow}")
private String keycloakRealm; private String keycloakRealm;
@Value("${keycloak.admin.name:admin") @Value("${ow.keycloak.admin.name:admin")
private String keycloakAdminName; private String keycloakAdminName;
@Value("${keycloak.admin.password:password") @Value("${ow.keycloak.admin.password:password")
private String keycloakAdminPassword; private String keycloakAdminPassword;
/** /**

View File

@ -3,32 +3,50 @@ package de.jottyfan.camporganizer.module.registration;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDate; import java.time.LocalDate;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
import de.jottyfan.camporganizer.db.jooq.enums.EnumCamprole; 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.module.registration.validate.UnusedUsername;
/** /**
* *
* @author jotty * @author jotty
* *
*/ */
@UnusedUsername(field = "login", message = "Dieses Login ist leider bereits vergeben. Bitte wähle ein anderes.")
public class RegistrationBean implements Serializable { public class RegistrationBean implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@NotBlank(message = "Bitte gib deinen Vornamen an.")
private String forename; private String forename;
@NotBlank(message = "Bitte gib deinen Nachnamen an.")
private String surname; private String surname;
@NotNull(message = "Bitte gib dein Geschlecht an. Wir benötigen das, um zu wissen, ob du in einem Jungs- oder Mädchenzimmer übernachten kannst.")
private EnumSex sex; private EnumSex sex;
@NotNull(message = "Bitte gib dein Geburtsdatum an. Damit errechnen wir, ob die Freizeit für dich geeignet ist.")
@DateTimeFormat(pattern="yyyy-MM-dd") @DateTimeFormat(pattern="yyyy-MM-dd")
private LocalDate birthDate; private LocalDate birthDate;
@NotBlank(message = "Bitte gib die Strasse deines Wohnsitzes an.")
private String street; private String street;
@NotBlank(message = "Bitte gib die Postleitzahl deines Wohnsitzes an.")
private String zip; private String zip;
@NotBlank(message = "Bitte gib den Ort deines Wohnsitzes an.")
private String city; private String city;
@Email(message = "Bitte gib eine gültige E-Mail-Adresse an (oder gar keine).")
private String email; private String email;
private String phone; private String phone;
private String comment; private String comment;
@NotNull(message = "Bitte gib an, zu welcher Freizeit du dich anmelden möchtest.")
private Integer fkCamp; private Integer fkCamp;
@NotNull(message = "Bitte gib an, in welcher Rolle du dich anmelden möchtest.")
private EnumCamprole campRole; private EnumCamprole campRole;
@NotNull(message = "Bitte gib an, ob du dir für spätere Anmeldungen einen Zugang einrichten willst.")
private Boolean registerInKeycloak; private Boolean registerInKeycloak;
private String login; private String login;
private String password; private String password;

View File

@ -1,10 +1,12 @@
package de.jottyfan.camporganizer.module.registration; package de.jottyfan.camporganizer.module.registration;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired; 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.validation.BindingResult;
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.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
@ -39,7 +41,14 @@ public class RegistrationController extends CommonController {
} }
@PostMapping("/registration/register") @PostMapping("/registration/register")
public String register(@ModelAttribute RegistrationBean bean, Model model) { public String register(@Valid @ModelAttribute RegistrationBean bean, Model model, final BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
super.setupSession(model, request);
CampBean campBean = service.getCamp(bean.getFkCamp());
model.addAttribute("camp", campBean);
model.addAttribute("bean", bean);
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 // 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 index(bean.getFkCamp(), model);

View File

@ -65,6 +65,22 @@ public class RegistrationGateway {
} }
} }
/**
* test if the login is available (not yet in use)
*
* @param login the login
* @return true or false
*/
public Boolean isLoginAvailable(String login) {
SelectConditionStep<TProfileRecord> sql = jooq
// @formatter:off
.selectFrom(T_PROFILE)
.where(T_PROFILE.USERNAME.eq(login));
// @formatter:on
LOGGER.debug(sql);
return sql.fetch().size() < 1;
}
/** /**
* save the content in t_person; also, create a profile for the user if * save the content in t_person; also, create a profile for the user if
* registerInKeycloak is true * registerInKeycloak is true
@ -76,10 +92,7 @@ public class RegistrationGateway {
LambdaResultWrapper lrw = new LambdaResultWrapper(); LambdaResultWrapper lrw = new LambdaResultWrapper();
jooq.transaction(t -> { jooq.transaction(t -> {
if (bean.getRegisterInKeycloak()) { if (bean.getRegisterInKeycloak()) {
SelectConditionStep<TProfileRecord> sql0 = DSL.using(t).selectFrom(T_PROFILE) if (!isLoginAvailable(bean.getLogin())) {
.where(T_PROFILE.USERNAME.eq(bean.getLogin()));
LOGGER.debug(sql0);
if (sql0.fetch().size() > 0) {
throw new DataAccessException("login already in use: " + bean.getLogin()); throw new DataAccessException("login already in use: " + bean.getLogin());
} }
// TODO: check if teacher is at least 2 years older than the camp participants // TODO: check if teacher is at least 2 years older than the camp participants

View File

@ -0,0 +1,29 @@
package de.jottyfan.camporganizer.module.registration.validate;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
/**
*
* @author jotty
*
*/
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UnusedUsernameValidator.class)
@Documented
public @interface UnusedUsername {
String message() default "username is already in use";
String field();
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@ -0,0 +1,40 @@
package de.jottyfan.camporganizer.module.registration.validate;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.factory.annotation.Autowired;
import de.jottyfan.camporganizer.module.registration.RegistrationGateway;
/**
*
* @author jotty
*
*/
public class UnusedUsernameValidator implements ConstraintValidator<UnusedUsername, Object> {
private String field;
private String message;
@Autowired
private RegistrationGateway gateway;
public void initialize(UnusedUsername uu) {
this.field = uu.field();
this.message = uu.message();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
Object login = new BeanWrapperImpl(value).getPropertyValue(field);
Boolean result = gateway.isLoginAvailable((String) login);
if (!result) {
context.buildConstraintViolationWithTemplate(message).addPropertyNode(field).addConstraintViolation()
.disableDefaultConstraintViolation();
}
return result;
}
}

View File

@ -1,5 +1,22 @@
# database credentials from defined config file # 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}
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}
ow.keycloak.admin.name=${ow.keycloak.admin.name}
ow.keycloak.admin.password=${ow.keycloak.admin.password}
# for development only # for development only
server.port = 8081 server.port = 8081

View File

@ -269,6 +269,13 @@ div {
color: red; color: red;
} }
.inputerror {
border: 1px solid red !important;
background-image: linear-gradient(to left bottom, #fff, #fcc) !important;
padding: 4px !important;
border-radius: 6px !important;
}
.locked { .locked {
background-color: rgba(255, 255, 255, 0.2) !important; background-color: rgba(255, 255, 255, 0.2) !important;
cursor: not-allowed; cursor: not-allowed;

View File

@ -21,41 +21,50 @@
<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" th:field="*{forename}" /> <span class="error" th:each="error : ${#fields.errors('surname')}">[[${error}]]<br /></span>
<input type="text" placeholder="Vorname" th:field="*{forename}" th:class="${#fields.hasErrors('forename') ? 'inputerror' : 'form-control'}" />
</div> </div>
<div class="col-sm-6 rowdist"> <div class="col-sm-6 rowdist">
<input type="text" class="form-control" placeholder="Nachname" th:field="*{surname}" /> <span class="error" th:each="error : ${#fields.errors('forename')}">[[${error}]]<br /></span>
<input type="text" placeholder="Nachname" th:field="*{surname}" th:class="${#fields.hasErrors('surname') ? 'inputerror' : 'form-control'}" />
</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" th:field="*{sex}"> <span class="error" th:each="error : ${#fields.errors('sex')}">[[${error}]]<br /></span>
<select th:field="*{sex}" th:class="${#fields.hasErrors('sex') ? 'inputerror' : 'form-select'}">
<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" th:field="*{birthDate}" /> <span class="error" th:each="error : ${#fields.errors('birthDate')}">[[${error}]]<br /></span>
<input type="date" placeholder="Geburtsdatum" th:field="*{birthDate}" th:class="${#fields.hasErrors('birthDate') ? 'inputerror' : 'form-control'}" />
</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" th:field="*{street}" /> <span class="error" th:each="error : ${#fields.errors('street')}">[[${error}]]<br /></span>
<input type="text" placeholder="Straße und Hausnummer" th:field="*{street}" th:class="${#fields.hasErrors('street') ? 'inputerror' : 'form-control'}" />
</div> </div>
<div class="col-sm-2 rowdist"> <div class="col-sm-2 rowdist">
<input type="text" class="form-control" placeholder="PLZ" th:field="*{zip}" /> <span class="error" th:each="error : ${#fields.errors('zip')}">[[${error}]]<br /></span>
<input type="text" placeholder="PLZ" th:field="*{zip}" th:class="${#fields.hasErrors('zip') ? 'inputerror' : 'form-control'}" />
</div> </div>
<div class="col-sm-4 rowdist"> <div class="col-sm-4 rowdist">
<input type="text" class="form-control" placeholder="Ort" th:field="*{city}" /> <span class="error" th:each="error : ${#fields.errors('city')}">[[${error}]]<br /></span>
<input type="text" placeholder="Ort" th:field="*{city}" th:class="${#fields.hasErrors('city') ? 'inputerror' : 'form-control'}" />
</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" th:field="*{email}" /> <span class="error" th:each="error : ${#fields.errors('email')}">[[${error}]]<br /></span>
<input type="text" placeholder="E-Mail" th:field="*{email}" th:class="${#fields.hasErrors('email') ? 'inputerror' : 'form-control'}" />
</div> </div>
<div class="col-sm-6 rowdist"> <div class="col-sm-6 rowdist">
<input type="text" class="form-control" placeholder="Telefonnummer" th:field="*{phone}" /> <span class="error" th:each="error : ${#fields.errors('phone')}">[[${error}]]<br /></span>
<input type="text" placeholder="Telefonnummer" th:field="*{phone}" th:class="${#fields.hasErrors('phone') ? 'inputerror' : 'form-control'}" />
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@ -63,7 +72,8 @@
<span>mitkommen als</span> <span>mitkommen als</span>
</div> </div>
<div class="col-sm-6 rowdist"> <div class="col-sm-6 rowdist">
<select class="form-select" th:field="*{campRole}"> <span class="error" th:each="error : ${#fields.errors('campRole')}">[[${error}]]<br /></span>
<select th:field="*{campRole}" th:class="${#fields.hasErrors('campRole') ? 'inputerror' : 'form-select'}">
<option value="student">Teilnehmer</option> <option value="student">Teilnehmer</option>
<option value="teacher">Mitarbeiter</option> <option value="teacher">Mitarbeiter</option>
<option value="feeder">Küchenteam</option> <option value="feeder">Küchenteam</option>
@ -86,10 +96,12 @@
</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" th:field="*{login}" /> <span class="error" th:each="error : ${#fields.errors('login')}">[[${error}]]<br /></span>
<input type="text" placeholder="Login" th:field="*{login}" th:class="${#fields.hasErrors('login') ? 'inputerror' : 'form-control'}" />
</div> </div>
<div class="col-sm-6 rowdist"> <div class="col-sm-6 rowdist">
<input type="password" class="form-control" placeholder="Passwort" th:field="*{password}" /> <span class="error" th:each="error : ${#fields.errors('password')}">[[${error}]]<br /></span>
<input type="password" placeholder="Passwort" th:field="*{password}" th:class="${#fields.hasErrors('password') ? 'inputerror' : 'form-control'}" />
</div> </div>
</div> </div>
<div class="row"> <div class="row">