diff --git a/README.md b/README.md
index e69632d..bf412be 100644
--- a/README.md
+++ b/README.md
@@ -165,6 +165,32 @@ spring:
server:
port: 8081
+ servlet:
+ context-path: /
+```
+
+#### Change the Context-Path
+
+The context-path or base-path of the application can be changed using the following property:
+
+```
+server:
+ servlet:
+ context-path: /tasklist/
+```
+
+It is then available under http://localhost:8081/tasklist.
+
+#### Customize the Look & Feel
+
+You can customize the look & feel of the Zeebe Simple Tasklist (aka. white-labeling). For example, to change the logo or
+alter the background color. The following configurations are available:
+
+```
+- white-label.logo.path=img/logo.png
+- white-label.custom.title=Zeebe Simple Tasklist
+- white-label.custom.css.path=css/custom.css
+- white-label.custom.js.path=js/custom.js
```
#### Change the Database
diff --git a/pom.xml b/pom.xml
index 624086b..a9feb8f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -145,8 +145,8 @@
org.webjars
- webjars-locator-core
- 0.52
+ webjars-locator
+ 0.42
org.webjars
diff --git a/src/main/java/io/zeebe/tasklist/WebSecurityConfig.java b/src/main/java/io/zeebe/tasklist/WebSecurityConfig.java
index bb35398..98f2ebf 100644
--- a/src/main/java/io/zeebe/tasklist/WebSecurityConfig.java
+++ b/src/main/java/io/zeebe/tasklist/WebSecurityConfig.java
@@ -21,9 +21,9 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
- .antMatchers("/css/**", "/img/**", "/js/**", "/fonts/**", "/favicon.ico")
+ .antMatchers("/css/**", "/img/**", "/js/**", "/fonts/**", "/favicon.ico", "/webjars/**")
.permitAll()
- .antMatchers("/login", "/login-error")
+ .antMatchers("/login", "/login-error", "/notifications/**")
.permitAll()
.antMatchers("/views/users", "/api/users", "/views/groups", "/api/groups")
.hasRole(Roles.ADMIN.name())
diff --git a/src/main/java/io/zeebe/tasklist/view/AbstractViewController.java b/src/main/java/io/zeebe/tasklist/view/AbstractViewController.java
new file mode 100644
index 0000000..26207c9
--- /dev/null
+++ b/src/main/java/io/zeebe/tasklist/view/AbstractViewController.java
@@ -0,0 +1,59 @@
+package io.zeebe.tasklist.view;
+
+import io.zeebe.tasklist.Roles;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Pageable;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+abstract class AbstractViewController {
+
+ private static final int FIRST_PAGE = 0;
+ private static final int PAGE_RANGE = 2;
+
+ @Autowired private WhitelabelProperties whitelabelProperties;
+ @Autowired private WhitelabelPropertiesMapper whitelabelPropertiesMapper;
+
+ protected void addPaginationToModel(
+ final Map model, final Pageable pageable, final long count) {
+
+ final int currentPage = pageable.getPageNumber();
+ model.put("currentPage", currentPage);
+ model.put("page", currentPage + 1);
+ if (currentPage > 0) {
+ model.put("prevPage", currentPage - 1);
+ }
+ if (count > (1 + currentPage) * pageable.getPageSize()) {
+ model.put("nextPage", currentPage + 1);
+ }
+ }
+
+ /*
+ * Needs to be added manually, since Spring does not detect @ModelAttribute in abstract classes.
+ */
+ protected void addDefaultAttributesToModel(Map model) {
+ whitelabelPropertiesMapper.addPropertiesToModel(model, whitelabelProperties);
+ }
+
+ protected void addCommonsToModel(Map model) {
+
+ final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ final List authorities =
+ authentication.getAuthorities().stream()
+ .map(GrantedAuthority::getAuthority)
+ .collect(Collectors.toList());
+
+ final UserDto userDto = new UserDto();
+ userDto.setName(authentication.getName());
+
+ final boolean isAdmin = authorities.contains("ROLE_" + Roles.ADMIN);
+ userDto.setAdmin(isAdmin);
+
+ model.put("user", userDto);
+ }
+}
diff --git a/src/main/java/io/zeebe/tasklist/view/ErrorMessage.java b/src/main/java/io/zeebe/tasklist/view/ErrorMessage.java
new file mode 100644
index 0000000..f45d1fe
--- /dev/null
+++ b/src/main/java/io/zeebe/tasklist/view/ErrorMessage.java
@@ -0,0 +1,18 @@
+package io.zeebe.tasklist.view;
+
+public class ErrorMessage {
+
+ private String message;
+
+ public ErrorMessage(String message) {
+ this.message = message;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(final String message) {
+ this.message = message;
+ }
+}
diff --git a/src/main/java/io/zeebe/tasklist/view/ExceptionHandler.java b/src/main/java/io/zeebe/tasklist/view/ExceptionHandler.java
new file mode 100644
index 0000000..ab55fbb
--- /dev/null
+++ b/src/main/java/io/zeebe/tasklist/view/ExceptionHandler.java
@@ -0,0 +1,50 @@
+package io.zeebe.tasklist.view;
+
+
+import io.camunda.zeebe.client.api.command.ClientException;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.context.request.WebRequest;
+
+@ControllerAdvice
+public class ExceptionHandler {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ExceptionHandler.class);
+
+ private final WhitelabelProperties whitelabelProperties;
+ private final WhitelabelPropertiesMapper whitelabelPropertiesMapper;
+
+ public ExceptionHandler(
+ WhitelabelProperties whitelabelProperties,
+ WhitelabelPropertiesMapper whitelabelPropertiesMapper) {
+ this.whitelabelProperties = whitelabelProperties;
+ this.whitelabelPropertiesMapper = whitelabelPropertiesMapper;
+ }
+
+ @org.springframework.web.bind.annotation.ExceptionHandler(value = {ClientException.class})
+ protected ResponseEntity