├── .gitignore ├── LICENSE ├── README.md ├── apigateway ├── .gitignore ├── pom.xml ├── runLocal.bat ├── runLocal.sh ├── runLocalNoSecurity.bat ├── runLocalNoSecurity.sh └── src │ ├── main │ ├── java │ │ └── de │ │ │ └── muenchen │ │ │ └── cove │ │ │ ├── ApiGatewayApplication.java │ │ │ ├── configuration │ │ │ ├── GuiConfiguration.java │ │ │ ├── MonitoringConfig.java │ │ │ ├── NoSecurityConfiguration.java │ │ │ └── SecurityConfiguration.java │ │ │ ├── controller │ │ │ └── PingController.java │ │ │ ├── exception │ │ │ └── ParameterPollutionException.java │ │ │ ├── filter │ │ │ ├── CsrfTokenAppendingHelperFilter.java │ │ │ ├── CustomTokenRelayGatewayFilterFactory.java │ │ │ ├── DistributedTracingFilter.java │ │ │ ├── GlobalAuthenticationErrorFilter.java │ │ │ ├── GlobalBackendErrorFilter.java │ │ │ └── GlobalRequestParameterPollutionFilter.java │ │ │ └── util │ │ │ └── GatewayUtils.java │ └── resources │ │ ├── application-dev.yml │ │ ├── application-kon.yml │ │ ├── application-local.yml │ │ ├── application-no-security.yml │ │ ├── application-prod.yml │ │ ├── application-test.yml │ │ ├── application.yml │ │ ├── banner.txt │ │ └── logback-spring.xml │ └── test │ └── java │ └── de │ └── muenchen │ └── cove │ ├── TestConstants.java │ ├── controller │ └── PingControllerTest.java │ ├── filter │ ├── CsrfTokenAppendingHelperFilterTest.java │ ├── GlobalAuthenticationErrorFilterTest.java │ ├── GlobalBackendErrorFilterTest.java │ └── GlobalRequestParameterPollutionFilterTest.java │ └── route │ └── BackendRouteTest.java ├── frontend ├── .browserslistrc ├── .env.development ├── .env.production ├── .eslintrc.js ├── .gitignore ├── README.md ├── babel.config.js ├── jest.config.js ├── package-lock.json ├── package.json ├── pom.xml ├── postcss.config.js ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── api │ │ ├── BerichtService.ts │ │ ├── FetchUtils.ts │ │ ├── HealthService.ts │ │ ├── PersonService.ts │ │ ├── error.ts │ │ └── types │ │ │ └── PersonSearchparams.ts │ ├── assets │ │ ├── corona.png │ │ └── logo.png │ ├── components │ │ ├── Common │ │ │ ├── DatetimeInput.vue │ │ │ ├── LoeschenDialog.vue │ │ │ └── YesNoDialog.vue │ │ ├── IndexTable.vue │ │ ├── KontaktFields.vue │ │ ├── KontaktTable.vue │ │ ├── PersonFields.vue │ │ ├── PersonenAuswaehler.vue │ │ ├── PersonenListe.vue │ │ ├── PersonenSuche.vue │ │ ├── ProbeFields.vue │ │ ├── ProbeTable.vue │ │ ├── TheSnackbar.vue │ │ └── call │ │ │ ├── CallDispositionPersonenTable.vue │ │ │ ├── CallPersonFields.vue │ │ │ ├── CallPersonenTable.vue │ │ │ ├── CallReminderButton.vue │ │ │ └── CallingPopup.vue │ ├── main.ts │ ├── mixins │ │ ├── formatter.ts │ │ └── saveLeaveMixin.ts │ ├── plugins │ │ └── vuetify.ts │ ├── router.ts │ ├── shims-tsx.d.ts │ ├── shims-vue.d.ts │ ├── store │ │ ├── index.ts │ │ └── modules │ │ │ ├── snackbar.ts │ │ │ └── user.ts │ ├── types │ │ ├── Bericht.ts │ │ ├── DailyCallReport.ts │ │ ├── Ergebnis.ts │ │ ├── Gespraeche.ts │ │ ├── HealthState.ts │ │ ├── Kategorie.ts │ │ ├── KeyVal.ts │ │ ├── Kontakt.ts │ │ ├── KontaktTyp.ts │ │ ├── MedEinrichtung.ts │ │ ├── Person.ts │ │ └── SearchResult.ts │ └── views │ │ ├── AnrufsplanungView.vue │ │ ├── BerichtView.vue │ │ ├── EndgespraecheView.vue │ │ ├── Main.vue │ │ ├── PersonCreateView.vue │ │ ├── PersonEditView.vue │ │ ├── PersonReadView.vue │ │ ├── PersonSearchView.vue │ │ └── TaeglicheAnrufeView.vue ├── tests │ └── unit │ │ └── example.spec.ts ├── tsconfig.json └── vue.config.js ├── img ├── Anrufliste-Abschlussgespraeche.png ├── Anrufliste-Indexpersonen.png ├── Anrufplanung.png ├── COVe_Bausteinsicht.png ├── COVe_Grafik.jpg ├── Datepicker.png ├── Popup-Telefonat-index-Ausschnitt.png ├── Preview-Kontakt.png ├── Reload.png ├── Start.png └── Suche.png └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | # Covers Maven specific 2 | target/ 3 | pom.xml.tag 4 | pom.xml.releaseBackup 5 | pom.xml.versionsBackup 6 | pom.xml.next 7 | release.properties 8 | dependency-reduced-pom.xml 9 | buildNumber.properties 10 | .mvn/timing.properties 11 | !/.mvn/wrapper/maven-wrapper.jar 12 | 13 | # Covers Eclipse specific: 14 | .settings/ 15 | .classpath 16 | .project 17 | 18 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 19 | .idea 20 | *.iml 21 | 22 | # Covers Netbeans: 23 | **/nbproject/private/ 24 | **/nbproject/Makefile-*.mk 25 | **/nbproject/Package-*.bash 26 | build/ 27 | nbbuild/ 28 | dist/ 29 | nbdist/ 30 | .nb-gradle/ 31 | 32 | 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # COVe Frontend 2 | 3 | ## Inhaltsverzeichnis 4 | 5 | * [Über das Projekt](#über-das-projekt) 6 | * [Architektur](#architektur) 7 | * [Verwendete Technologien](#verwendete-technologien) 8 | * [Erste Schritte](#erste-schritte) 9 | * [Installation](#installation) 10 | * [Konfiguration](#konfiguration) 11 | * [Routen](#routen) 12 | * [OAuth](#oauth) 13 | * [Frontend](#frontend) 14 | * [Verwendung](#verwendung) 15 | * [Lizenz](#lizenz) 16 | 17 | 18 | ## Über das Projekt 19 | 20 | In Zeiten von COVID-19 müssen Gesundheitsämter die gemeldeten COVID-19-Verdachtsfälle und deren 21 | Kontaktpersonen erfassen, Laborergebnisse und Quarantäne-Zeiträume dokumentieren und 22 | zeitgleich viele Telefonanrufe mit den Betroffenen führen. 23 | 24 | COVe (COVID-19-Verdachtsfall-Verwaltung) vereint dies innerhalb einer modernen Web-App. Durch sie lassen sich Verdachtsfälle einfach erfassen, die Anrufe leichter organisieren und die Ergebnisse schneller dokumentieren. 25 | Durch den innovativen Ansatz der Telefonlisten haben alle Mitarbeiterinnen und Mitarbeiter eines Telefonservices gleichzeitig Zugriff auf aktuelle Daten. 26 | Das spart erheblich Zeit in der Krisensituation. 27 | 28 | ![Funktionsweise][functionality-screenshot] 29 | 30 | 31 | ### Architektur 32 | 33 | ![Architektur][architecture-screenshot] 34 | 35 | COVe-Backend: https://github.com/it-at-m/cove-backend 36 | 37 | COVe-Frontend: https://github.com/it-at-m/cove-frontend 38 | 39 | 40 | ### Verwendete Technologien 41 | 42 | * [Java](https://www.java.com/de/) 43 | * [Maven](https://maven.apache.org/) 44 | * [Spring Boot](https://spring.io/projects/spring-boot) 45 | * [Node.js](https://nodejs.org/) 46 | * [VUE.js](https://vuejs.org/) 47 | 48 | 49 | ## Erste Schritte 50 | 51 | Für das erfolgreiche Bauen und Ausführen der Anwendung sollte **Java**, **Maven**, **Node** und **Vue** bereits installiert und eingerichtet sein. 52 | Desweiteren wird für den Security Modus eine **Single-Sign-On Umgebung** benötigt. In unserem Fall wurde Keycloak verwendet. 53 | Es kann aber auch jeder andere OAuth2-Provider (OpenID-Connect) wie zum Beispiel AWS Cognito genutzt werden. 54 | 55 | 56 | ### Installation 57 | 58 | 1. Das Repository clonen 59 | ```shell script 60 | git clone https://github.com/it-at-m/cove-frontend.git 61 | ``` 62 | 63 | 64 | ### Konfiguration 65 | 66 | Vor der Verwendung der Anwendung müssen noch einige Konfigurationen vorgenommen werden. 67 | 68 | 69 | #### Routen 70 | 71 | In der application.yml der jeweiligen Umgebung muss die Route für das Backend noch konfiguriert werden. 72 | ```yaml 73 | routes: 74 | - id: backend 75 | uri: 76 | predicates: 77 | - Path=/api/cove-backend-service/** 78 | filters: 79 | - RewritePath=/api/cove-backend-service/(?.*), /$\{urlsegments} 80 | - RemoveResponseHeader=WWW-Authenticate 81 | ``` 82 | 83 | 84 | #### OAuth 85 | 86 | In der application.yml der jeweiligen Umgebung muss der Realm, die Keycloak-issuer-uri, die 87 | Client-id und das Client-secret noch gesetzt werden. 88 | 89 | ```yaml 90 | # security config 91 | security: 92 | oauth2: 93 | client: 94 | provider: 95 | keycloak: 96 | issuer-uri: /auth/realms/${spring.realm} 97 | registration: 98 | keycloak: 99 | client-id: client_name 100 | client-secret: client_secret 101 | ``` 102 | 103 | 104 | #### Frontend 105 | 106 | Unter dem Ordner `frontend` können folgende Parameter für die entwicklungs und produktiv Umgebung in der Datei `.env.development` bzw. `.env.production` gesetzt werden. 107 | 108 | ```text 109 | NODE_ENV= ENVIRONMENT 110 | VUE_APP_API_URL= URL_API 111 | VUE_APP_FAQ= URL_FAQ 112 | VUE_APP_BENUTZERHANDBUCH= URL_Benutzerhandbuch 113 | ``` 114 | 115 | 116 | ## Verwendung 117 | 118 | Die Anwendung besitzt folgende Spring Profiles: 119 | 120 | - security (defaultmäßig aktiviert) 121 | - no-security 122 | - local 123 | - dev 124 | - test 125 | - kon 126 | - prod 127 | 128 | Um die Anwendung local zu starten, können folgende zwei Skripte ausgeführt werden: 129 | ```shell script 130 | # Mit Security 131 | ./runLocal.sh 132 | 133 | # Ohne Security 134 | ./runLocalNosecurity.sh 135 | ``` 136 | 137 | Eine weitere Möglichkeit ist es, dass Maven Plugin zu verwenden: 138 | ```shell script 139 | # Ausführbare Jar Datei erzeugen 140 | mvn clean install 141 | 142 | # Anwendung mit jeweiligen Profil starten (Bsp.: local,no-security) 143 | mvn clean spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=local,no-security" 144 | ``` 145 | 146 | ## Lizenzierung und Copyright ## 147 | © Copyright 2020 – it@M 148 | 149 | *COVe* ist lizenziert unter der *European Union Public Licence (EUPL)*. 150 | Für mehr Informationen siehe `LICENSE`. 151 | 152 | 153 | 154 | 155 | [functionality-screenshot]: img/COVe_Grafik.jpg 156 | [architecture-screenshot]: img/COVe_Bausteinsicht.png 157 | -------------------------------------------------------------------------------- /apigateway/.gitignore: -------------------------------------------------------------------------------- 1 | # Covers Maven specific 2 | target/ 3 | pom.xml.tag 4 | pom.xml.releaseBackup 5 | pom.xml.versionsBackup 6 | pom.xml.next 7 | release.properties 8 | dependency-reduced-pom.xml 9 | buildNumber.properties 10 | .mvn/timing.properties 11 | !/.mvn/wrapper/maven-wrapper.jar 12 | 13 | # Covers Eclipse specific: 14 | .settings/ 15 | .classpath 16 | .project 17 | 18 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 19 | .idea 20 | *.iml 21 | 22 | # Covers Netbeans: 23 | **/nbproject/private/ 24 | **/nbproject/Makefile-*.mk 25 | **/nbproject/Package-*.bash 26 | build/ 27 | nbbuild/ 28 | dist/ 29 | nbdist/ 30 | .nb-gradle/ 31 | 32 | 33 | -------------------------------------------------------------------------------- /apigateway/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | de.muenchen.cove.apigateway 8 | cove-frontend-apigateway 9 | 1.0-SNAPSHOT 10 | jar 11 | cove_frontend_apigateway 12 | 13 | 14 | 15 | UTF-8 16 | 1.8 17 | 1.8 18 | 1.8 19 | 2.2.4.RELEASE 20 | Hoxton.SR2 21 | 6.2 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-dependencies 31 | ${spring.boot.version} 32 | pom 33 | import 34 | 35 | 36 | 37 | org.springframework.cloud 38 | spring-cloud-dependencies 39 | ${spring.cloud.version} 40 | pom 41 | import 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | org.springframework.cloud 52 | spring-cloud-starter-gateway 53 | 54 | 55 | 56 | 57 | org.springframework.boot 58 | spring-boot-starter-hateoas 59 | 60 | 61 | 62 | org.springframework.boot 63 | spring-boot-starter-web 64 | 65 | 66 | 67 | 68 | 69 | 70 | org.springframework.boot 71 | spring-boot-starter-actuator 72 | 73 | 74 | 75 | 76 | org.springframework.cloud 77 | spring-cloud-starter-oauth2 78 | 79 | 80 | org.springframework.security.oauth.boot 81 | spring-security-oauth2-autoconfigure 82 | 83 | 84 | org.springframework.security 85 | spring-security-oauth2-client 86 | 87 | 88 | org.springframework.security 89 | spring-security-oauth2-jose 90 | 91 | 92 | org.springframework.boot 93 | spring-boot-starter-oauth2-client 94 | 95 | 96 | org.springframework.cloud 97 | spring-cloud-starter-security 98 | 99 | 100 | 101 | 102 | org.springframework.boot 103 | spring-boot-starter-test 104 | test 105 | 106 | 107 | org.springframework.security 108 | spring-security-test 109 | test 110 | 111 | 112 | org.springframework.cloud 113 | spring-cloud-starter-contract-stub-runner 114 | test 115 | 116 | 117 | 118 | 119 | org.projectlombok 120 | lombok 121 | provided 122 | 123 | 124 | 125 | 126 | org.springframework.cloud 127 | spring-cloud-starter-sleuth 128 | 129 | 130 | net.logstash.logback 131 | logstash-logback-encoder 132 | ${logstash.encoder} 133 | 134 | 135 | 136 | org.springframework.boot 137 | spring-boot-starter-cache 138 | 139 | 140 | com.github.ben-manes.caffeine 141 | caffeine 142 | 143 | 144 | 145 | 146 | io.micrometer 147 | micrometer-registry-prometheus 148 | 149 | 150 | 151 | 152 | de.muenchen.cove.frontend 153 | cove-frontend-frontend 154 | 1.0-SNAPSHOT 155 | runtime 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | src/main/resources 165 | true 166 | 167 | 168 | 169 | 170 | org.springframework.boot 171 | spring-boot-maven-plugin 172 | ${spring.boot.version} 173 | 174 | 175 | 176 | repackage 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /apigateway/runLocal.bat: -------------------------------------------------------------------------------- 1 | mvn clean spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=local" 2 | -------------------------------------------------------------------------------- /apigateway/runLocal.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | mvn clean spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=local" 3 | -------------------------------------------------------------------------------- /apigateway/runLocalNoSecurity.bat: -------------------------------------------------------------------------------- 1 | mvn clean spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=local,no-security" 2 | -------------------------------------------------------------------------------- /apigateway/runLocalNoSecurity.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | mvn clean spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=local,no-security" 3 | -------------------------------------------------------------------------------- /apigateway/src/main/java/de/muenchen/cove/ApiGatewayApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik 3 | * der Landeshauptstadt München, 2020 4 | */ 5 | package de.muenchen.cove; 6 | 7 | import org.springframework.boot.SpringApplication; 8 | import org.springframework.boot.autoconfigure.SpringBootApplication; 9 | 10 | 11 | /** 12 | * To do some base configuration for the non blocking client-server framework 13 | * named Netty via properties use the properties listed in the link down below: 14 | * 15 | * @see https://projectreactor.io/docs/netty/release/api/constant-values.html 16 | * 17 | * As listed below, this above mentioned properties should be set before the application startup: 18 | * 19 | *
    20 | *
  • As command line argument: e.g. -Dreactor.netty.pool.maxConnections=1000. 21 | *
  • As environmental property in Openshift: e.g. with key REACTOR_NETTY_POOL_MAXCONNECTIONS and value 1000. 22 | *
  • As programatically set property before call {@link SpringApplication#run} in {@link ApiGatewayApplication#main}: e.g. System.setProperty("reactor.netty.pool.maxConnections", "1000");. 23 | *
24 | * 25 | * To get more information about Spring Cloud Gateway visit the following link: 26 | * 27 | * @see https://cloud.spring.io/spring-cloud-gateway/reference/html/ 28 | */ 29 | @SpringBootApplication( 30 | scanBasePackages = {"de.muenchen.cove"} 31 | ) 32 | public class ApiGatewayApplication { 33 | 34 | public static void main(String[] args) { 35 | SpringApplication.run(ApiGatewayApplication.class, args); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /apigateway/src/main/java/de/muenchen/cove/configuration/GuiConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik 3 | * der Landeshauptstadt München, 2020 4 | */ 5 | package de.muenchen.cove.configuration; 6 | 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | import org.springframework.core.io.Resource; 13 | import org.springframework.http.MediaType; 14 | import org.springframework.web.reactive.function.server.RouterFunction; 15 | import org.springframework.web.reactive.function.server.ServerResponse; 16 | 17 | import static org.springframework.web.reactive.function.server.RequestPredicates.GET; 18 | import static org.springframework.web.reactive.function.server.RouterFunctions.route; 19 | import static org.springframework.web.reactive.function.server.ServerResponse.ok; 20 | 21 | 22 | /** 23 | * This class supplies the endpoint which provides the gui. 24 | * 25 | * The default path to the gui entry point is "classpath:/static/index.html". 26 | */ 27 | @Configuration 28 | public class GuiConfiguration { 29 | 30 | private static final Logger LOG = LoggerFactory.getLogger(GuiConfiguration.class); 31 | 32 | /** 33 | * A router which returns the index.html as a resource. 34 | * 35 | * @param indexHtml The path to the index.html which serves as the starting point. 36 | * @return the index.html as a resource. 37 | */ 38 | @Bean 39 | public RouterFunction indexRouter(@Value("classpath:/static/index.html") final Resource indexHtml) { 40 | LOG.debug("Location gui entry point: {}", indexHtml); 41 | return route(GET("/"), 42 | request -> ok().contentType(MediaType.TEXT_HTML) 43 | .bodyValue(indexHtml)); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /apigateway/src/main/java/de/muenchen/cove/configuration/MonitoringConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik 3 | * der Landeshauptstadt München, 2020 4 | */ 5 | package de.muenchen.cove.configuration; 6 | 7 | import io.micrometer.core.instrument.MeterRegistry; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | 13 | @Configuration 14 | public class MonitoringConfig { 15 | 16 | @Value("${spring.application.name}") 17 | private String appName; 18 | 19 | @Bean 20 | MeterRegistryCustomizer metricsCommonTags() { 21 | return registry -> registry.config().commonTags("application", appName); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /apigateway/src/main/java/de/muenchen/cove/configuration/NoSecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik 3 | * der Landeshauptstadt München, 2020 4 | */ 5 | package de.muenchen.cove.configuration; 6 | 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.context.annotation.Profile; 10 | import org.springframework.security.config.web.server.ServerHttpSecurity; 11 | import org.springframework.security.web.server.SecurityWebFilterChain; 12 | 13 | 14 | @Configuration 15 | @Profile("no-security") 16 | public class NoSecurityConfiguration { 17 | 18 | @Bean 19 | public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { 20 | return http 21 | .authorizeExchange() 22 | .anyExchange().permitAll() 23 | .and() 24 | .cors() 25 | .and() 26 | .csrf().disable() 27 | .build(); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /apigateway/src/main/java/de/muenchen/cove/configuration/SecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik 3 | * der Landeshauptstadt München, 2020 4 | */ 5 | package de.muenchen.cove.configuration; 6 | 7 | import de.muenchen.cove.filter.CsrfTokenAppendingHelperFilter; 8 | import de.muenchen.cove.util.GatewayUtils; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.context.annotation.Profile; 12 | import org.springframework.http.HttpMethod; 13 | import org.springframework.http.server.reactive.ServerHttpResponse; 14 | import org.springframework.security.config.web.server.ServerHttpSecurity; 15 | import org.springframework.security.web.server.SecurityWebFilterChain; 16 | import org.springframework.security.web.server.csrf.CookieServerCsrfTokenRepository; 17 | import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; 18 | 19 | 20 | @Configuration 21 | @Profile("!no-security") 22 | public class SecurityConfiguration { 23 | 24 | private static final String LOGOUT_URL = "/logout"; 25 | 26 | private static final String LOGOUT_SUCCESS_URL = "/loggedout.html"; 27 | 28 | @Bean 29 | public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { 30 | http 31 | .logout() 32 | .logoutSuccessHandler(GatewayUtils.createLogoutSuccessHandler(LOGOUT_SUCCESS_URL)) 33 | .logoutUrl(LOGOUT_URL) 34 | .requiresLogout(ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, LOGOUT_URL)) 35 | .and() 36 | .authorizeExchange() 37 | // permitAll 38 | .pathMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() 39 | .pathMatchers(LOGOUT_SUCCESS_URL).permitAll() 40 | .pathMatchers("/api/*/info", "/actuator/health", "/actuator/info", "/actuator/metrics").permitAll() 41 | // only authenticated 42 | .anyExchange().authenticated() 43 | .and() 44 | /** 45 | * The necessary subscription for csrf token attachment to {@link ServerHttpResponse} 46 | * is done in class {@link CsrfTokenAppendingHelperFilter}. 47 | */ 48 | .csrf().csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse()) 49 | .and() 50 | .cors() 51 | .and() 52 | .oauth2Login() 53 | .and() 54 | .oauth2Client(); 55 | return http.build(); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /apigateway/src/main/java/de/muenchen/cove/controller/PingController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik 3 | * der Landeshauptstadt München, 2020 4 | */ 5 | package de.muenchen.cove.controller; 6 | 7 | import lombok.EqualsAndHashCode; 8 | import lombok.ToString; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.hateoas.Link; 12 | import org.springframework.hateoas.RepresentationModel; 13 | import org.springframework.http.HttpStatus; 14 | import org.springframework.web.bind.annotation.GetMapping; 15 | import org.springframework.web.bind.annotation.RestController; 16 | import reactor.core.publisher.Mono; 17 | 18 | import java.util.List; 19 | 20 | import static org.springframework.hateoas.server.reactive.WebFluxLinkBuilder.linkTo; 21 | import static org.springframework.hateoas.server.reactive.WebFluxLinkBuilder.methodOn; 22 | 23 | 24 | /** 25 | * Endpoint for pinging with an authorized request 26 | * to check for available or expired security sessions. 27 | */ 28 | @RestController 29 | public class PingController { 30 | 31 | private static final Logger LOG = LoggerFactory.getLogger(PingController.class); 32 | 33 | private static final String PING_PATH = "/api"; 34 | 35 | /** 36 | * The hateoas response for the ping controller. 37 | */ 38 | @EqualsAndHashCode(callSuper = true) 39 | @ToString(callSuper = true) 40 | private static class HateoasResponse extends RepresentationModel { 41 | 42 | public HateoasResponse(Link initialLink) { 43 | super(initialLink); 44 | } 45 | 46 | public HateoasResponse(List initialLinks) { 47 | super(initialLinks); 48 | } 49 | 50 | } 51 | 52 | /** 53 | * Endpoint returns {@link HttpStatus#OK} with self 54 | * link as a hateoas response. 55 | * 56 | * @return a response with {@link HttpStatus#OK}. 57 | */ 58 | @GetMapping(value = PING_PATH) 59 | public Mono ping() { 60 | LOG.debug("GET request on endpoint \"{}\".", PING_PATH); 61 | return linkTo(methodOn(PingController.class).ping()).withSelfRel() 62 | .toMono() 63 | .map(HateoasResponse::new); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /apigateway/src/main/java/de/muenchen/cove/exception/ParameterPollutionException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik 3 | * der Landeshauptstadt München, 2020 4 | */ 5 | package de.muenchen.cove.exception; 6 | 7 | import de.muenchen.cove.filter.GlobalRequestParameterPollutionFilter; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.NoArgsConstructor; 10 | import lombok.ToString; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.web.bind.annotation.ResponseStatus; 13 | 14 | 15 | /** 16 | * Used in {@link GlobalRequestParameterPollutionFilter} to signal a possible parameter pollution attack. 17 | */ 18 | @ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "parameter pollution") 19 | @NoArgsConstructor 20 | @EqualsAndHashCode(callSuper = true) 21 | @ToString(callSuper = true) 22 | public class ParameterPollutionException extends RuntimeException { 23 | // default Ctor 24 | } 25 | -------------------------------------------------------------------------------- /apigateway/src/main/java/de/muenchen/cove/filter/CsrfTokenAppendingHelperFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik 3 | * der Landeshauptstadt München, 2020 4 | */ 5 | package de.muenchen.cove.filter; 6 | 7 | import de.muenchen.cove.configuration.SecurityConfiguration; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.http.server.reactive.ServerHttpResponse; 11 | import org.springframework.security.web.server.csrf.CookieServerCsrfTokenRepository; 12 | import org.springframework.security.web.server.csrf.CsrfToken; 13 | import org.springframework.security.web.server.csrf.CsrfWebFilter; 14 | import org.springframework.stereotype.Component; 15 | import org.springframework.web.server.ServerWebExchange; 16 | import org.springframework.web.server.WebFilter; 17 | import org.springframework.web.server.WebFilterChain; 18 | import reactor.core.publisher.Mono; 19 | 20 | 21 | /** 22 | * This class subscribes the {@link ServerWebExchange} for csrf token attachment 23 | * within the classes {@link CookieServerCsrfTokenRepository} and {@link CsrfWebFilter}. 24 | * The csrf configuration done only in {@link SecurityConfiguration#springSecurityFilterChain} is 25 | * not sufficient for csrf token attachment to a {@link ServerHttpResponse}. 26 | */ 27 | @Component 28 | public class CsrfTokenAppendingHelperFilter implements WebFilter { 29 | 30 | private static final Logger LOG = LoggerFactory.getLogger(CsrfTokenAppendingHelperFilter.class); 31 | 32 | public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { 33 | LOG.debug("Trigger to append CSRF token to response"); 34 | Mono csrfToken = exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty()); 35 | return csrfToken.doOnSuccess(token -> { 36 | // do nothing -> CSRF-Token is added as cookie in class CookieServerCsrfTokenRepository#saveToken 37 | }).then(chain.filter(exchange)); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /apigateway/src/main/java/de/muenchen/cove/filter/CustomTokenRelayGatewayFilterFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik 3 | * der Landeshauptstadt München, 2020 4 | */ 5 | package de.muenchen.cove.filter; 6 | 7 | import java.time.Duration; 8 | 9 | import lombok.EqualsAndHashCode; 10 | import lombok.ToString; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.cloud.gateway.filter.GatewayFilter; 15 | import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; 16 | import org.springframework.cloud.security.oauth2.gateway.TokenRelayGatewayFilterFactory; 17 | import org.springframework.context.annotation.Profile; 18 | import org.springframework.security.core.Authentication; 19 | import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest; 20 | import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; 21 | import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager; 22 | import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider; 23 | import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder; 24 | import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; 25 | import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; 26 | import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager; 27 | import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction; 28 | import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository; 29 | import org.springframework.security.oauth2.core.OAuth2AccessToken; 30 | import org.springframework.stereotype.Component; 31 | import org.springframework.web.server.ServerWebExchange; 32 | 33 | import reactor.core.publisher.Mono; 34 | 35 | 36 | /** 37 | * This class provides a {@link TokenRelayGatewayFilterFactory} with the 38 | * functionality to refresh an expired access token with the refresh token. 39 | * 40 | * The functionality for access token refreshment is inspired 41 | * by {@link ServerOAuth2AuthorizedClientExchangeFilterFunction}. 42 | * As soon as the following spring issue is solved this class isn't required any more. 43 | * 44 | * @see https://github.com/spring-cloud/spring-cloud-security/issues/175 45 | * 46 | * This filter factory is appended in properties file to a route filter chain with property "spring.cloud.gateway.routes.filters" and value "CustomTokenRelay=". 47 | */ 48 | @Component 49 | @Profile("!no-security") 50 | @EqualsAndHashCode(callSuper = true) 51 | @ToString(callSuper = true) 52 | public class CustomTokenRelayGatewayFilterFactory extends AbstractGatewayFilterFactory { 53 | 54 | private static final Logger LOG = LoggerFactory.getLogger(CustomTokenRelayGatewayFilterFactory.class); 55 | 56 | private static final Duration ACCESS_TOKEN_EXPIRATION_SKEW = Duration.ofSeconds(3); 57 | 58 | private final ReactiveOAuth2AuthorizedClientManager authorizedClientManager; 59 | 60 | @Autowired 61 | public CustomTokenRelayGatewayFilterFactory(ServerOAuth2AuthorizedClientRepository authorizedClientRepository, 62 | ReactiveClientRegistrationRepository clientRegistrationRepository) { 63 | super(Object.class); 64 | this.authorizedClientManager = createDefaultAuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository); 65 | } 66 | 67 | private static ReactiveOAuth2AuthorizedClientManager createDefaultAuthorizedClientManager( 68 | ReactiveClientRegistrationRepository clientRegistrationRepository, 69 | ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { 70 | 71 | final ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = 72 | ReactiveOAuth2AuthorizedClientProviderBuilder.builder() 73 | .authorizationCode() 74 | .refreshToken(configurer -> configurer.clockSkew(ACCESS_TOKEN_EXPIRATION_SKEW)) 75 | .clientCredentials(configurer -> configurer.clockSkew(ACCESS_TOKEN_EXPIRATION_SKEW)) 76 | .password(configurer -> configurer.clockSkew(ACCESS_TOKEN_EXPIRATION_SKEW)) 77 | .build(); 78 | final DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager( 79 | clientRegistrationRepository, 80 | authorizedClientRepository); 81 | authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); 82 | 83 | return authorizedClientManager; 84 | } 85 | 86 | public GatewayFilter apply() { 87 | return apply((Object) null); 88 | } 89 | 90 | @Override 91 | public GatewayFilter apply(Object config) { 92 | return (exchange, chain) -> exchange.getPrincipal() 93 | .filter(principal -> principal instanceof OAuth2AuthenticationToken) 94 | .cast(OAuth2AuthenticationToken.class) 95 | .flatMap(this::authorizeClient) 96 | .map(OAuth2AuthorizedClient::getAccessToken) 97 | .map(token -> withBearerAuth(exchange, token)) 98 | .defaultIfEmpty(exchange) 99 | .flatMap(chain::filter); 100 | } 101 | 102 | private ServerWebExchange withBearerAuth(ServerWebExchange exchange, OAuth2AccessToken accessToken) { 103 | LOG.debug("set bearer token in header"); 104 | return exchange.mutate().request(r -> r.headers(headers -> headers.setBearerAuth(accessToken.getTokenValue()))).build(); 105 | } 106 | 107 | private Mono authorizeClient(OAuth2AuthenticationToken oAuth2AuthenticationToken) { 108 | final String clientRegistrationId = oAuth2AuthenticationToken.getAuthorizedClientRegistrationId(); 109 | LOG.debug("authorizeClient with client reg id: {}", clientRegistrationId); 110 | return Mono.defer(() -> authorizedClientManager.authorize(createOAuth2AuthorizeRequest(clientRegistrationId, oAuth2AuthenticationToken))); 111 | } 112 | 113 | private OAuth2AuthorizeRequest createOAuth2AuthorizeRequest(String clientRegistrationId, Authentication principal) { 114 | return OAuth2AuthorizeRequest.withClientRegistrationId(clientRegistrationId).principal(principal).build(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /apigateway/src/main/java/de/muenchen/cove/filter/DistributedTracingFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik 3 | * der Landeshauptstadt München, 2020 4 | */ 5 | package de.muenchen.cove.filter; 6 | 7 | import brave.Tracer; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.cloud.gateway.filter.GatewayFilterChain; 10 | import org.springframework.cloud.gateway.filter.GlobalFilter; 11 | import org.springframework.http.server.reactive.ServerHttpResponseDecorator; 12 | import org.springframework.stereotype.Component; 13 | import org.springframework.util.MultiValueMap; 14 | import org.springframework.web.server.ServerWebExchange; 15 | import reactor.core.publisher.Mono; 16 | 17 | 18 | /** 19 | * This class adds the zipkin headers "X-B3-SpanId" and "X-B3-TraceId" 20 | * to each route response. 21 | */ 22 | @Component 23 | public class DistributedTracingFilter implements GlobalFilter { 24 | 25 | private static final String SPAN_ID_HEADER = "X-B3-SpanId"; 26 | 27 | private static final String TRACE_ID_HEADER = "X-B3-TraceId"; 28 | 29 | @Autowired 30 | private Tracer tracer; 31 | 32 | /** 33 | * This method adds the zipkin headers "X-B3-SpanId" and "X-B3-TraceId" 34 | * to each response in {@link ServerWebExchange}. 35 | * 36 | * @param exchange the current server exchange without zipkin headers 37 | * @param chain provides a way to delegate to the next filter 38 | * @return {@code Mono} to indicate when request processing for adding zipkin headers is complete 39 | */ 40 | @Override 41 | public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { 42 | return chain.filter(exchange).then(Mono.fromRunnable(() -> { 43 | ServerHttpResponseDecorator decorator = new ServerHttpResponseDecorator(exchange.getResponse()); 44 | MultiValueMap headers = decorator.getHeaders(); 45 | headers.set(SPAN_ID_HEADER, tracer.currentSpan().context().spanIdString()); 46 | headers.set(TRACE_ID_HEADER, tracer.currentSpan().context().traceIdString()); 47 | })); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /apigateway/src/main/java/de/muenchen/cove/filter/GlobalAuthenticationErrorFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik 3 | * der Landeshauptstadt München, 2020 4 | */ 5 | package de.muenchen.cove.filter; 6 | 7 | import de.muenchen.cove.util.GatewayUtils; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.cloud.gateway.filter.GatewayFilterChain; 11 | import org.springframework.cloud.gateway.filter.GlobalFilter; 12 | import org.springframework.core.Ordered; 13 | import org.springframework.http.HttpHeaders; 14 | import org.springframework.http.HttpStatus; 15 | import org.springframework.stereotype.Component; 16 | import org.springframework.web.server.ServerWebExchange; 17 | import reactor.core.publisher.Mono; 18 | 19 | 20 | /** 21 | * This {@link GlobalFilter} replaces the body by a generic authentication error body, 22 | * when a server responses with a {@link HttpStatus#UNAUTHORIZED}. 23 | *

24 | * The header {@link HttpHeaders#WWW_AUTHENTICATE} containing the access token is removed 25 | * by the property 'RemoveResponseHeader' in the corresponding route within 'application.yml'. 26 | */ 27 | @Component 28 | public class GlobalAuthenticationErrorFilter implements GlobalFilter, Ordered { 29 | 30 | private static final Logger LOG = LoggerFactory.getLogger(GlobalAuthenticationErrorFilter.class); 31 | 32 | private static final String GENERIC_AUTHENTICATION_ERROR = "{ \"status\":401, \"error\":\"Authentication Error\" }"; 33 | 34 | @Override 35 | public int getOrder() { 36 | return GatewayUtils.ORDER_GLOBAL_FILTER; 37 | } 38 | 39 | @Override 40 | public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { 41 | LOG.debug("Check for authentication errors"); 42 | return GatewayUtils.globalResponseFilterLogic(exchange, chain, HttpStatus.UNAUTHORIZED, GENERIC_AUTHENTICATION_ERROR, LOG); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /apigateway/src/main/java/de/muenchen/cove/filter/GlobalBackendErrorFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik 3 | * der Landeshauptstadt München, 2020 4 | */ 5 | package de.muenchen.cove.filter; 6 | 7 | import de.muenchen.cove.util.GatewayUtils; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.cloud.gateway.filter.GatewayFilterChain; 11 | import org.springframework.cloud.gateway.filter.GlobalFilter; 12 | import org.springframework.core.Ordered; 13 | import org.springframework.http.HttpStatus; 14 | import org.springframework.stereotype.Component; 15 | import org.springframework.web.server.ServerWebExchange; 16 | import reactor.core.publisher.Mono; 17 | 18 | 19 | /** 20 | * This {@link GlobalFilter} replaces the body by a generic error body, when a server responses 21 | * with a {@link HttpStatus#INTERNAL_SERVER_ERROR}. 22 | */ 23 | @Component 24 | public class GlobalBackendErrorFilter implements GlobalFilter, Ordered { 25 | 26 | private static final Logger LOG = LoggerFactory.getLogger(GlobalBackendErrorFilter.class); 27 | 28 | private static final String GENERIC_ERROR = "{ \"status\":500, \"error\":\"Internal Server Error\" }"; 29 | 30 | @Override 31 | public int getOrder() { 32 | return GatewayUtils.ORDER_GLOBAL_FILTER; 33 | } 34 | 35 | @Override 36 | public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { 37 | LOG.debug("Check for backend errors"); 38 | return GatewayUtils.globalResponseFilterLogic(exchange, chain, HttpStatus.INTERNAL_SERVER_ERROR, GENERIC_ERROR, LOG); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /apigateway/src/main/java/de/muenchen/cove/filter/GlobalRequestParameterPollutionFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik 3 | * der Landeshauptstadt München, 2020 4 | */ 5 | package de.muenchen.cove.filter; 6 | 7 | import de.muenchen.cove.exception.ParameterPollutionException; 8 | import de.muenchen.cove.util.GatewayUtils; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.cloud.gateway.filter.GatewayFilterChain; 12 | import org.springframework.cloud.gateway.filter.GlobalFilter; 13 | import org.springframework.core.Ordered; 14 | import org.springframework.http.HttpRequest; 15 | import org.springframework.http.HttpStatus; 16 | import org.springframework.http.server.reactive.ServerHttpRequest; 17 | import org.springframework.stereotype.Component; 18 | import org.springframework.util.CollectionUtils; 19 | import org.springframework.util.MultiValueMap; 20 | import org.springframework.web.server.ServerWebExchange; 21 | import reactor.core.publisher.Mono; 22 | 23 | import java.util.List; 24 | import java.util.Map; 25 | 26 | 27 | /** 28 | * This {@link GlobalFilter} is used to detect and to fend off a parameter pollution attack. 29 | * 30 | * Within a {@link HttpRequest} each request parameter should only exist once. 31 | * This check is necessary to avoid e.g. SQL injection split over multiple request parameters with the same name. 32 | */ 33 | @Component 34 | public class GlobalRequestParameterPollutionFilter implements GlobalFilter, Ordered { 35 | 36 | private static final Logger LOG = LoggerFactory.getLogger(GlobalRequestParameterPollutionFilter.class); 37 | 38 | @Override 39 | public int getOrder() { 40 | return GatewayUtils.ORDER_GLOBAL_FILTER; 41 | } 42 | 43 | /** 44 | * See {@link GlobalFilter#filter(ServerWebExchange, GatewayFilterChain)} 45 | * 46 | * @throws ParameterPollutionException is throw when a request parameter exists multiple times. 47 | * The exception represents a http response with status {@link HttpStatus#BAD_REQUEST}. 48 | */ 49 | @Override 50 | public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) throws ParameterPollutionException { 51 | LOG.debug("Check for parameter pollution attack."); 52 | ServerHttpRequest request = exchange.getRequest(); 53 | if (!CollectionUtils.isEmpty(request.getQueryParams())) { 54 | MultiValueMap parameterMap = request.getQueryParams(); 55 | for(Map.Entry> entry : parameterMap.entrySet()) { 56 | String key = entry.getKey(); 57 | List value = entry.getValue(); 58 | if (!CollectionUtils.isEmpty(value) && value.size() > 1) { 59 | LOG.warn("Possible parameter pollution attack detected: Parameter \"{}\" detected more than once in the request!", key); 60 | throw new ParameterPollutionException(); 61 | } 62 | } 63 | } 64 | return chain.filter(exchange); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /apigateway/src/main/java/de/muenchen/cove/util/GatewayUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik 3 | * der Landeshauptstadt München, 2020 4 | */ 5 | package de.muenchen.cove.util; 6 | 7 | import lombok.AccessLevel; 8 | import lombok.NoArgsConstructor; 9 | import org.apache.commons.codec.binary.StringUtils; 10 | import org.reactivestreams.Publisher; 11 | import org.slf4j.Logger; 12 | import org.springframework.cloud.gateway.filter.GatewayFilterChain; 13 | import org.springframework.cloud.gateway.filter.GlobalFilter; 14 | import org.springframework.core.io.buffer.DataBuffer; 15 | import org.springframework.core.io.buffer.DataBufferFactory; 16 | import org.springframework.http.HttpStatus; 17 | import org.springframework.http.MediaType; 18 | import org.springframework.http.server.reactive.ServerHttpResponse; 19 | import org.springframework.http.server.reactive.ServerHttpResponseDecorator; 20 | import org.springframework.security.web.server.SecurityWebFilterChain; 21 | import org.springframework.security.web.server.authentication.logout.RedirectServerLogoutSuccessHandler; 22 | import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler; 23 | import org.springframework.web.server.ServerWebExchange; 24 | import reactor.core.publisher.Flux; 25 | import reactor.core.publisher.Mono; 26 | 27 | import java.net.URI; 28 | 29 | 30 | /** 31 | * Utility methods and constants which are used in multiple 32 | * locations throughout the application. 33 | */ 34 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 35 | public class GatewayUtils { 36 | 37 | public static final int ORDER_GLOBAL_EXCEPTION_HANDLER = -2; 38 | 39 | public static final int ORDER_GLOBAL_FILTER = -3; 40 | 41 | /** 42 | * The method is used in {@link GlobalFilter}s to add the response body given in the 43 | * parameter when the {@link HttpStatus} given in the parameter is met. 44 | * 45 | * If the {@link HttpStatus} given in the parameter is the same as in {@link ServerHttpResponse} 46 | * the body will be added otherwise the body received from upstream stays the same. 47 | * 48 | * @param exchange Contains the response. 49 | * @param chain The filter chain for delegation to the next filter. 50 | * @param httpStatus Status of the http {@link ServerHttpResponse}. 51 | * @param responseBody The UTF8 conform message to add into the body of the {@link ServerHttpResponse}. 52 | * @param logger For logging purposes. 53 | * @return An empty mono. The results are processed within the {@link GatewayFilterChain}. 54 | */ 55 | public static Mono globalResponseFilterLogic(final ServerWebExchange exchange, 56 | final GatewayFilterChain chain, 57 | final HttpStatus httpStatus, 58 | final String responseBody, 59 | final Logger logger) { 60 | final ServerHttpResponse response = exchange.getResponse(); 61 | final DataBufferFactory dataBufferFactory = response.bufferFactory(); 62 | final DataBuffer newDataBuffer = dataBufferFactory.wrap(StringUtils.getBytesUtf8(responseBody)); 63 | 64 | final ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(response) { 65 | 66 | /** 67 | * This overridden method adds the response body given in the parameter of 68 | * the surrounding method when the http status given in the parameter of 69 | * the surrounding method is met otherwise the already appended body will be used. 70 | * 71 | * @param body The body received by the upstream response. 72 | * @return Either the body received by the upstream response or 73 | * the body given by the parameter. 74 | */ 75 | @Override 76 | public Mono writeWith(Publisher body) { 77 | HttpStatus responseHttpStatus = getDelegate().getStatusCode(); 78 | if (body instanceof Flux && responseHttpStatus.equals(httpStatus)) { 79 | logger.debug("Response from upstream {} get new response body: {}", httpStatus, responseBody); 80 | getDelegate().getHeaders().setContentLength(newDataBuffer.readableByteCount()); 81 | getDelegate().getHeaders().setContentType(MediaType.APPLICATION_JSON); 82 | Flux flux = (Flux) body; 83 | return super.writeWith(flux.buffer().map( 84 | // replace old body represented by dataBuffer by the new one 85 | dataBuffer -> newDataBuffer 86 | )); 87 | } 88 | return super.writeWith(body); 89 | } 90 | 91 | }; 92 | 93 | final ServerWebExchange swe = exchange.mutate().response(decoratedResponse).build(); 94 | return chain.filter(swe); 95 | } 96 | 97 | /** 98 | * This method creates the {@link ServerLogoutSuccessHandler} for handling a successful logout. 99 | * The usage is necessary in {@link SecurityWebFilterChain}. 100 | * 101 | * @param uri to forward after an successful logout. 102 | * @return The handler for forwarding after an succesful logout. 103 | */ 104 | public static ServerLogoutSuccessHandler createLogoutSuccessHandler(final String uri) { 105 | final RedirectServerLogoutSuccessHandler successHandler = new RedirectServerLogoutSuccessHandler(); 106 | successHandler.setLogoutSuccessUrl(URI.create(uri)); 107 | return successHandler; 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /apigateway/src/main/resources/application-dev.yml: -------------------------------------------------------------------------------- 1 | server.port: 8080 2 | 3 | spring: 4 | 5 | # Define the keycloak consolidation realm here 6 | realm: 7 | 8 | # spring cloud gateway config 9 | cloud: 10 | gateway: 11 | actuator: 12 | verbose: 13 | enabled: false 14 | globalcors: 15 | corsConfigurations: 16 | # The cors configuration to allow frontend developers cross origin request via this api gateway 17 | '[/**]': 18 | allowedOrigins: 19 | - "http://localhost:8081" 20 | - "http://127.0.0.1:8081" 21 | allowedMethods: "*" 22 | http://cove-backend:8080/ allowedHeaders: "*" 23 | allowCredentials: true 24 | maxAge: 3600 25 | routes: 26 | - id: userinfo 27 | uri: 28 | predicates: 29 | - Path=/api/userinfo/** 30 | filters: 31 | - RewritePath=/api/userinfo/(?.*), /$\{urlsegments} 32 | - id: backend 33 | uri: 34 | predicates: 35 | - Path=/api/cove-backend-service/** 36 | filters: 37 | - RewritePath=/api/cove-backend-service/(?.*), /$\{urlsegments} 38 | - RemoveResponseHeader=WWW-Authenticate 39 | 40 | # security config 41 | security: 42 | oauth2: 43 | client: 44 | provider: 45 | keycloak: 46 | issuer-uri: 47 | registration: 48 | keycloak: 49 | client-id: 50 | client-secret: 51 | http: 52 | log-request-details: on 53 | -------------------------------------------------------------------------------- /apigateway/src/main/resources/application-kon.yml: -------------------------------------------------------------------------------- 1 | server.port: 8080 2 | 3 | spring: 4 | 5 | # Define the keycloak consolidation realm here 6 | realm: 7 | 8 | # spring cloud gateway config 9 | cloud: 10 | gateway: 11 | actuator: 12 | verbose: 13 | enabled: false 14 | globalcors: 15 | corsConfigurations: 16 | # The cors configuration to allow frontend developers cross origin request via this api gateway 17 | '[/**]': 18 | allowedOrigins: 19 | - "http://localhost:8081" 20 | - "http://127.0.0.1:8081" 21 | allowedMethods: "*" 22 | allowedHeaders: "*" 23 | allowCredentials: true 24 | maxAge: 3600 25 | routes: 26 | - id: userinfo 27 | uri: 28 | predicates: 29 | - Path=/api/userinfo/** 30 | filters: 31 | - RewritePath=/api/userinfo/(?.*), /$\{urlsegments} 32 | - id: backend 33 | uri: 34 | predicates: 35 | - Path=/api/cove-backend-service/** 36 | filters: 37 | - RewritePath=/api/cove-backend-service/(?.*), /$\{urlsegments} 38 | - RemoveResponseHeader=WWW-Authenticate 39 | 40 | # security config 41 | security: 42 | oauth2: 43 | client: 44 | provider: 45 | keycloak: 46 | issuer-uri: 47 | registration: 48 | keycloak: 49 | client-id: 50 | client-secret: 51 | http: 52 | log-request-details: on 53 | -------------------------------------------------------------------------------- /apigateway/src/main/resources/application-local.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | 3 | # Define the local keycloak realm here 4 | realm: 5 | 6 | # spring cloud gateway config 7 | cloud: 8 | gateway: 9 | actuator: 10 | verbose: 11 | enabled: false 12 | globalcors: 13 | corsConfigurations: 14 | # The cors configuration to allow frontend developers cross origin request via this api gateway 15 | '[/**]': 16 | allowedOrigins: 17 | - "http://localhost:8081" 18 | - "http://127.0.0.1:8081" 19 | allowedMethods: "*" 20 | allowedHeaders: "*" 21 | allowCredentials: true 22 | maxAge: 3600 23 | routes: 24 | - id: userinfo 25 | uri: 26 | predicates: 27 | - Path=/api/userinfo/** 28 | filters: 29 | - RewritePath=/api/userinfo/(?.*), /$\{urlsegments} 30 | - id: backend 31 | uri: 32 | predicates: 33 | - Path=/api/cove-backend-service/** 34 | filters: 35 | - RewritePath=/api/cove-backend-service/(?.*), /$\{urlsegments} 36 | - RemoveResponseHeader=WWW-Authenticate 37 | - AddResponseHeader=Access-Control-Expose-Headers, ETag 38 | 39 | # security config 40 | security: 41 | oauth2: 42 | client: 43 | provider: 44 | keycloak: 45 | issuer-uri: 46 | registration: 47 | keycloak: 48 | client-id: 49 | client-secret: 50 | 51 | http: 52 | log-request-details: on 53 | -------------------------------------------------------------------------------- /apigateway/src/main/resources/application-no-security.yml: -------------------------------------------------------------------------------- 1 | # NO-SECURITY CONFIGURATION 2 | spring: 3 | h2.console.enabled: true 4 | cloud: 5 | gateway: 6 | default-filters: 7 | - RemoveResponseHeader=Expires 8 | - RemoveRequestHeader=cookie 9 | -------------------------------------------------------------------------------- /apigateway/src/main/resources/application-prod.yml: -------------------------------------------------------------------------------- 1 | server.port: 8080 2 | 3 | spring: 4 | 5 | # Define the keycloak productive realm here 6 | realm: 7 | 8 | # spring cloud gateway config 9 | cloud: 10 | gateway: 11 | actuator: 12 | verbose: 13 | enabled: false 14 | globalcors: 15 | corsConfigurations: 16 | # The cors configuration to allow frontend developers cross origin request via this api gateway 17 | '[/**]': 18 | allowedOrigins: 19 | - "http://localhost:8081" 20 | - "http://127.0.0.1:8081" 21 | allowedMethods: "*" 22 | allowedHeaders: "*" 23 | allowCredentials: true 24 | maxAge: 3600 25 | routes: 26 | - id: userinfo 27 | uri: 28 | predicates: 29 | - Path=/api/userinfo/** 30 | filters: 31 | - RewritePath=/api/userinfo/(?.*), /$\{urlsegments} 32 | - id: backend 33 | uri: 34 | predicates: 35 | - Path=/api/cove-backend-service/** 36 | filters: 37 | - RewritePath=/api/cove-backend-service/(?.*), /$\{urlsegments} 38 | - RemoveResponseHeader=WWW-Authenticate 39 | default-filters: 40 | - RemoveResponseHeader=Expires 41 | - RemoveRequestHeader=cookie 42 | - RemoveRequestHeader=x-xsrf-token 43 | - CustomTokenRelay= 44 | 45 | # security config 46 | security: 47 | oauth2: 48 | client: 49 | provider: 50 | keycloak: 51 | issuer-uri: 52 | registration: 53 | keycloak: 54 | client-id: 55 | client-secret: 56 | 57 | http: 58 | log-request-details: false -------------------------------------------------------------------------------- /apigateway/src/main/resources/application-test.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | 3 | # Define the keycloak test realm here 4 | realm: 5 | 6 | # spring cloud gateway config 7 | cloud: 8 | gateway: 9 | actuator: 10 | verbose: 11 | enabled: false 12 | globalcors: 13 | corsConfigurations: 14 | # The cors configuration to allow frontend developers cross origin request via this api gateway 15 | '[/**]': 16 | allowedOrigins: 17 | - "http://localhost:8081" 18 | - "http://127.0.0.1:8081" 19 | allowedMethods: "*" 20 | allowedHeaders: "*" 21 | allowCredentials: true 22 | maxAge: 3600 23 | routes: 24 | - id: userinfo 25 | uri: 26 | predicates: 27 | - Path=/api/userinfo/** 28 | filters: 29 | - RewritePath=/api/userinfo/(?.*), /$\{urlsegments} 30 | - id: backend 31 | uri: 32 | predicates: 33 | - Path=/api/cove-backend-service/** 34 | filters: 35 | - RewritePath=/api/cove-backend-service/(?.*), /$\{urlsegments} 36 | - RemoveResponseHeader=WWW-Authenticate 37 | default-filters: 38 | - RemoveResponseHeader=Expires 39 | - RemoveRequestHeader=cookie 40 | - RemoveRequestHeader=x-xsrf-token 41 | - CustomTokenRelay= 42 | 43 | # security config 44 | security: 45 | oauth2: 46 | client: 47 | provider: 48 | keycloak: 49 | issuer-uri: 50 | registration: 51 | keycloak: 52 | client-id: 53 | client-secret: 54 | 55 | http: 56 | log-request-details: on -------------------------------------------------------------------------------- /apigateway/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application.name: cove-frontend-apigateway 3 | banner.location: banner.txt 4 | main: 5 | web-application-type: reactive 6 | jackson: 7 | serialization: 8 | indent-output: on 9 | session: 10 | timeout: 36000 # in seconds; Goal: same lifetime like SSO Session (e.g. 10 hours) 11 | cloud: 12 | gateway: 13 | default-filters: 14 | - RemoveResponseHeader=Expires 15 | - RemoveRequestHeader=cookie 16 | - RemoveRequestHeader=x-xsrf-token 17 | - RemoveRequestHeader=origin 18 | - CustomTokenRelay= 19 | 20 | server: 21 | port: 8082 22 | error: 23 | include-exception: false 24 | include-stacktrace: never 25 | whitelabel: 26 | enabled: false 27 | 28 | 29 | management: 30 | endpoints: 31 | enabled-by-default: false 32 | web: 33 | cors: 34 | allowedOrigins: 35 | - "http://localhost:8081" 36 | - "http://127.0.0.1:8081" 37 | allowedMethods: "*" 38 | allowedHeaders: "*" 39 | allowCredentials: on 40 | maxAge: 3600 41 | exposure: 42 | include: 'health, info, prometheus' 43 | path-mapping: 44 | prometheus: 'metrics' 45 | endpoint: 46 | health: 47 | enabled: on 48 | info: 49 | enabled: on 50 | prometheus: 51 | enabled: true 52 | info.application.name: ${spring.application.name} 53 | info.application.version: v1.0-SNAPSHOT 54 | -------------------------------------------------------------------------------- /apigateway/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------------------------------------------------------------------------- 2 | ____ _ _ _ _ 3 | | _ \ | | | | /\ | | | | 4 | | |_) | __ _ _ __ _ __ __ _ | | __ _ _ __| | __ _ ______ / \ _ __ ___ | |__ ___ | |_ _ _ _ __ ___ 5 | | _ < / _` | | '__| | '__| / _` | | |/ / | | | | / _` | / _` | |______| / /\ \ | '__| / __| | '_ \ / _ \ | __| | | | | | '_ \ / _ \ 6 | | |_) | | (_| | | | | | | (_| | | < | |_| | | (_| | | (_| | / ____ \ | | | (__ | | | | | __/ | |_ | |_| | | |_) | | __/ 7 | |____/ \__,_| |_| |_| \__,_| |_|\_\ \__,_| \__,_| \__,_| /_/ \_\ |_| \___| |_| |_| \___| \__| \__, | | .__/ \___| 8 | __/ | | | 9 | |___/ |_| by CCSE 10 | Application Name : ${spring.application.name} 11 | Spring Boot Version : ${spring-boot.formatted-version} 12 | --------------------------------------------------------------------------------------------------------------------------------------------------- 13 | -------------------------------------------------------------------------------- /apigateway/src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | %date{yyyy.MM.dd HH:mm:ss.SSS} | [${springAppName}, TraceId: %X{X-B3-TraceId:-}, SpanId: %X{X-B3-SpanId:-}, Span-Export: %X{X-Span-Export:-}] | %level | [%thread] | %logger{0} | [%file : %line] - %msg%n 18 | 19 | 20 | 21 | 22 | 23 | 26 | 27 | 28 | 29 | 32 | 33 | 34 | 35 | 38 | 39 | 40 | 41 | 44 | 45 | 46 | 47 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | { 72 | "timestamp" : "%date{yyyy-MM-dd'T'HH:mm:ss.SSS}", 73 | "appName" : "${springAppName}", 74 | "X-B3-TraceId" : "%mdc{X-B3-TraceId}", 75 | "X-B3-SpanId" : "%mdc{X-B3-SpanId}", 76 | "X-Span-Export" : "%mdc{X-Span-Export}", 77 | "thread" : "%thread", 78 | "level" : "%level", 79 | "logger": "%logger", 80 | "location" : { 81 | "fileName" : "%file", 82 | "line" : "%line" 83 | }, 84 | "message": "%message" 85 | } 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 96 | 97 | 98 | 99 | 102 | 103 | 104 | 105 | 108 | 109 | 110 | 111 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | { 135 | "timestamp" : "%date{yyyy-MM-dd'T'HH:mm:ss.SSS}", 136 | "appName" : "${springAppName}", 137 | "X-B3-TraceId" : "%mdc{X-B3-TraceId}", 138 | "X-B3-SpanId" : "%mdc{X-B3-SpanId}", 139 | "X-Span-Export" : "%mdc{X-Span-Export}", 140 | "thread" : "%thread", 141 | "level" : "%level", 142 | "logger": "%logger", 143 | "location" : { 144 | "fileName" : "%file", 145 | "line" : "%line" 146 | }, 147 | "message": "%message" 148 | } 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 159 | 160 | 161 | 162 | 165 | 166 | 167 | 168 | 171 | 172 | 173 | 174 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | -------------------------------------------------------------------------------- /apigateway/src/test/java/de/muenchen/cove/TestConstants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik 3 | * der Landeshauptstadt München, 2020 4 | */ 5 | package de.muenchen.cove; 6 | 7 | import lombok.AccessLevel; 8 | import lombok.NoArgsConstructor; 9 | 10 | 11 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 12 | public final class TestConstants { 13 | 14 | public static final String SPRING_TEST_PROFILE = "test"; 15 | 16 | public static final int WIREMOCK_PORT_NUMBER = 44444; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /apigateway/src/test/java/de/muenchen/cove/controller/PingControllerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik 3 | * der Landeshauptstadt München, 2020 4 | */ 5 | package de.muenchen.cove.controller; 6 | 7 | import com.github.tomakehurst.wiremock.junit.WireMockClassRule; 8 | import de.muenchen.cove.ApiGatewayApplication; 9 | import org.junit.ClassRule; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.boot.test.context.SpringBootTest; 14 | import org.springframework.cloud.contract.wiremock.WireMockSpring; 15 | import org.springframework.http.HttpStatus; 16 | import org.springframework.security.test.context.support.WithMockUser; 17 | import org.springframework.test.context.ActiveProfiles; 18 | import org.springframework.test.context.junit4.SpringRunner; 19 | import org.springframework.test.web.reactive.server.WebTestClient; 20 | 21 | import static de.muenchen.cove.TestConstants.SPRING_TEST_PROFILE; 22 | import static de.muenchen.cove.TestConstants.WIREMOCK_PORT_NUMBER; 23 | 24 | 25 | @RunWith(SpringRunner.class) 26 | @SpringBootTest( 27 | classes = { ApiGatewayApplication.class }, 28 | webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT 29 | ) 30 | @ActiveProfiles(SPRING_TEST_PROFILE) 31 | public class PingControllerTest { 32 | 33 | /** 34 | * The port must correspond to the port of the backend route. 35 | */ 36 | @ClassRule 37 | public static WireMockClassRule wiremock = new WireMockClassRule(WireMockSpring.options().port(WIREMOCK_PORT_NUMBER)); 38 | 39 | @Autowired 40 | private WebTestClient webTestClient; 41 | 42 | @Test 43 | @WithMockUser() 44 | public void ping() { 45 | webTestClient.get().uri("/api").exchange() 46 | .expectStatus() 47 | .isEqualTo(HttpStatus.OK.value()); 48 | } 49 | 50 | @Test 51 | public void pingNotAuthenticated() { 52 | webTestClient.get().uri("/api").exchange() 53 | .expectStatus() 54 | .isEqualTo(HttpStatus.FOUND); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /apigateway/src/test/java/de/muenchen/cove/filter/CsrfTokenAppendingHelperFilterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik 3 | * der Landeshauptstadt München, 2020 4 | */ 5 | package de.muenchen.cove.filter; 6 | 7 | import de.muenchen.cove.ApiGatewayApplication; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.test.context.SpringBootTest; 12 | import org.springframework.security.test.context.support.WithMockUser; 13 | import org.springframework.test.context.ActiveProfiles; 14 | import org.springframework.test.context.junit4.SpringRunner; 15 | import org.springframework.test.web.reactive.server.WebTestClient; 16 | 17 | import static de.muenchen.cove.TestConstants.SPRING_TEST_PROFILE; 18 | 19 | 20 | @RunWith(SpringRunner.class) 21 | @SpringBootTest( 22 | classes = { ApiGatewayApplication.class }, 23 | webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT 24 | ) 25 | @ActiveProfiles(SPRING_TEST_PROFILE) 26 | public class CsrfTokenAppendingHelperFilterTest { 27 | 28 | @Autowired 29 | private WebTestClient webTestClient; 30 | 31 | @Test 32 | @WithMockUser() 33 | public void csrfCookieAppendition() { 34 | webTestClient.get().uri("/").exchange() 35 | .expectHeader() 36 | .valueMatches("set-cookie", "XSRF-TOKEN=[a-f\\d]{8}(-[a-f\\d]{4}){3}-[a-f\\d]{12}?;\\sPath=/"); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /apigateway/src/test/java/de/muenchen/cove/filter/GlobalAuthenticationErrorFilterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik 3 | * der Landeshauptstadt München, 2020 4 | */ 5 | package de.muenchen.cove.filter; 6 | 7 | import com.github.tomakehurst.wiremock.http.HttpHeader; 8 | import com.github.tomakehurst.wiremock.http.HttpHeaders; 9 | import com.github.tomakehurst.wiremock.junit.WireMockClassRule; 10 | import de.muenchen.cove.ApiGatewayApplication; 11 | import org.junit.ClassRule; 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | import org.junit.runner.RunWith; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.boot.test.context.SpringBootTest; 17 | import org.springframework.cloud.contract.wiremock.WireMockSpring; 18 | import org.springframework.http.HttpStatus; 19 | import org.springframework.security.test.context.support.WithMockUser; 20 | import org.springframework.test.context.ActiveProfiles; 21 | import org.springframework.test.context.junit4.SpringRunner; 22 | import org.springframework.test.web.reactive.server.WebTestClient; 23 | 24 | import static de.muenchen.cove.TestConstants.SPRING_TEST_PROFILE; 25 | import static de.muenchen.cove.TestConstants.WIREMOCK_PORT_NUMBER; 26 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; 27 | import static com.github.tomakehurst.wiremock.client.WireMock.get; 28 | import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; 29 | import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; 30 | 31 | 32 | @RunWith(SpringRunner.class) 33 | @SpringBootTest( 34 | classes = { ApiGatewayApplication.class }, 35 | webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT 36 | ) 37 | @ActiveProfiles(SPRING_TEST_PROFILE) 38 | public class GlobalAuthenticationErrorFilterTest { 39 | 40 | /** 41 | * The port must correspond to the port of the backend route. 42 | */ 43 | @ClassRule 44 | public static WireMockClassRule wiremock = new WireMockClassRule(WireMockSpring.options().port(WIREMOCK_PORT_NUMBER)); 45 | 46 | @Autowired 47 | private WebTestClient webTestClient; 48 | 49 | @Before 50 | public void setup() { 51 | stubFor(get(urlEqualTo("/remote")) 52 | .willReturn(aResponse() 53 | .withStatus(HttpStatus.UNAUTHORIZED.value()) 54 | .withHeaders(new HttpHeaders( 55 | new HttpHeader("Content-Type", "application/json"), 56 | new HttpHeader("WWW-Authenticate", "Bearer realm=\"Access to the staging site\", charset=\"UTF-8\""), 57 | new HttpHeader("Expires", "Wed, 21 Oct 2099 07:28:06 GMT") 58 | )) 59 | .withBody("{ \"testkey\" : \"testvalue\" }"))); 60 | } 61 | 62 | @Test 63 | @WithMockUser() 64 | public void backendAuthenticationError() { 65 | webTestClient.get().uri("/api/cove-backend-service/remote").exchange() 66 | .expectStatus().isEqualTo(HttpStatus.UNAUTHORIZED) 67 | .expectHeader().valueMatches("Content-Type", "application/json") 68 | .expectHeader().doesNotExist("WWW-Authenticate") 69 | .expectHeader().valueMatches("Expires", "0") 70 | .expectBody() 71 | .jsonPath("$.status").isEqualTo("401") 72 | .jsonPath("$.error").isEqualTo("Authentication Error"); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /apigateway/src/test/java/de/muenchen/cove/filter/GlobalBackendErrorFilterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik 3 | * der Landeshauptstadt München, 2020 4 | */ 5 | package de.muenchen.cove.filter; 6 | 7 | import com.github.tomakehurst.wiremock.http.HttpHeader; 8 | import com.github.tomakehurst.wiremock.http.HttpHeaders; 9 | import com.github.tomakehurst.wiremock.junit.WireMockClassRule; 10 | import de.muenchen.cove.ApiGatewayApplication; 11 | import org.junit.ClassRule; 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | import org.junit.runner.RunWith; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.boot.test.context.SpringBootTest; 17 | import org.springframework.cloud.contract.wiremock.WireMockSpring; 18 | import org.springframework.http.HttpStatus; 19 | import org.springframework.security.test.context.support.WithMockUser; 20 | import org.springframework.test.context.ActiveProfiles; 21 | import org.springframework.test.context.junit4.SpringRunner; 22 | import org.springframework.test.web.reactive.server.WebTestClient; 23 | 24 | import static de.muenchen.cove.TestConstants.SPRING_TEST_PROFILE; 25 | import static de.muenchen.cove.TestConstants.WIREMOCK_PORT_NUMBER; 26 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; 27 | import static com.github.tomakehurst.wiremock.client.WireMock.get; 28 | import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; 29 | import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; 30 | 31 | 32 | @RunWith(SpringRunner.class) 33 | @SpringBootTest( 34 | classes = { ApiGatewayApplication.class }, 35 | webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT 36 | ) 37 | @ActiveProfiles(SPRING_TEST_PROFILE) 38 | public class GlobalBackendErrorFilterTest { 39 | 40 | /** 41 | * The port must correspond to the port of the backend route. 42 | */ 43 | @ClassRule 44 | public static WireMockClassRule wiremock = new WireMockClassRule(WireMockSpring.options().port(WIREMOCK_PORT_NUMBER)); 45 | 46 | @Autowired 47 | private WebTestClient webTestClient; 48 | 49 | @Before 50 | public void setup() { 51 | stubFor(get(urlEqualTo("/remote")) 52 | .willReturn(aResponse() 53 | .withStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()) 54 | .withHeaders(new HttpHeaders( 55 | new HttpHeader("Content-Type", "application/json"), 56 | new HttpHeader("WWW-Authenticate", "Bearer realm=\"Access to the staging site\", charset=\"UTF-8\""), 57 | new HttpHeader("Expires", "Wed, 21 Oct 2099 07:28:06 GMT") 58 | )) 59 | .withBody("{ \"testkey\" : \"testvalue\" }"))); 60 | } 61 | 62 | @Test 63 | @WithMockUser() 64 | public void backendError() { 65 | webTestClient.get().uri("/api/cove-backend-service/remote").exchange() 66 | .expectStatus().isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR) 67 | .expectHeader().valueMatches("Content-Type", "application/json") 68 | .expectHeader().doesNotExist("WWW-Authenticate") 69 | .expectHeader().valueMatches("Expires", "0") 70 | .expectBody() 71 | .jsonPath("$.status").isEqualTo("500") 72 | .jsonPath("$.error").isEqualTo("Internal Server Error"); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /apigateway/src/test/java/de/muenchen/cove/filter/GlobalRequestParameterPollutionFilterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik 3 | * der Landeshauptstadt München, 2020 4 | */ 5 | package de.muenchen.cove.filter; 6 | 7 | import de.muenchen.cove.ApiGatewayApplication; 8 | import org.apache.commons.codec.Charsets; 9 | import org.junit.Assert; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.boot.test.context.SpringBootTest; 14 | import org.springframework.http.HttpStatus; 15 | import org.springframework.security.test.context.support.WithMockUser; 16 | import org.springframework.test.context.ActiveProfiles; 17 | import org.springframework.test.context.junit4.SpringRunner; 18 | import org.springframework.test.web.reactive.server.WebTestClient; 19 | 20 | import static de.muenchen.cove.TestConstants.SPRING_TEST_PROFILE; 21 | 22 | 23 | @RunWith(SpringRunner.class) 24 | @SpringBootTest( 25 | classes = { ApiGatewayApplication.class }, 26 | webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT 27 | ) 28 | @ActiveProfiles(SPRING_TEST_PROFILE) 29 | public class GlobalRequestParameterPollutionFilterTest { 30 | 31 | @Autowired 32 | private WebTestClient webTestClient; 33 | 34 | @Test 35 | @WithMockUser() 36 | public void parameterPollutionAttack() { 37 | final StringBuilder jsonResponseBody = new StringBuilder(); 38 | webTestClient.get().uri("/api/cove-backend-service/testendpoint?parameter1=testdata_1¶meter2=testdata¶meter1=testdata_2").exchange() 39 | .expectStatus() 40 | .isEqualTo(HttpStatus.BAD_REQUEST) 41 | .expectBody() 42 | .consumeWith(responseBody -> jsonResponseBody.append(new String(responseBody.getResponseBody(), Charsets.UTF_8))); 43 | Assert.assertTrue(jsonResponseBody.toString().contains("\"message\" : \"parameter pollution\"")); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /apigateway/src/test/java/de/muenchen/cove/route/BackendRouteTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik 3 | * der Landeshauptstadt München, 2020 4 | */ 5 | package de.muenchen.cove.route; 6 | 7 | import com.github.tomakehurst.wiremock.http.HttpHeader; 8 | import com.github.tomakehurst.wiremock.http.HttpHeaders; 9 | import com.github.tomakehurst.wiremock.junit.WireMockClassRule; 10 | import com.github.tomakehurst.wiremock.matching.EqualToPattern; 11 | import de.muenchen.cove.ApiGatewayApplication; 12 | import org.junit.ClassRule; 13 | import org.junit.Before; 14 | import org.junit.Test; 15 | import org.junit.runner.RunWith; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.boot.test.context.SpringBootTest; 18 | import org.springframework.cloud.contract.wiremock.WireMockSpring; 19 | import org.springframework.http.HttpStatus; 20 | import org.springframework.security.test.context.support.WithMockUser; 21 | import org.springframework.test.context.ActiveProfiles; 22 | import org.springframework.test.context.junit4.SpringRunner; 23 | import org.springframework.test.web.reactive.server.WebTestClient; 24 | 25 | import static de.muenchen.cove.TestConstants.SPRING_TEST_PROFILE; 26 | import static de.muenchen.cove.TestConstants.WIREMOCK_PORT_NUMBER; 27 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; 28 | import static com.github.tomakehurst.wiremock.client.WireMock.get; 29 | import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; 30 | import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; 31 | import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; 32 | import static com.github.tomakehurst.wiremock.client.WireMock.verify; 33 | 34 | 35 | @RunWith(SpringRunner.class) 36 | @SpringBootTest( 37 | classes = { ApiGatewayApplication.class }, 38 | webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT 39 | ) 40 | @ActiveProfiles(SPRING_TEST_PROFILE) 41 | public class BackendRouteTest { 42 | 43 | /** 44 | * The port must correspond to the port of the backend route. 45 | */ 46 | @ClassRule 47 | public static WireMockClassRule wiremock = new WireMockClassRule(WireMockSpring.options().port(WIREMOCK_PORT_NUMBER)); 48 | 49 | @Autowired 50 | private WebTestClient webTestClient; 51 | 52 | @Before 53 | public void setup() { 54 | stubFor(get(urlEqualTo("/remote/endpoint")) 55 | .willReturn(aResponse() 56 | .withStatus(HttpStatus.OK.value()) 57 | .withHeaders(new HttpHeaders( 58 | new HttpHeader("Content-Type", "application/json"), 59 | new HttpHeader("WWW-Authenticate", "Bearer realm=\"Access to the staging site\", charset=\"UTF-8\""), // removed by route filter 60 | new HttpHeader("Expires", "Wed, 21 Oct 2099 07:28:06 GMT") // removed by route filter 61 | )) 62 | .withBody("{ \"testkey\" : \"testvalue\" }"))); 63 | } 64 | 65 | @Test 66 | @WithMockUser() 67 | public void backendRouteResponse() { 68 | webTestClient.get().uri("/api/cove-backend-service/remote/endpoint") 69 | .header("Cookie", "SESSION=5cfb01a3-b691-4ca9-8735-a05690e6c2ec; XSRF-TOKEN=4d82f9f1-41f6-4a09-994a-df99d30d1be9") // removed by default-filter 70 | .header("X-XSRF-TOKEN", "5cfb01a3-b691-4ca9-8735-a05690e6c2ec") // angular specific -> removed by default-filter 71 | .header("Content-Type", "application/hal+json") 72 | .exchange() 73 | .expectStatus().isEqualTo(HttpStatus.OK) 74 | .expectHeader().valueMatches("Content-Type", "application/json") 75 | .expectHeader().doesNotExist("WWW-Authenticate") 76 | .expectHeader().valueMatches("Expires", "0") 77 | .expectBody().jsonPath("$.testkey").isEqualTo("testvalue"); 78 | 79 | verify(getRequestedFor(urlEqualTo("/remote/endpoint")) 80 | .withoutHeader("Cookie") 81 | .withoutHeader("X-SRF-TOKEN") 82 | .withHeader("Content-Type", new EqualToPattern("application/hal+json"))); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /frontend/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | Firefox > 5 -------------------------------------------------------------------------------- /frontend/.env.development: -------------------------------------------------------------------------------- 1 | NODE_ENV= 2 | VUE_APP_API_URL= 3 | VUE_APP_FAQ= 4 | VUE_APP_BENUTZERHANDBUCH= -------------------------------------------------------------------------------- /frontend/.env.production: -------------------------------------------------------------------------------- 1 | NODE_ENV= 2 | VUE_APP_API_URL= 3 | VUE_APP_FAQ= 4 | VUE_APP_BENUTZERHANDBUCH= -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | 'eslint:recommended', 9 | '@vue/typescript' 10 | ], 11 | rules: { 12 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 13 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 14 | }, 15 | parserOptions: { 16 | parser: '@typescript-eslint/parser' 17 | }, 18 | overrides: [ 19 | { 20 | files: [ 21 | '**/__tests__/*.{j,t}s?(x)', 22 | '**/tests/unit/**/*.spec.{j,t}s?(x)' 23 | ], 24 | env: { 25 | jest: true 26 | } 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Covers Maven specific 2 | target/ 3 | pom.xml.tag 4 | pom.xml.releaseBackup 5 | pom.xml.versionsBackup 6 | pom.xml.next 7 | release.properties 8 | dependency-reduced-pom.xml 9 | buildNumber.properties 10 | .mvn/timing.properties 11 | !/.mvn/wrapper/maven-wrapper.jar 12 | 13 | # Covers Eclipse specific: 14 | .settings/ 15 | .classpath 16 | .project 17 | 18 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 19 | .idea 20 | *.iml 21 | 22 | # Covers Netbeans: 23 | **/nbproject/private/ 24 | **/nbproject/Makefile-*.mk 25 | **/nbproject/Package-*.bash 26 | build/ 27 | nbbuild/ 28 | dist/ 29 | nbdist/ 30 | .nb-gradle/ 31 | 32 | # Visual Studio Code specific 33 | .vscode/ 34 | *.coverage 35 | *.coveragexml 36 | *_i.c 37 | *_p.c 38 | *_i.h 39 | *.ilk 40 | *.meta 41 | *.obj 42 | *.pch 43 | *.pdb 44 | *.pgc 45 | *.pgd 46 | *.rsp 47 | *.sbr 48 | *.tlb 49 | *.tli 50 | *.tlh 51 | *.tmp 52 | *.tmp_proj 53 | *.vspscc 54 | *.vssscc 55 | .builds 56 | *.pidb 57 | *.svclog 58 | *.scc 59 | *.suo 60 | *.user 61 | *.userosscache 62 | *.sln.docstates 63 | *.userprefs 64 | [Dd]ebug/ 65 | [Dd]ebugPublic/ 66 | [Rr]elease/ 67 | [Rr]eleases/ 68 | x64/ 69 | x86/ 70 | bld/ 71 | [Bb]in/ 72 | [Oo]bj/ 73 | [Ll]og/ 74 | 75 | .DS_Store 76 | node_modules 77 | /dist 78 | 79 | # local env files 80 | .env.local 81 | .env.*.local 82 | 83 | # Logs 84 | logs 85 | *.log 86 | npm-debug.log* 87 | yarn-debug.log* 88 | yarn-error.log* 89 | lerna-debug.log* 90 | 91 | # Diagnostic reports (https://nodejs.org/api/report.html) 92 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 93 | 94 | # Runtime data 95 | pids 96 | *.pid 97 | *.seed 98 | *.pid.lock 99 | 100 | # Directory for instrumented libs generated by jscoverage/JSCover 101 | lib-cov 102 | 103 | # Coverage directory used by tools like istanbul 104 | coverage 105 | *.lcov 106 | 107 | # nyc test coverage 108 | .nyc_output 109 | 110 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 111 | .grunt 112 | 113 | # Bower dependency directory (https://bower.io/) 114 | bower_components 115 | 116 | # node-waf configuration 117 | .lock-wscript 118 | 119 | # Compiled binary addons (https://nodejs.org/api/addons.html) 120 | build/Release 121 | 122 | # Dependency directories 123 | node_modules/ 124 | jspm_packages/ 125 | 126 | # TypeScript v1 declaration files 127 | typings/ 128 | 129 | # TypeScript cache 130 | *.tsbuildinfo 131 | 132 | # Optional npm cache directory 133 | .npm 134 | 135 | # Optional eslint cache 136 | .eslintcache 137 | 138 | # Microbundle cache 139 | .rpt2_cache/ 140 | .rts2_cache_cjs/ 141 | .rts2_cache_es/ 142 | .rts2_cache_umd/ 143 | 144 | # Optional REPL history 145 | .node_repl_history 146 | 147 | # Output of 'npm pack' 148 | *.tgz 149 | 150 | # Yarn Integrity file 151 | .yarn-integrity 152 | 153 | # dotenv environment variables file 154 | .env 155 | .env.test 156 | 157 | # parcel-bundler cache (https://parceljs.org/) 158 | .cache 159 | 160 | # Next.js build output 161 | .next 162 | 163 | # Nuxt.js build / generate output 164 | .nuxt 165 | dist 166 | 167 | # Gatsby files 168 | .cache/ 169 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 170 | # https://nextjs.org/blog/next-9-1#public-directory-support 171 | # public 172 | 173 | # vuepress build output 174 | .vuepress/dist 175 | 176 | # Serverless directories 177 | .serverless/ 178 | 179 | # FuseBox cache 180 | .fusebox/ 181 | 182 | # DynamoDB Local files 183 | .dynamodb/ 184 | 185 | # TernJS port file 186 | .tern-port 187 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # RefArch-Kickstarter-GUI 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Run your tests 19 | ``` 20 | npm run test 21 | ``` 22 | 23 | ### Lints and fixes files 24 | ``` 25 | npm run lint 26 | ``` 27 | 28 | ### Customize configuration 29 | See [Configuration Reference](https://cli.vuejs.org/config/). 30 | -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-env', 4 | ['@vue/app', 5 | {useBuiltIns: 'entry'} 6 | ] 7 | ] 8 | }; 9 | -------------------------------------------------------------------------------- /frontend/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel' 3 | } 4 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testexample", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve --port 8081", 7 | "build": "vue-cli-service build", 8 | "test:unit": "vue-cli-service test:unit", 9 | "lint": "vue-cli-service lint" 10 | }, 11 | "dependencies": { 12 | "babel-polyfill": "^6.26.0", 13 | "core-js": "^3.5.0", 14 | "moment": "^2.24.0", 15 | "vue": "^2.6.11", 16 | "vue-class-component": "^7.0.2", 17 | "vue-property-decorator": "^8.3.0", 18 | "vue-router": "^3.1.3", 19 | "vuetify": "^2.2.18", 20 | "vuex": "^3.1.2" 21 | }, 22 | "devDependencies": { 23 | "@types/jest": "^24.0.19", 24 | "@types/node": "^12.12.21", 25 | "@babel/preset-env": "^7.7.6", 26 | "@mdi/font": "^4.7.95", 27 | "@vue/cli-plugin-router": "^4.1.1", 28 | "@vue/cli-plugin-typescript": "^4.1.1", 29 | "@vue/cli-plugin-unit-jest": "^4.1.1", 30 | "@vue/cli-plugin-vuex": "^4.1.1", 31 | "@vue/cli-plugin-babel": "^4.1.1", 32 | "@vue/cli-plugin-eslint": "^4.1.1", 33 | "@vue/cli-service": "^4.1.1", 34 | "@vue/eslint-config-typescript": "^4.0.0", 35 | "@vue/test-utils": "1.0.0-beta.29", 36 | "babel-eslint": "^10.0.3", 37 | "css-loader": "^3.4.0", 38 | "eslint": "^6.7.2", 39 | "eslint-plugin-vue": "^6.0.1", 40 | "file-loader": "^5.0.2", 41 | "sass": "^1.23.7", 42 | "sass-loader": "^8.0.0", 43 | "stylus": "^0.54.7", 44 | "stylus-loader": "^3.0.2", 45 | "url-loader": "^3.0.0", 46 | "vue-cli-plugin-vuetify": "^2.0.2", 47 | "vue-template-compiler": "^2.6.11", 48 | "vuetify-loader": "^1.4.3", 49 | "typescript": "~3.5.3" 50 | }, 51 | "postcss": { 52 | "plugins": { 53 | "autoprefixer": {} 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /frontend/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | de.muenchen.cove.frontend 8 | cove-frontend-frontend 9 | 1.0-SNAPSHOT 10 | jar 11 | cove_frontend_frontend 12 | 13 | 14 | 15 | 1.8 16 | 1.8 17 | 1.8 18 | 1.6.0 19 | 3.1.0 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | org.codehaus.mojo 29 | exec-maven-plugin 30 | ${exec.maven.plugin} 31 | 32 | 33 | npm install 34 | generate-sources 35 | 36 | exec 37 | 38 | 39 | npm 40 | 41 | install 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | npm run build 51 | generate-sources 52 | 53 | exec 54 | 55 | 56 | npm 57 | 58 | run 59 | build 60 | 61 | 62 | 63 | 64 | npm run test 65 | generate-sources 66 | 67 | exec 68 | 69 | 70 | npm 71 | 72 | run 73 | test:unit 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | maven-clean-plugin 82 | ${maven.clean.plugin} 83 | 84 | 85 | 86 | dist 87 | 88 | **/* 89 | 90 | false 91 | 92 | 93 | node_modules 94 | 95 | **/* 96 | 97 | false 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | dist 108 | static 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/it-at-m/cove-frontend/22b7c544d4c3eb06d79583644df39a1e88927ea1/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | CoVe 9 | 10 | 11 | 14 |

15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 87 | 88 | 93 | 94 | -------------------------------------------------------------------------------- /frontend/src/api/BerichtService.ts: -------------------------------------------------------------------------------- 1 | import { ApiError, Levels } from '@/api/error'; 2 | import FetchUtils from './FetchUtils'; 3 | import DailyCallReport from '@/types/DailyCallReport'; 4 | import Bericht from '@/types/Bericht'; 5 | 6 | export default class BerichtService { 7 | 8 | private static base: string | undefined = process.env.VUE_APP_API_URL + "/api/cove-backend-service"; 9 | 10 | static getBericht(): Promise { 11 | return fetch(`${this.base}/persons/bericht`, FetchUtils.getGETConfig()) 12 | .catch(FetchUtils.defaultPersonenServiceCatchHandler) 13 | .then(response => { 14 | FetchUtils.defaultResponseHandler(response, `Beim Erstellen des Berichts ist ein Fehler aufgetreten.`); 15 | return new Promise((resolve) => resolve(response.json())); 16 | }) 17 | } 18 | 19 | static getDailyCallStatistik(): Promise { 20 | return fetch(`${this.base}/persons/dailyCallStatistik`, FetchUtils.getGETConfig()) 21 | .catch(FetchUtils.defaultPersonenServiceCatchHandler) 22 | .then(response => { 23 | FetchUtils.defaultResponseHandler(response, `Beim Erstellen des täglichen Anrufsfortschritts ist ein Fehler aufgetreten.`); 24 | return new Promise((resolve) => resolve(response.json())); 25 | }) 26 | } 27 | } -------------------------------------------------------------------------------- /frontend/src/api/FetchUtils.ts: -------------------------------------------------------------------------------- 1 | import { ApiError, Levels } from "@/api/error"; 2 | 3 | export default class FetchUtils { 4 | 5 | /** 6 | * Liefert eine default GET-Config für fetch 7 | */ 8 | static getGETConfig() : RequestInit { 9 | return { 10 | headers: this.getHeaders(), 11 | mode: 'cors', 12 | credentials: 'include', 13 | redirect: 'manual' 14 | }; 15 | } 16 | 17 | /** 18 | * Liefert eine default POST-Config für fetch 19 | * @param body Optional zu übertragender Body 20 | */ 21 | static getPOSTConfig(body:any) : RequestInit { 22 | return { 23 | method: 'POST', 24 | body: body ? JSON.stringify(body) : undefined, 25 | headers: FetchUtils.getHeaders(), 26 | mode: 'cors', 27 | credentials: 'include', 28 | redirect: "manual" 29 | } 30 | } 31 | 32 | /** 33 | * Liefert eine default PUT-Config für fetch 34 | * In dieser wird, wenn vorhanden, die Version der zu aktualisierenden Entität 35 | * als "If-Match"-Header mitgesetzt. 36 | * @param body Optional zu übertragender Body 37 | */ 38 | static getPUTConfig(body:any) : RequestInit { 39 | let headers = FetchUtils.getHeaders(); 40 | if(body.version) { 41 | headers.append("If-Match", body.version) 42 | } 43 | return { 44 | method: 'PUT', 45 | body: body ? JSON.stringify(body) : undefined, 46 | headers, 47 | mode: 'cors', 48 | credentials: 'include', 49 | redirect: "manual" 50 | } 51 | } 52 | 53 | /** 54 | * Liefert eine default PATCH-Config für fetch 55 | * In dieser wird, wenn vorhanden, die Version der zu aktualisierenden Entität 56 | * als "If-Match"-Header mitgesetzt. 57 | * @param body Optional zu übertragender Body 58 | */ 59 | static getPATCHConfig(body:any) : RequestInit { 60 | let headers = FetchUtils.getHeaders(); 61 | if(body.version !== undefined) { 62 | headers.append("If-Match", body.version) 63 | } 64 | return { 65 | method: 'PATCH', 66 | body: body ? JSON.stringify(body) : undefined, 67 | headers, 68 | mode: 'cors', 69 | credentials: 'include', 70 | redirect: "manual" 71 | } 72 | } 73 | 74 | /** 75 | * Default Catch-Handler für alle Anfragen des Personen-Service. 76 | * Schmeißt derzeit nur einen ApiError 77 | * @param error die Fehlermeldung aus fetch-Befehl 78 | */ 79 | static defaultPersonenServiceCatchHandler(error : Error) : PromiseLike { 80 | throw new ApiError({ 81 | level: Levels.ERROR, 82 | message: "Verbindung zum Personen-Service konnte nicht aufgebaut werden." 83 | }); 84 | } 85 | 86 | /** 87 | * Deckt das Default-Handling einer Response ab. Dazu zählt: 88 | * 89 | * - Fehler bei fehlenden Berechtigungen --> HTTP 403 90 | * - Reload der App bei Session-Timeout --> HTTP 3xx 91 | * - Default-Fehler bei allen HTTP-Codes !2xx 92 | * 93 | * @param response Die response aus fetch-Befehl die geprüft werden soll. 94 | * @param errorMessage Die Fehlermeldung, welche bei einem HTTP-Code != 2xx angezeigt werden soll. 95 | */ 96 | static defaultResponseHandler(response : Response, errorMessage : string = "Es ist ein unbekannter Fehler aufgetreten.") { 97 | if (!response.ok) { 98 | if(response.status === 403) { 99 | throw new ApiError({ 100 | level: Levels.ERROR, 101 | message: `Sie haben nicht die nötigen Rechte um diese Aktion durchzuführen.` 102 | }); 103 | } else if (response.type === "opaqueredirect") { 104 | location.reload() 105 | } 106 | throw new ApiError({ 107 | level: Levels.WARNING, 108 | message: errorMessage 109 | }); 110 | } 111 | } 112 | 113 | /** 114 | * Baut den Header fuer den Request auf 115 | * @returns {Headers} 116 | */ 117 | static getHeaders(): Headers { 118 | let headers = new Headers({ 119 | 'Content-Type': 'application/json' 120 | }); 121 | let csrf_cookie = this._getXSRFToken(); 122 | if (csrf_cookie !== '') { 123 | headers.append('X-XSRF-TOKEN', csrf_cookie); 124 | } 125 | return headers; 126 | } 127 | 128 | /** 129 | * Liefert den XSRF-TOKEN zurück. 130 | * @returns {string|string} 131 | */ 132 | static _getXSRFToken(): string { 133 | let help = document.cookie.match('(^|;)\\s*' + 'XSRF-TOKEN' + '\\s*=\\s*([^;]+)'); 134 | return (help ? help.pop() : '') as string; 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /frontend/src/api/HealthService.ts: -------------------------------------------------------------------------------- 1 | import {ApiError, Levels} from '@/api/error'; 2 | import HealthState from "@/types/HealthState"; 3 | import FetchUtils from "@/api/FetchUtils"; 4 | 5 | export default class HealthService { 6 | 7 | private static base: string | undefined = process.env.VUE_APP_API_URL 8 | 9 | static checkHealth(): Promise { 10 | return fetch(`${this.base}/actuator/health`, FetchUtils.getGETConfig()) 11 | .then(response => { 12 | FetchUtils.defaultResponseHandler(response, "Beim Laden der Daten vom API-Gateway ist ein Fehler aufgetreten."); 13 | return response.json(); 14 | }) 15 | .catch(error => { 16 | throw new ApiError({ 17 | level: Levels.ERROR, 18 | message: "Verbindung zum API-Gateway konnte nicht aufgebaut werden." 19 | }); 20 | 21 | }) 22 | } 23 | } -------------------------------------------------------------------------------- /frontend/src/api/error.ts: -------------------------------------------------------------------------------- 1 | export const enum Levels { 2 | INFO = 'info', 3 | WARNING = 'warning', 4 | ERROR = 'error' 5 | } 6 | 7 | export class ApiError extends Error { 8 | level: string; 9 | constructor({level = Levels.ERROR, message = "Ein unbekannter Fehler ist aufgetreten, bitte den Administrator informieren."}: { level?: string, message?: string }) { 10 | // Übergibt die verbleibenden Parameter (einschließlich Vendor spezifischer Parameter) dem Error Konstruktor 11 | super(message); 12 | 13 | // Behält den richtigen Stack-Trace für die Stelle bei, an der unser Fehler ausgelöst wurde 14 | this.stack = new Error().stack; 15 | 16 | // Benutzerdefinierte Informationen 17 | this.level = level; 18 | this.message = message; 19 | } 20 | } -------------------------------------------------------------------------------- /frontend/src/api/types/PersonSearchparams.ts: -------------------------------------------------------------------------------- 1 | import Kategorie from '@/types/Kategorie'; 2 | 3 | export default class PersonSearchParams { 4 | name: string; 5 | kategorie: Kategorie; 6 | 7 | constructor(name:string, kategorie:Kategorie) { 8 | this.name = name; 9 | this.kategorie = kategorie; 10 | } 11 | } -------------------------------------------------------------------------------- /frontend/src/assets/corona.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/it-at-m/cove-frontend/22b7c544d4c3eb06d79583644df39a1e88927ea1/frontend/src/assets/corona.png -------------------------------------------------------------------------------- /frontend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/it-at-m/cove-frontend/22b7c544d4c3eb06d79583644df39a1e88927ea1/frontend/src/assets/logo.png -------------------------------------------------------------------------------- /frontend/src/components/Common/DatetimeInput.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | -------------------------------------------------------------------------------- /frontend/src/components/Common/LoeschenDialog.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | -------------------------------------------------------------------------------- /frontend/src/components/Common/YesNoDialog.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | -------------------------------------------------------------------------------- /frontend/src/components/IndexTable.vue: -------------------------------------------------------------------------------- 1 | 29 | -------------------------------------------------------------------------------- /frontend/src/components/KontaktFields.vue: -------------------------------------------------------------------------------- 1 | 46 | -------------------------------------------------------------------------------- /frontend/src/components/PersonenAuswaehler.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 72 | 73 | -------------------------------------------------------------------------------- /frontend/src/components/PersonenListe.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 120 | 121 | -------------------------------------------------------------------------------- /frontend/src/components/PersonenSuche.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 88 | 89 | -------------------------------------------------------------------------------- /frontend/src/components/ProbeFields.vue: -------------------------------------------------------------------------------- 1 | 33 | -------------------------------------------------------------------------------- /frontend/src/components/ProbeTable.vue: -------------------------------------------------------------------------------- 1 | 52 | -------------------------------------------------------------------------------- /frontend/src/components/TheSnackbar.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 59 | 60 | -------------------------------------------------------------------------------- /frontend/src/components/call/CallDispositionPersonenTable.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 39 | 40 | -------------------------------------------------------------------------------- /frontend/src/components/call/CallPersonFields.vue: -------------------------------------------------------------------------------- 1 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /frontend/src/components/call/CallPersonenTable.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 32 | 33 | -------------------------------------------------------------------------------- /frontend/src/components/call/CallReminderButton.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 107 | 108 | -------------------------------------------------------------------------------- /frontend/src/components/call/CallingPopup.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 106 | 107 | -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import 'babel-polyfill' 3 | import Vuetify from "./plugins/vuetify"; 4 | import store from './store' 5 | import App from './App.vue'; 6 | import router from "./router"; 7 | import moment from "moment"; 8 | 9 | Vue.config.productionTip = false; 10 | 11 | moment.locale(window.navigator.language); 12 | 13 | new Vue({ 14 | router, 15 | store: store, 16 | vuetify: Vuetify, 17 | render: h => h(App), 18 | }).$mount('#app'); 19 | -------------------------------------------------------------------------------- /frontend/src/mixins/formatter.ts: -------------------------------------------------------------------------------- 1 | import {Component, Vue} from 'vue-property-decorator'; 2 | import moment from "moment"; 3 | 4 | @Component 5 | export default class Formatter extends Vue { 6 | 7 | startingCharUpperCase(text:string) { 8 | return text ? text.toLowerCase().replace(/^\w/, c => c.toUpperCase()): "" 9 | } 10 | formatDate(date: Date | undefined | null) { 11 | return date ? moment(date).format('L'): "" 12 | } 13 | formatDateTime(date: Date | undefined | null) { 14 | return date ? moment(date).format('L - LT'): "" 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /frontend/src/mixins/saveLeaveMixin.ts: -------------------------------------------------------------------------------- 1 | import { Component, Vue } from "vue-property-decorator"; 2 | 3 | Component.registerHooks([ 4 | 'beforeRouteLeave', 5 | ]); 6 | @Component 7 | export default class SaveLeaveMixin extends Vue{ 8 | name:string = "saveLeaveMixin"; 9 | 10 | 11 | saveLeaveDialogTitle:string = 'Ungespeicherte Änderungen'; 12 | saveLeaveDialogText:string = 'Es sind ungespeicherte Änderungen vorhanden. Wollen Sie die Seite verlassen?'; 13 | saveLeaveDialog:boolean = false; 14 | isSave:boolean = false; 15 | next:any = null; 16 | 17 | beforeRouteLeave (to:any, from:any, next:any) { 18 | if(this.isDirty() && !this.isSave) { 19 | this.saveLeaveDialog = true; 20 | this.next = next; 21 | } else { 22 | this.saveLeaveDialog = false; 23 | next() 24 | } 25 | } 26 | cancel() { 27 | //erzwingt das Neuladen des Dialogs. Somit werden nicht gespeicherte Eingaben wieder zurückgesetzt. 28 | this.saveLeaveDialog = false; 29 | this.next(false); 30 | } 31 | 32 | leave() { 33 | this.next(); 34 | } 35 | 36 | isDirty() { 37 | return true; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /frontend/src/plugins/vuetify.ts: -------------------------------------------------------------------------------- 1 | import '@mdi/font/css/materialdesignicons.css' 2 | import Vue from 'vue'; 3 | import Vuetify from 'vuetify/lib'; 4 | 5 | Vue.use(Vuetify); 6 | 7 | const theme = { 8 | themes: { 9 | light: { 10 | primary: '#333333', 11 | secondary: '#FFCC00', 12 | accent: '#7BA4D9', 13 | success: '#69BE28', 14 | error: '#FF0000', 15 | }, 16 | } 17 | }; 18 | 19 | export default new Vuetify({ 20 | theme: theme 21 | }); -------------------------------------------------------------------------------- /frontend/src/router.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Router from "vue-router"; 3 | import Main from './views/Main.vue' 4 | import BerichtView from './views/BerichtView.vue' 5 | import PersonCreateView from './views/PersonCreateView.vue' 6 | import PersonSearchView from './views/PersonSearchView.vue' 7 | import PersonEditView from './views/PersonEditView.vue' 8 | import PersonReadView from './views/PersonReadView.vue' 9 | import EndgespraecheView from './views/EndgespraecheView.vue' 10 | import TaeglicheAnrufeView from './views/TaeglicheAnrufeView.vue' 11 | import AnrufsplanungView from "@/views/AnrufsplanungView.vue"; 12 | 13 | Vue.use(Router); 14 | 15 | /* 16 | * Preventing "NavigationDuplicated" errors in console in Vue-router >= 3.1.0 17 | * https://github.com/vuejs/vue-router/issues/2881#issuecomment-520554378 18 | * */ 19 | const routerMethods = ['push', 'replace']; 20 | routerMethods.forEach((method: string) => { 21 | const originalCall = (Router.prototype as any)[method]; 22 | (Router.prototype as any)[method] = function(location: any, onResolve: any, onReject: any): Promise { 23 | if (onResolve || onReject) { 24 | return originalCall.call(this, location, onResolve, onReject); 25 | } 26 | return (originalCall.call(this, location) as any).catch((err: any) => err); 27 | }; 28 | }); 29 | 30 | export default new Router({ 31 | base: process.env.BASE_URL, 32 | routes: [ 33 | { 34 | path: "/create", 35 | name: "create", 36 | component: PersonCreateView 37 | }, 38 | { 39 | path: "/bericht", 40 | name: "bericht", 41 | component: BerichtView 42 | }, 43 | { 44 | path: "/search", 45 | name: "search", 46 | component: PersonSearchView 47 | }, 48 | { 49 | path: "/edit/:id", 50 | name: "edit", 51 | component: PersonEditView 52 | }, 53 | { 54 | path: "/read/:id", 55 | name: "read", 56 | component: PersonReadView 57 | }, 58 | { 59 | path: "/endCalls/", 60 | redirect: "/endcalls/Index" 61 | }, 62 | { 63 | path: "/endCalls/:type", 64 | component: EndgespraecheView 65 | }, 66 | { 67 | path: "/dailyCalls", 68 | component: TaeglicheAnrufeView 69 | }, 70 | { 71 | path: "/calldisposition", 72 | name: "calldisposition", 73 | component: AnrufsplanungView 74 | }, 75 | {path: '/', redirect: '/create'}, //Fallback 1 76 | {path: '*', redirect: '/create'} //Fallback 2 77 | ] 78 | }); -------------------------------------------------------------------------------- /frontend/src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue' 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | -------------------------------------------------------------------------------- /frontend/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import user from './modules/user' 4 | import snackbar from './modules/snackbar' 5 | 6 | Vue.use(Vuex); 7 | const debug = process.env.NODE_ENV !== 'production'; 8 | 9 | export default new Vuex.Store({ 10 | modules: { 11 | user, 12 | snackbar 13 | }, 14 | strict: debug 15 | }) 16 | -------------------------------------------------------------------------------- /frontend/src/store/modules/snackbar.ts: -------------------------------------------------------------------------------- 1 | import {Levels} from "@/api/error"; 2 | 3 | export interface SnackbarState { 4 | message: string | undefined, 5 | level: Levels, 6 | aktive: boolean 7 | } 8 | 9 | export default { 10 | namespaced: true, 11 | state: { 12 | message: undefined, 13 | level: Levels.INFO, 14 | aktive: false 15 | } as SnackbarState, 16 | getters: { 17 | }, 18 | mutations: { 19 | SET_MESSAGE(state:SnackbarState, message:string) { 20 | state.message = message 21 | }, 22 | SET_LEVEL(state:SnackbarState, level:Levels) { 23 | state.level = level 24 | }, 25 | SET_AKTIVE(state:SnackbarState, aktive:boolean) { 26 | state.aktive = aktive 27 | } 28 | }, 29 | actions: { 30 | showMessage(context: any, message: SnackbarState) { 31 | context.commit('SET_LEVEL', message.level ? message.level : Levels.INFO); 32 | context.commit('SET_MESSAGE', message.message); 33 | context.commit('SET_AKTIVE', true); 34 | }, 35 | deactivate(context: any) { 36 | context.commit('SET_AKTIVE', false); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /frontend/src/store/modules/user.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | namespaced: true, 3 | state: { 4 | user: null 5 | }, 6 | getters: { 7 | 8 | }, 9 | mutations: { 10 | setUser({state, user}: { state: any, user: any }){ 11 | state.user = user 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /frontend/src/types/Bericht.ts: -------------------------------------------------------------------------------- 1 | export interface InQuarantaene{ 2 | I: string; 3 | KP: string; 4 | } 5 | 6 | export interface Konversionen { 7 | I: string; 8 | } 9 | 10 | export interface Kategorien{ 11 | KP: string; 12 | KPN: string; 13 | I: string; 14 | nicht_gesetzt: string; 15 | gesamt: string; 16 | } 17 | 18 | export interface Probenergebnis{ 19 | A: string; 20 | P: string; 21 | N: string; 22 | gesamt: string; 23 | } 24 | export interface Einrichtungen{ 25 | KH: string; 26 | AH: string; 27 | PR: string; 28 | SCHU: string; 29 | KITA: string; 30 | nicht_gesetzt: string; 31 | gesamt: string; 32 | } 33 | export default interface Bericht{ 34 | anzahl: { 35 | kategorie: Kategorien; 36 | probenergebnis: Probenergebnis; 37 | inQuarantaene: InQuarantaene; 38 | einrichtungen: Einrichtungen; 39 | konversionen: Konversionen; 40 | } 41 | } -------------------------------------------------------------------------------- /frontend/src/types/DailyCallReport.ts: -------------------------------------------------------------------------------- 1 | export default interface DailyCallReport { 2 | dailyCallsTodo: number, 3 | dailyCallsTotal: number 4 | } -------------------------------------------------------------------------------- /frontend/src/types/Ergebnis.ts: -------------------------------------------------------------------------------- 1 | enum Ergebnis{ 2 | Positiv = 'P', 3 | Ausstehend = 'A', 4 | Negativ = 'N' 5 | } 6 | export const ErgebnisToBeschreibung = new Map ([ 7 | [Ergebnis.Positiv, 'positiv'], 8 | [Ergebnis.Ausstehend, 'ausstehend'], 9 | [Ergebnis.Negativ, 'negativ'], 10 | ]) 11 | 12 | export const BeschreibungToErgebnis = new Map([...ErgebnisToBeschreibung].reverse()); 13 | 14 | export default Ergebnis; -------------------------------------------------------------------------------- /frontend/src/types/Gespraeche.ts: -------------------------------------------------------------------------------- 1 | export enum Gespraeche { 2 | Index = "index", 3 | End = "endgespraech" 4 | } -------------------------------------------------------------------------------- /frontend/src/types/HealthState.ts: -------------------------------------------------------------------------------- 1 | export default class HealthState { 2 | status: string 3 | 4 | constructor(status:string) { 5 | this.status = status 6 | } 7 | } -------------------------------------------------------------------------------- /frontend/src/types/Kategorie.ts: -------------------------------------------------------------------------------- 1 | enum Kategorie{ 2 | Index = 'I', 3 | Kontaktperson = 'KP', 4 | NegativeKontaktperson = 'KPN' 5 | } 6 | 7 | export const KategorieToText = new Map ([ 8 | [Kategorie.Index, 'Index (I)'], 9 | [Kategorie.Kontaktperson, 'Kontaktperson (KP)'], 10 | [Kategorie.NegativeKontaktperson, 'Negativ getestete Kontaktperson (KPN)'], 11 | ]) 12 | 13 | export default Kategorie; -------------------------------------------------------------------------------- /frontend/src/types/KeyVal.ts: -------------------------------------------------------------------------------- 1 | export default interface KeyVal { 2 | text: string; 3 | value: string; 4 | } -------------------------------------------------------------------------------- /frontend/src/types/Kontakt.ts: -------------------------------------------------------------------------------- 1 | import KontaktTyp from './KontaktTyp'; 2 | import Person from './Person'; 3 | export interface Kontakt { 4 | // Die ID die man wegen HATEOAS zum schreiben verwenden muss 🤷‍♂️ 5 | kontakt: string; 6 | kommentar: string; 7 | kontakttyp: KontaktTyp; 8 | kontaktdatum: Date; 9 | // Kommt nicht von Backend, wird manuell aufgelöst 10 | _person: Person; 11 | _links: { 12 | kontakt: { 13 | href: string; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /frontend/src/types/KontaktTyp.ts: -------------------------------------------------------------------------------- 1 | enum KontaktTyp{ 2 | A = 'A', 3 | B = 'B', 4 | C = 'C' 5 | } 6 | 7 | export const KontakttypToBeschreibung = new Map ([ 8 | [KontaktTyp.A, '15 mins F2F'], 9 | [KontaktTyp.B, 'B'], 10 | [KontaktTyp.C, 'C'], 11 | ]) 12 | 13 | export const BeschreibungToKontakttyp = new Map([...KontakttypToBeschreibung].reverse()); 14 | export default KontaktTyp; -------------------------------------------------------------------------------- /frontend/src/types/MedEinrichtung.ts: -------------------------------------------------------------------------------- 1 | enum MedEinrichtung{ 2 | Krankenhaus = 'KH', 3 | Altenheim = 'AH', 4 | Praxis = 'PR', 5 | Schule = 'SCHU', 6 | Kindertagesstätte = 'KITA' 7 | } 8 | 9 | export const MedEinrichtungToText = new Map ([ 10 | [MedEinrichtung.Krankenhaus, 'Krankenhaus (KH)'], 11 | [MedEinrichtung.Altenheim, 'Altenheim (AH)'], 12 | [MedEinrichtung.Praxis, 'Praxis (PR)'], 13 | [MedEinrichtung.Schule, 'Schule (SCHU)'], 14 | [MedEinrichtung.Kindertagesstätte, 'Kindertagesstätte (KITA)'], 15 | ]) 16 | 17 | export default MedEinrichtung; -------------------------------------------------------------------------------- /frontend/src/types/Person.ts: -------------------------------------------------------------------------------- 1 | import Kategorie from './Kategorie' 2 | import MedEinrichtung from './MedEinrichtung' 3 | import Ergebnis from './Ergebnis'; 4 | import { Kontakt } from './Kontakt'; 5 | 6 | export interface Quarantäne{ 7 | start: Date; 8 | ende: Date; 9 | } 10 | 11 | export interface Probe{ 12 | kommentar: string; 13 | datum : Date; 14 | ergebnis: Ergebnis; 15 | } 16 | 17 | /** 18 | * Teilperson die per patch die aktuell angerufene Person aktualisiert. 19 | * Verhindert Probleme beim speichern von Kontakten und anderen Relationen 20 | */ 21 | export interface PersonCallUpdate { 22 | id: string; 23 | telefonnotizen?: string; 24 | doku?: string; 25 | quarantaene: Quarantäne; 26 | 27 | letzterKontakt : Date | null; 28 | endTelefonatErfolgtAm : Date | null; 29 | aktuellerBearbeiter : string | null; 30 | 31 | // Wird aus "ETag"-Header im PersonService gesetzt. 32 | version: number; 33 | } 34 | 35 | export default interface Person{ 36 | id: string; 37 | createdDate: Date 38 | name: string; 39 | vorname: string; 40 | vornameNachname: string; 41 | kategorie: Kategorie; 42 | medEinrichtung: MedEinrichtung; 43 | standort?: string; 44 | geburtsdatum?: Date; 45 | strasse?: string; 46 | ort?: string; 47 | plz?: string; 48 | landkreis?: string; 49 | land?: string; 50 | telefon?: string; 51 | mobile?: string; 52 | mail?: string; 53 | kontakte: Kontakt[]; 54 | quarantaene: Quarantäne; 55 | proben: Probe[]; 56 | kommentare?: string; 57 | haushalt?: string; 58 | telefonnotizen?: string; 59 | doku?: string; 60 | ehemalsKp? : Date; 61 | letzterKontakt : Date | null; 62 | erstKontaktErfolgtAm?: Date; 63 | endTelefonatErfolgtAm : Date | null; 64 | aktuellerBearbeiter : string | null; 65 | 66 | // Wird aus "ETag"-Header im PersonService gesetzt, wenn es nicht direkt vom Backend kommt... 67 | version: number; 68 | } 69 | 70 | export function equalsPerson(person1:Person, person2:Person) : Boolean { 71 | return person1.id === person2.id && 72 | person1.createdDate === person2.createdDate && 73 | person1.name === person2.name && 74 | person1.vorname === person2.vorname && 75 | person1.vornameNachname === person2.vornameNachname && 76 | person1.kategorie === person2.kategorie && 77 | person1.medEinrichtung === person2.medEinrichtung && 78 | person1.standort === person2.standort && 79 | person1.geburtsdatum === person2.geburtsdatum && 80 | person1.strasse === person2.strasse && 81 | person1.ort === person2.ort && 82 | person1.plz === person2.plz && 83 | person1.landkreis === person2.landkreis && 84 | person1.land === person2.land && 85 | person1.telefon === person2.telefon && 86 | person1.mobile === person2.mobile && 87 | person1.mail === person2.mail && 88 | person1.quarantaene.start === person2.quarantaene.start && 89 | person1.quarantaene.ende === person2.quarantaene.ende && 90 | person1.kommentare === person2.kommentare && 91 | person1.haushalt === person2.haushalt && 92 | person1.telefonnotizen === person2.telefonnotizen && 93 | person1.doku === person2.doku && 94 | person1.ehemalsKp === person2.ehemalsKp && 95 | person1.letzterKontakt === person2.letzterKontakt && 96 | person1.erstKontaktErfolgtAm === person2.erstKontaktErfolgtAm && 97 | person1.endTelefonatErfolgtAm === person2.endTelefonatErfolgtAm && 98 | person1.aktuellerBearbeiter === person2.aktuellerBearbeiter && 99 | person1.id === person2.id && 100 | person1.id === person2.id && 101 | equalsKontakte(person1.kontakte,person2.kontakte) && 102 | equalsProben(person1.proben,person2.proben); 103 | } 104 | 105 | function equalsKontakte(kontakte1: Kontakt[], kontakte2:Kontakt[]) : Boolean { 106 | if(kontakte1.length !== kontakte2.length) return false; 107 | 108 | for (let i=0; i < kontakte1.length; i++) { 109 | if(kontakte1[i].kommentar !== kontakte2[i].kommentar && 110 | kontakte1[i].kontakt !== kontakte2[i].kontakt && 111 | kontakte1[i].kontaktdatum !== kontakte2[i].kontaktdatum && 112 | kontakte1[i].kontakttyp !== kontakte2[i].kontakttyp 113 | ) { 114 | return false; 115 | } 116 | } 117 | 118 | return true; 119 | } 120 | 121 | function equalsProben(proben1: Probe[], proben2:Probe[]) : Boolean { 122 | if(proben1.length !== proben2.length) return false; 123 | 124 | for (let i=0; i < proben1.length; i++) { 125 | if(proben1[i].ergebnis !== proben2[i].ergebnis && 126 | proben1[i].datum !== proben2[i].datum && 127 | proben1[i].kommentar !== proben2[i].kommentar 128 | ) { 129 | return false; 130 | } 131 | } 132 | 133 | return true; 134 | } 135 | 136 | export function mapPersonToPersonCallUpdate(person: Person) : PersonCallUpdate { 137 | var mappedPerson = {} as PersonCallUpdate; 138 | 139 | mappedPerson.id = person.id; 140 | mappedPerson.version = person.version; 141 | mappedPerson.doku = person.doku; 142 | mappedPerson.telefonnotizen = person.telefonnotizen; 143 | mappedPerson.quarantaene = person.quarantaene; 144 | mappedPerson.letzterKontakt = person.letzterKontakt; 145 | mappedPerson.endTelefonatErfolgtAm = person.endTelefonatErfolgtAm; 146 | mappedPerson.aktuellerBearbeiter = person.aktuellerBearbeiter; 147 | 148 | return mappedPerson; 149 | } -------------------------------------------------------------------------------- /frontend/src/types/SearchResult.ts: -------------------------------------------------------------------------------- 1 | import Person from "@/types/Person"; 2 | 3 | export default class SearchResult { 4 | persons: Person[]; 5 | pageNumber:number; 6 | pageSize:number; 7 | totalElements:number; 8 | 9 | constructor(persons:Person[], pageNumber:number, pageSize:number, totalElements:number) { 10 | this.persons = persons; 11 | this.pageNumber = pageNumber; 12 | this.pageSize = pageSize; 13 | this.totalElements = totalElements; 14 | } 15 | } -------------------------------------------------------------------------------- /frontend/src/views/BerichtView.vue: -------------------------------------------------------------------------------- 1 | 10 | 125 | 126 | 174 | -------------------------------------------------------------------------------- /frontend/src/views/EndgespraecheView.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 180 | -------------------------------------------------------------------------------- /frontend/src/views/Main.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 36 | 37 | -------------------------------------------------------------------------------- /frontend/src/views/PersonCreateView.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 88 | -------------------------------------------------------------------------------- /frontend/src/views/PersonEditView.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 124 | -------------------------------------------------------------------------------- /frontend/src/views/PersonReadView.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 92 | -------------------------------------------------------------------------------- /frontend/src/views/PersonSearchView.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 85 | -------------------------------------------------------------------------------- /frontend/tests/unit/example.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount, mount, createLocalVue } from '@vue/test-utils'; 2 | import Vuetify from 'vuetify'; 3 | import Vue from 'vue'; 4 | import TheSnackbar from '@/components/TheSnackbar.vue' 5 | 6 | const localVue = createLocalVue(); 7 | 8 | describe('TheSnackbar.vue', () => { 9 | 10 | let vuetify: any; 11 | 12 | beforeAll(() => { 13 | Vue.use(Vuetify); 14 | }); 15 | 16 | beforeEach(() => { 17 | vuetify = new Vuetify(); 18 | }); 19 | 20 | it('renders props.message when passed', () => { 21 | const message = 'Hello_World' 22 | const wrapper = shallowMount(TheSnackbar, { 23 | localVue, 24 | vuetify, 25 | propsData: {message: message} 26 | }) 27 | 28 | expect(wrapper.html()).toContain(message) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "types": [ 15 | "webpack-env", 16 | "node", 17 | "jest", 18 | "vuetify" 19 | ], 20 | "typeRoots": [ 21 | "./node_modules/@types", 22 | "./node_modules/vuetify/types" 23 | ], 24 | "paths": { 25 | "@/*": [ 26 | "src/*" 27 | ] 28 | }, 29 | "lib": [ 30 | "esnext", 31 | "dom", 32 | "dom.iterable", 33 | "scripthost" 34 | ] 35 | }, 36 | "include": [ 37 | "src/**/*.ts", 38 | "src/**/*.tsx", 39 | "src/**/*.vue", 40 | "tests/**/*.ts", 41 | "tests/**/*.tsx" 42 | ], 43 | "exclude": [ 44 | "node_modules" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /frontend/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transpileDependencies: ['vuetify'] 3 | } -------------------------------------------------------------------------------- /img/Anrufliste-Abschlussgespraeche.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/it-at-m/cove-frontend/22b7c544d4c3eb06d79583644df39a1e88927ea1/img/Anrufliste-Abschlussgespraeche.png -------------------------------------------------------------------------------- /img/Anrufliste-Indexpersonen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/it-at-m/cove-frontend/22b7c544d4c3eb06d79583644df39a1e88927ea1/img/Anrufliste-Indexpersonen.png -------------------------------------------------------------------------------- /img/Anrufplanung.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/it-at-m/cove-frontend/22b7c544d4c3eb06d79583644df39a1e88927ea1/img/Anrufplanung.png -------------------------------------------------------------------------------- /img/COVe_Bausteinsicht.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/it-at-m/cove-frontend/22b7c544d4c3eb06d79583644df39a1e88927ea1/img/COVe_Bausteinsicht.png -------------------------------------------------------------------------------- /img/COVe_Grafik.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/it-at-m/cove-frontend/22b7c544d4c3eb06d79583644df39a1e88927ea1/img/COVe_Grafik.jpg -------------------------------------------------------------------------------- /img/Datepicker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/it-at-m/cove-frontend/22b7c544d4c3eb06d79583644df39a1e88927ea1/img/Datepicker.png -------------------------------------------------------------------------------- /img/Popup-Telefonat-index-Ausschnitt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/it-at-m/cove-frontend/22b7c544d4c3eb06d79583644df39a1e88927ea1/img/Popup-Telefonat-index-Ausschnitt.png -------------------------------------------------------------------------------- /img/Preview-Kontakt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/it-at-m/cove-frontend/22b7c544d4c3eb06d79583644df39a1e88927ea1/img/Preview-Kontakt.png -------------------------------------------------------------------------------- /img/Reload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/it-at-m/cove-frontend/22b7c544d4c3eb06d79583644df39a1e88927ea1/img/Reload.png -------------------------------------------------------------------------------- /img/Start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/it-at-m/cove-frontend/22b7c544d4c3eb06d79583644df39a1e88927ea1/img/Start.png -------------------------------------------------------------------------------- /img/Suche.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/it-at-m/cove-frontend/22b7c544d4c3eb06d79583644df39a1e88927ea1/img/Suche.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | de.muenchen.cove 8 | cove-frontend 9 | 1.0-SNAPSHOT 10 | pom 11 | cove_frontend 12 | 13 | 14 | 15 | 16 | 17 | maven-scm-plugin 18 | 1.10.0 19 | 20 | RT-REL-${project.version} 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | frontend 29 | apigateway 30 | 31 | 32 | 33 | --------------------------------------------------------------------------------