From 9e0d84c639beb1048d38c03b1b3969db63fba9ff Mon Sep 17 00:00:00 2001
From: Sonia Zorba <sonia.zorba@inaf.it>
Date: Thu, 14 May 2020 15:16:35 +0200
Subject: [PATCH] #3 Improved error messages

---
 gms-ui/src/App.vue                            |  4 +-
 gms-ui/src/api/server/index.js                | 12 +--
 gms/pom.xml                                   |  2 +-
 .../it/inaf/ia2/gms/authn/SecurityConfig.java | 12 ++-
 .../gms/controller/HomePageController.java    |  3 +-
 .../gms/controller/KeepAliveController.java   |  7 +-
 .../gms/exception/BadRequestException.java    |  2 +-
 .../ia2/gms/exception/ErrorController.java    | 92 +++++++++++++++++++
 .../inaf/ia2/gms/exception/GmsException.java  |  8 ++
 .../ia2/gms/exception/NotFoundException.java  |  2 +-
 .../gms/exception/UnauthorizedException.java  |  2 +-
 .../inaf/ia2/gms/persistence/LoggingDAO.java  |  4 +-
 gms/src/main/resources/application.properties |  6 +-
 gms/src/main/resources/public/error/404.html  | 14 +++
 .../main/resources/public/error/error.html    | 16 ++++
 15 files changed, 163 insertions(+), 23 deletions(-)
 create mode 100644 gms/src/main/java/it/inaf/ia2/gms/exception/ErrorController.java
 create mode 100644 gms/src/main/java/it/inaf/ia2/gms/exception/GmsException.java
 create mode 100644 gms/src/main/resources/public/error/404.html
 create mode 100644 gms/src/main/resources/public/error/error.html

diff --git a/gms-ui/src/App.vue b/gms-ui/src/App.vue
index 152a93a..cd5b2f3 100644
--- a/gms-ui/src/App.vue
+++ b/gms-ui/src/App.vue
@@ -41,8 +41,8 @@ export default {
   mounted: function() {
     var self = this;
     document.addEventListener('apiError', function(event) {
-      self.$bvToast.toast(event.message, {
-        title: "Error",
+      self.$bvToast.toast(event.message.body, {
+        title: event.message.title,
         variant: 'danger',
         solid: true
       });
diff --git a/gms-ui/src/api/server/index.js b/gms-ui/src/api/server/index.js
index d5c81cc..61abe17 100644
--- a/gms-ui/src/api/server/index.js
+++ b/gms-ui/src/api/server/index.js
@@ -26,15 +26,11 @@ function apiRequest(url, options, showLoading = true) {
 }
 
 function dispatchApiErrorEvent(error) {
-  let message;
-  if (error.message) {
-    message = error.message;
-  } else {
-    message = 'Generic error';
-  }
-
   let event = new CustomEvent('apiError');
-  event.message = message;
+  event.message = {
+    title: error.error || 'Error',
+    body: error.message || 'Unknown error'
+  };
   document.dispatchEvent(event);
 }
 
diff --git a/gms/pom.xml b/gms/pom.xml
index fb2694e..cc40470 100644
--- a/gms/pom.xml
+++ b/gms/pom.xml
@@ -5,7 +5,7 @@
     <parent>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-parent</artifactId>
-        <version>2.1.7.RELEASE</version>
+        <version>2.2.7.RELEASE</version>
         <relativePath/> <!-- lookup parent from repository -->
     </parent>
     <groupId>it.inaf.ia2</groupId>
diff --git a/gms/src/main/java/it/inaf/ia2/gms/authn/SecurityConfig.java b/gms/src/main/java/it/inaf/ia2/gms/authn/SecurityConfig.java
index 63b992e..8d47685 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/authn/SecurityConfig.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/authn/SecurityConfig.java
@@ -18,6 +18,8 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.builders.WebSecurity;
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 import org.springframework.security.oauth2.provider.token.store.jwk.JwkTokenStore;
+import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.web.cors.CorsConfiguration;
 import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
 import org.springframework.web.filter.CorsFilter;
@@ -45,14 +47,20 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
     @Override
     public void configure(HttpSecurity http) throws Exception {
 
-        super.configure(http);
-
         // CORS are necessary only for development (API access from npm server)
         if (Arrays.asList(env.getActiveProfiles()).contains("dev")) {
             http.authorizeRequests()
                     .antMatchers(HttpMethod.OPTIONS, "/**").permitAll();
         }
 
+        super.configure(http);
+
+        // avoid displaying the annoying BasicAuth browser popup when the
+        // session expires (this should happen mostly during development)
+        // [401 WWW-Authenticate is converted to 403]
+        http.exceptionHandling().defaultAuthenticationEntryPointFor(
+                new Http403ForbiddenEntryPoint(), new AntPathRequestMatcher("/keepAlive"));
+
         http.csrf().disable();
     }
 
diff --git a/gms/src/main/java/it/inaf/ia2/gms/controller/HomePageController.java b/gms/src/main/java/it/inaf/ia2/gms/controller/HomePageController.java
index ced3a3d..bb5240f 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/controller/HomePageController.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/controller/HomePageController.java
@@ -34,7 +34,7 @@ public class HomePageController {
     private InvitedRegistrationManager invitedRegistrationManager;
 
     @ResponseBody
-    @GetMapping(value = "/home", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+    @GetMapping(value = "/home", produces = MediaType.APPLICATION_JSON_VALUE)
     public ResponseEntity<HomePageResponse> getMainPage(@Valid GroupsRequest request) {
 
         HomePageResponse response = new HomePageResponse();
@@ -56,7 +56,6 @@ public class HomePageController {
         if (optReg.isPresent()) {
             request.setAttribute("invited-registration", optReg.get());
             return "/registration-completed";
-            //request.getRequestDispatcher("/registration-completed").forward(request, response);
         }
 
         return "index.html";
diff --git a/gms/src/main/java/it/inaf/ia2/gms/controller/KeepAliveController.java b/gms/src/main/java/it/inaf/ia2/gms/controller/KeepAliveController.java
index f543633..c41f012 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/controller/KeepAliveController.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/controller/KeepAliveController.java
@@ -2,9 +2,11 @@ package it.inaf.ia2.gms.controller;
 
 import it.inaf.ia2.gms.authn.SessionData;
 import it.inaf.ia2.gms.rap.RapClient;
+import java.util.HashMap;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RestController;
@@ -20,13 +22,14 @@ public class KeepAliveController {
     @Autowired
     private RapClient rapClient;
 
-    @GetMapping("/keepAlive")
+    @GetMapping(value = "/keepAlive", produces = MediaType.APPLICATION_JSON_VALUE)
     public ResponseEntity<?> keepAlive() {
         LOG.trace("Keepalive called");
         if (sessionData.getExpiresIn() < 60) {
             rapClient.refreshToken();
             LOG.trace("RAP token refreshed");
         }
-        return ResponseEntity.noContent().build();
+        // empty JSON object response
+        return ResponseEntity.ok(new HashMap<>());
     }
 }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/exception/BadRequestException.java b/gms/src/main/java/it/inaf/ia2/gms/exception/BadRequestException.java
index 50f633f..535c5a9 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/exception/BadRequestException.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/exception/BadRequestException.java
@@ -4,7 +4,7 @@ import org.springframework.http.HttpStatus;
 import org.springframework.web.bind.annotation.ResponseStatus;
 
 @ResponseStatus(value = HttpStatus.BAD_REQUEST)
-public class BadRequestException extends RuntimeException {
+public class BadRequestException extends GmsException {
 
     public BadRequestException(String message) {
         super(message);
diff --git a/gms/src/main/java/it/inaf/ia2/gms/exception/ErrorController.java b/gms/src/main/java/it/inaf/ia2/gms/exception/ErrorController.java
new file mode 100644
index 0000000..7fb46c4
--- /dev/null
+++ b/gms/src/main/java/it/inaf/ia2/gms/exception/ErrorController.java
@@ -0,0 +1,92 @@
+package it.inaf.ia2.gms.exception;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Map;
+import java.util.Scanner;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.web.servlet.error.AbstractErrorController;
+import org.springframework.boot.web.servlet.error.ErrorAttributes;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("${server.error.path:${error.path:/error}}")
+public class ErrorController extends AbstractErrorController {
+
+    @Value("${support.contact.label}")
+    private String supportContactLabel;
+    @Value("${support.contact.email}")
+    private String supportContactEmail;
+
+    @Autowired
+    public ErrorController(ErrorAttributes errorAttributes) {
+        super(errorAttributes);
+    }
+
+    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
+    public void errorHtml(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+        Map<String, Object> errors = super.getErrorAttributes(request, true);
+
+        HttpStatus status = getStatus(request);
+
+        String responseText;
+        if (status == HttpStatus.NOT_FOUND) {
+            responseText = getFileContent("404.html");
+        } else {
+            responseText = getFileContent("error.html")
+                    .replace("#ERROR_TITLE#", (String) errors.get("error"))
+                    .replace("#ERROR_MESSAGE#", (String) errors.get("message"))
+                    .replace("#ADDITIONAL_MESSAGE#", getAdditionalMessage(status));
+        }
+
+        response.setContentType("text/html;charset=UTF-8");
+        response.getOutputStream().print(responseText);
+    }
+
+    private String getAdditionalMessage(HttpStatus status) {
+        if (status.is5xxServerError()) {
+            // unexpected error -> let users report the issue
+            return "<br/>If you need support please contact"
+                    + " <a href=\"mailto:" + supportContactEmail + "\">" + supportContactLabel + "</a>";
+        }
+        return "";
+    }
+
+    @RequestMapping(produces = MediaType.TEXT_PLAIN_VALUE)
+    public void errorText(HttpServletRequest request, HttpServletResponse response) throws Exception {
+        Map<String, Object> errors = super.getErrorAttributes(request, true);
+        response.setContentType("text/plain;charset=UTF-8");
+        response.getOutputStream().print(errors.get("error") + ": " + errors.get("message"));
+    }
+
+    @RequestMapping
+    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
+        HttpStatus status = getStatus(request);
+        if (status == HttpStatus.NO_CONTENT) {
+            return new ResponseEntity<>(status);
+        }
+        Map<String, Object> body = getErrorAttributes(request, false);
+        return new ResponseEntity<>(body, status);
+    }
+
+    private String getFileContent(String templateFileName) throws IOException {
+        try (InputStream in = ErrorController.class.getClassLoader()
+                .getResourceAsStream("public/error/" + templateFileName)) {
+            Scanner s = new Scanner(in).useDelimiter("\\A");
+            return s.hasNext() ? s.next() : "";
+        }
+    }
+
+    @Override
+    public String getErrorPath() {
+        return null;
+    }
+}
diff --git a/gms/src/main/java/it/inaf/ia2/gms/exception/GmsException.java b/gms/src/main/java/it/inaf/ia2/gms/exception/GmsException.java
new file mode 100644
index 0000000..c7a5ce4
--- /dev/null
+++ b/gms/src/main/java/it/inaf/ia2/gms/exception/GmsException.java
@@ -0,0 +1,8 @@
+package it.inaf.ia2.gms.exception;
+
+public abstract class GmsException extends RuntimeException {
+
+    public GmsException(String message) {
+        super(message);
+    }
+}
diff --git a/gms/src/main/java/it/inaf/ia2/gms/exception/NotFoundException.java b/gms/src/main/java/it/inaf/ia2/gms/exception/NotFoundException.java
index f04bc1f..da5acb8 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/exception/NotFoundException.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/exception/NotFoundException.java
@@ -4,7 +4,7 @@ import org.springframework.http.HttpStatus;
 import org.springframework.web.bind.annotation.ResponseStatus;
 
 @ResponseStatus(value = HttpStatus.NOT_FOUND)
-public class NotFoundException extends RuntimeException {
+public class NotFoundException extends GmsException {
 
     public NotFoundException(String message) {
         super(message);
diff --git a/gms/src/main/java/it/inaf/ia2/gms/exception/UnauthorizedException.java b/gms/src/main/java/it/inaf/ia2/gms/exception/UnauthorizedException.java
index e4f73db..70f4465 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/exception/UnauthorizedException.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/exception/UnauthorizedException.java
@@ -4,7 +4,7 @@ import org.springframework.http.HttpStatus;
 import org.springframework.web.bind.annotation.ResponseStatus;
 
 @ResponseStatus(value = HttpStatus.UNAUTHORIZED)
-public class UnauthorizedException extends RuntimeException {
+public class UnauthorizedException extends GmsException {
 
     public UnauthorizedException(String message) {
         super(message);
diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/LoggingDAO.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/LoggingDAO.java
index b82e83d..b04b810 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/persistence/LoggingDAO.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/LoggingDAO.java
@@ -90,8 +90,10 @@ public class LoggingDAO {
     private String getUser(HttpServletRequest request) {
         if (request.getUserPrincipal() != null && request.getUserPrincipal() instanceof RapPrincipal) {
             return request.getUserPrincipal().getName();
-        } else {
+        } else if (request.getSession(false) != null) {
             return sessionData.getUserId();
+        } else {
+            return null;
         }
     }
 }
diff --git a/gms/src/main/resources/application.properties b/gms/src/main/resources/application.properties
index 902956e..19b3dac 100644
--- a/gms/src/main/resources/application.properties
+++ b/gms/src/main/resources/application.properties
@@ -2,6 +2,7 @@ server.port=8082
 server.servlet.context-path=/gms
 
 spring.main.allow-bean-definition-overriding=true
+server.error.whitelabel.enabled=false
 
 security.oauth2.client.client-id=gms
 security.oauth2.client.client-secret=gms-secret
@@ -16,12 +17,13 @@ logging.level.org.springframework.security=DEBUG
 logging.level.org.springframework.jdbc=TRACE
 logging.level.org.springframework.web=TRACE
 
-spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
+spring.datasource.url=jdbc:postgresql://localhost:5433/postgres
 spring.datasource.username=gms
 spring.datasource.password=gms
 
 rap.ws-url=http://localhost/rap-ia2/ws
-rap.ws.basic-auth=false
+support.contact.label=IA2 team
+support.contact.email=ia2@inaf.it
 
 # For development only:
 spring.profiles.active=dev
diff --git a/gms/src/main/resources/public/error/404.html b/gms/src/main/resources/public/error/404.html
new file mode 100644
index 0000000..f9d3cfe
--- /dev/null
+++ b/gms/src/main/resources/public/error/404.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>Page Not Found</title>
+        <meta charset="UTF-8" />
+        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous" />
+    </head>
+    <body>
+        <div class="container mt-4">
+            <h1 class="mb-3 text-primary">Page Not Found</h1>
+        </div>
+    </body>
+</html>
diff --git a/gms/src/main/resources/public/error/error.html b/gms/src/main/resources/public/error/error.html
new file mode 100644
index 0000000..b0fa515
--- /dev/null
+++ b/gms/src/main/resources/public/error/error.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>#ERROR_TITLE#</title>
+        <meta charset="UTF-8" />
+        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous" />
+    </head>
+    <body>
+        <div class="container mt-4">
+            <h1 class="mb-3 text-danger">#ERROR_TITLE#</h1>
+            <p><strong>#ERROR_MESSAGE#</strong></p>
+            <p>#ADDITIONAL_MESSAGE#</p>
+        </div>
+    </body>
+</html>
-- 
GitLab