From 3f948b7ebe8005732cbe79d7244e52b088f7ecfb Mon Sep 17 00:00:00 2001 From: Cedric Rische Date: Fri, 24 Mar 2023 22:32:45 +0100 Subject: [PATCH 01/22] Added frontend changes --- generator-templates/webclient/.public-env.ftl | 3 + generator-templates/webclient/EntityRest.ftl | 2 +- reacthookspring/webclient/app/.public-env | 3 + reacthookspring/webclient/app/Dockerfile | 28 ++++++ reacthookspring/webclient/app/env.sh | 29 +++++++ reacthookspring/webclient/app/package.json | 3 +- reacthookspring/webclient/app/src/app/App.jsx | 1 - .../commons/BearerProvider/BearerProvider.jsx | 29 +++++++ .../app/src/app/services/VersionRest.js | 2 +- reacthookspring/webclient/app/src/index.js | 22 ++++- template-config-backend.json | 85 +++++++++++++++++++ template-config-frontend.json | 61 +++++++++++++ template-config.json | 6 ++ 13 files changed, 267 insertions(+), 7 deletions(-) create mode 100644 generator-templates/webclient/.public-env.ftl create mode 100644 reacthookspring/webclient/app/.public-env create mode 100644 reacthookspring/webclient/app/Dockerfile create mode 100755 reacthookspring/webclient/app/env.sh create mode 100644 reacthookspring/webclient/app/src/app/commons/BearerProvider/BearerProvider.jsx create mode 100755 template-config-backend.json create mode 100755 template-config-frontend.json diff --git a/generator-templates/webclient/.public-env.ftl b/generator-templates/webclient/.public-env.ftl new file mode 100644 index 0000000..cb26fa7 --- /dev/null +++ b/generator-templates/webclient/.public-env.ftl @@ -0,0 +1,3 @@ +API_URL=http://127.0.0.1:8081/${app.baseName}/ +AUTH_AUTHORITY=http://localhost:8080/auth/realms/${app.baseName} +AUTH_CLIENT_ID=${app.baseName} \ No newline at end of file diff --git a/generator-templates/webclient/EntityRest.ftl b/generator-templates/webclient/EntityRest.ftl index afe12e7..f644c71 100755 --- a/generator-templates/webclient/EntityRest.ftl +++ b/generator-templates/webclient/EntityRest.ftl @@ -13,7 +13,7 @@ import axios from "axios"; class ${entity.name}Rest extends CrudRest { constructor() { - super(window.location.pathname + "api/${entity.name?lower_case}"); + super(window._env_.API_URL + "api/${entity.name?lower_case}"); } <#if entity.relationships??> <#list (entity.relationships) as relation> diff --git a/reacthookspring/webclient/app/.public-env b/reacthookspring/webclient/app/.public-env new file mode 100644 index 0000000..39699ae --- /dev/null +++ b/reacthookspring/webclient/app/.public-env @@ -0,0 +1,3 @@ +API_URL=http://127.0.0.1:8081/citydashboard/ +AUTH_AUTHORITY=http://localhost:8080/auth/realms/citydashboard +AUTH_CLIENT_ID=citydashboard \ No newline at end of file diff --git a/reacthookspring/webclient/app/Dockerfile b/reacthookspring/webclient/app/Dockerfile new file mode 100644 index 0000000..cab984e --- /dev/null +++ b/reacthookspring/webclient/app/Dockerfile @@ -0,0 +1,28 @@ +FROM nginx:alpine + +RUN apk update +RUN apk add --no-cache bash + +RUN rm /etc/nginx/conf.d/default.conf +COPY ./nginx/config.conf /etc/nginx/conf.d + +COPY ./build /usr/share/nginx/html/ +CMD ls -al /usr/share/nginx/html +EXPOSE 80 + +# Copy .env file and shell script to container +WORKDIR /usr/share/nginx/html +CMD ls -al /usr/share/nginx/html + +COPY ./env.sh . +COPY .env . + +CMD ls -al /usr/share/nginx/html + +# Make our shell script executable +RUN chmod +x env.sh + +CMD ["/bin/bash", "-c", "/usr/share/nginx/html/env.sh && nginx -g \"daemon off;\""] + + + diff --git a/reacthookspring/webclient/app/env.sh b/reacthookspring/webclient/app/env.sh new file mode 100755 index 0000000..d9875c2 --- /dev/null +++ b/reacthookspring/webclient/app/env.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# Recreate config file +rm -rf ./env-config.js +touch ./env-config.js + +# Add assignment +echo "window._env_ = {" >> ./env-config.js + +# Read each line in .env file +# Each line represents key=value pairs +while read -r line || [[ -n "$line" ]]; +do + # Split env variables by character `=` + if printf '%s\n' "$line" | grep -q -e '='; then + varname=$(printf '%s\n' "$line" | sed -e 's/=.*//') + varvalue=$(printf '%s\n' "$line" | sed -e 's/^[^=]*=//') + fi + + # Read value of current variable if exists as Environment variable + value=$(printf '%s\n' "${!varname}") + # Otherwise use value from .env file + [[ -z $value ]] && value=${varvalue} + + # Append configuration property to JS file + echo " $varname: \"$value\"," >> ./env-config.js +done < .public-env + +echo "}" >> ./env-config.js diff --git a/reacthookspring/webclient/app/package.json b/reacthookspring/webclient/app/package.json index e8d4af8..2cfbd10 100644 --- a/reacthookspring/webclient/app/package.json +++ b/reacthookspring/webclient/app/package.json @@ -26,7 +26,8 @@ "use-immer": "^0.7.0", "@date-io/moment": "^2.15.0", "@mui/x-date-pickers": "^5.0.1", - "moment": "^2.29.4" + "moment": "^2.29.4", + "oidc-react": "^3.0.3" }, "scripts": { "start": "react-scripts start", diff --git a/reacthookspring/webclient/app/src/app/App.jsx b/reacthookspring/webclient/app/src/app/App.jsx index f315477..6b5b286 100644 --- a/reacthookspring/webclient/app/src/app/App.jsx +++ b/reacthookspring/webclient/app/src/app/App.jsx @@ -2,7 +2,6 @@ import React from "react"; import MainContentRouter from "./MainContentRouter"; import {CssBaseline} from "@mui/material"; import {ErrorHandler} from "@starwit/react-starwit"; -import AppHeader from "./commons/appHeader/AppHeader"; import {useTranslation} from "react-i18next"; import {appItems} from "./AppConfig"; import Navigation from "./commons/navigation/Navigation"; diff --git a/reacthookspring/webclient/app/src/app/commons/BearerProvider/BearerProvider.jsx b/reacthookspring/webclient/app/src/app/commons/BearerProvider/BearerProvider.jsx new file mode 100644 index 0000000..6ff6346 --- /dev/null +++ b/reacthookspring/webclient/app/src/app/commons/BearerProvider/BearerProvider.jsx @@ -0,0 +1,29 @@ +import {useEffect, useState} from "react"; +import {useAuth} from "oidc-react"; +import {CircularProgress} from "@material-ui/core"; +import axios from "axios"; + +function BearerProvider(props) { + const [headerSet, setHeaderSet] = useState(false); + const auth = useAuth(); + const {children} = props; + + useEffect(() => { + if (auth?.userData?.access_token) { + if (!auth?.userData?.access_token) { + return; + } + axios.defaults.headers.common["Authorization"] = `Bearer ${auth.userData.access_token}`; + setHeaderSet(true); + } + }, [auth.userData?.access_token]); + + if (auth.isLoading || !headerSet){ + return ; + } + + return children; + +} + +export default BearerProvider; \ No newline at end of file diff --git a/reacthookspring/webclient/app/src/app/services/VersionRest.js b/reacthookspring/webclient/app/src/app/services/VersionRest.js index 2e028d2..feeaff6 100755 --- a/reacthookspring/webclient/app/src/app/services/VersionRest.js +++ b/reacthookspring/webclient/app/src/app/services/VersionRest.js @@ -2,7 +2,7 @@ import axios from "axios"; class VersionRest { constructor() { - this.baseUrl = window.location.pathname + "/monitoring"; + this.baseUrl = window._env_.API_URL + + "/monitoring"; } info = () => { diff --git a/reacthookspring/webclient/app/src/index.js b/reacthookspring/webclient/app/src/index.js index ea42528..9730d79 100755 --- a/reacthookspring/webclient/app/src/index.js +++ b/reacthookspring/webclient/app/src/index.js @@ -7,13 +7,29 @@ import {HashRouter as Router} from "react-router-dom"; import "./localization/i18n"; import {SnackbarProvider} from "notistack"; import MainTheme from "./app/assets/themes/MainTheme"; +import BearerProvider from "./commons/BearerProvider/BearerProvider"; +import {AuthProvider} from "oidc-react"; ReactDOM.render(( - - - + { + console.log("on signin"); + }} + > + + + + + + ), diff --git a/template-config-backend.json b/template-config-backend.json new file mode 100755 index 0000000..087b953 --- /dev/null +++ b/template-config-backend.json @@ -0,0 +1,85 @@ +{ + "templateName": "reacthookspring-backend", + "packagePlaceholder": "starwit", + "imageUrl": "https://raw.githubusercontent.com/starwit/reacthook-spring-template/main/screenshot.png", + "templateFiles": [ + { + "fileName": "${entity.name}Entity.java", + "templatePath": "persistence/Entity.ftl", + "targetPath": "persistence/src/main/java/de/${app.packageName?lower_case}/persistence/entity/", + "category": "ENTITY" + }, + { + "fileName": "${enumDef.name}.java", + "templatePath": "persistence/Enum.ftl", + "targetPath": "persistence/src/main/java/de/${app.packageName?lower_case}/persistence/entity/", + "category": "ENTITY" + }, + { + "fileName": "application.properties", + "templatePath": "application/properties.ftl", + "targetPath": "application/src/main/resources/", + "category": "ENTITY" + }, + { + "fileName": "application.properties", + "templatePath": "application/propertiestest.ftl", + "targetPath": "rest/src/test/resources/", + "category": "REST" + }, + { + "fileName": "${entity.name}Repository.java", + "templatePath": "persistence/EntityRepository.ftl", + "targetPath": "persistence/src/main/java/de/${app.packageName?lower_case}/persistence/repository/", + "category": "ENTITY" + }, + { + "fileName": "${entity.name}RepositoryTest.java", + "templatePath": "persistence/EntityRepositoryTest.ftl", + "targetPath": "persistence/src/test/java/de/${app.packageName?lower_case}/persistence/repository/", + "category": "ENTITY" + }, + { + "fileName": "V1_0__init.sql", + "templatePath": "flyway/V1_0__init.ftl", + "targetPath": "persistence/src/main/resources/db/migration/", + "category": "ENTITY" + }, + { + "fileName": "V1_0__init.sql", + "templatePath": "flyway/V1_0__init.ftl", + "targetPath": "persistence/src/test/resources/db/test/", + "category": "ENTITY" + }, + { + "fileName": "${entity.name}Service.java", + "templatePath": "service/Service.ftl", + "targetPath": "service/src/main/java/de/${app.packageName?lower_case}/service/impl/", + "category": "SERVICE" + }, + { + "fileName": "${entity.name}Controller.java", + "templatePath": "rest/Controller.ftl", + "targetPath": "rest/src/main/java/de/${app.packageName?lower_case}/rest/controller/", + "category": "REST" + }, + { + "fileName": "${entity.name}ControllerAcceptanceTest.java", + "templatePath": "rest/ControllerAcceptanceTest.ftl", + "targetPath": "rest/src/test/java/de/${app.packageName?lower_case}/rest/acceptance/", + "category": "REST" + }, + { + "fileName": "${entity.name}ControllerIntegrationTest.java", + "templatePath": "rest/ControllerIntegrationTest.ftl", + "targetPath": "rest/src/test/java/de/${app.packageName?lower_case}/rest/integration/", + "category": "REST" + }, + { + "fileName": ".public-env", + "templatePath": "webclient/.public-env.ftl", + "targetPath": "webclient/.public-env", + "category": "FRONTEND" + } + ] +} diff --git a/template-config-frontend.json b/template-config-frontend.json new file mode 100755 index 0000000..7ae92fb --- /dev/null +++ b/template-config-frontend.json @@ -0,0 +1,61 @@ +{ + "templateName": "reacthookspring-frontend", + "packagePlaceholder": "starwit", + "imageUrl": "https://raw.githubusercontent.com/starwit/reacthook-spring-template/main/screenshot.png", + "templateFiles": [ + { + "fileName": "AppConfig.js", + "templatePath": "webclient/AppConfig.ftl", + "targetPath": "webclient/app/src/app/", + "category": "FRONTEND" + }, + { + "fileName": "translation-en-EN.js", + "templatePath": "webclient/translationEnEN.ftl", + "targetPath": "webclient/app/src/localization/", + "category": "FRONTEND" + }, + { + "fileName": "${entity.name}Rest.js", + "templatePath": "webclient/EntityRest.ftl", + "targetPath": "webclient/app/src/app/services/", + "category": "FRONTEND" + }, + { + "fileName": "${entity.name}Modifier.js", + "templatePath": "webclient/EntityModifier.ftl", + "targetPath": "webclient/app/src/app/modifiers/", + "category": "FRONTEND" + }, + { + "fileName": "MainContentRouter.jsx", + "templatePath": "webclient/MainContentRouter.ftl", + "targetPath": "webclient/app/src/app/", + "category": "FRONTEND" + }, + { + "fileName": "${entity.name}Main.jsx", + "templatePath": "webclient/EntityMain.ftl", + "targetPath": "webclient/app/src/app/features/${entity.name?uncap_first}/", + "category": "FRONTEND" + }, + { + "fileName": "${entity.name}Detail.jsx", + "templatePath": "webclient/EntityDetail.ftl", + "targetPath": "webclient/app/src/app/features/${entity.name?uncap_first}/", + "category": "FRONTEND" + }, + { + "fileName": "${entity.name}Overview.jsx", + "templatePath": "webclient/EntityOverview.ftl", + "targetPath": "webclient/app/src/app/features/${entity.name?uncap_first}/", + "category": "FRONTEND" + }, + { + "fileName": ".public-env", + "templatePath": "webclient/.public-env.ftl", + "targetPath": "webclient/.public-env", + "category": "FRONTEND" + } + ] +} diff --git a/template-config.json b/template-config.json index c5697d8..24f6d54 100755 --- a/template-config.json +++ b/template-config.json @@ -122,6 +122,12 @@ "templatePath": "webclient/EntityOverview.ftl", "targetPath": "webclient/app/src/app/features/${entity.name?uncap_first}/", "category": "FRONTEND" + }, + { + "fileName": ".public-env", + "templatePath": "webclient/.public-env.ftl", + "targetPath": "webclient/.public-env", + "category": "FRONTEND" } ] } From 85129353f6e53e4feffe2a5a71a522289bfb083a Mon Sep 17 00:00:00 2001 From: Cedric Rische Date: Fri, 24 Mar 2023 22:41:08 +0100 Subject: [PATCH 02/22] Added backend resource Server configuration --- .../config/AuthenticationFacade.java | 13 +++ .../application/config/CorsConfig.java | 7 +- .../config/DisableSecurityConfig.java | 2 +- .../config/IAuthenticationFacade.java | 5 ++ .../application/config/KeycloakConfig.java | 76 ----------------- .../config/KeycloakResolverConfig.java | 14 ---- .../LocalRoleAuthenticationConverter.java | 41 +++++++++ .../config/LocalRoleAuthenticationToken.java | 30 +++++++ .../config/MethodSecurityConfig.java | 16 ---- .../config/ResourceServerConfig.java | 76 +++++++++++++++++ .../starwit/application/dto/PrincipalDto.java | 84 +++++++++++++++++++ 11 files changed, 254 insertions(+), 110 deletions(-) create mode 100644 reacthookspring/application/src/main/java/de/starwit/application/config/AuthenticationFacade.java create mode 100644 reacthookspring/application/src/main/java/de/starwit/application/config/IAuthenticationFacade.java delete mode 100644 reacthookspring/application/src/main/java/de/starwit/application/config/KeycloakConfig.java delete mode 100644 reacthookspring/application/src/main/java/de/starwit/application/config/KeycloakResolverConfig.java create mode 100644 reacthookspring/application/src/main/java/de/starwit/application/config/LocalRoleAuthenticationConverter.java create mode 100644 reacthookspring/application/src/main/java/de/starwit/application/config/LocalRoleAuthenticationToken.java delete mode 100644 reacthookspring/application/src/main/java/de/starwit/application/config/MethodSecurityConfig.java create mode 100644 reacthookspring/application/src/main/java/de/starwit/application/config/ResourceServerConfig.java create mode 100644 reacthookspring/application/src/main/java/de/starwit/application/dto/PrincipalDto.java diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/AuthenticationFacade.java b/reacthookspring/application/src/main/java/de/starwit/application/config/AuthenticationFacade.java new file mode 100644 index 0000000..c0e4f3c --- /dev/null +++ b/reacthookspring/application/src/main/java/de/starwit/application/config/AuthenticationFacade.java @@ -0,0 +1,13 @@ +package de.city.application.config; + +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +@Component +public class AuthenticationFacade implements IAuthenticationFacade { + + @Override + public LocalRoleAuthenticationToken getAuthentication() { + return (LocalRoleAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + } +} \ No newline at end of file diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/CorsConfig.java b/reacthookspring/application/src/main/java/de/starwit/application/config/CorsConfig.java index 50337fc..b26384d 100644 --- a/reacthookspring/application/src/main/java/de/starwit/application/config/CorsConfig.java +++ b/reacthookspring/application/src/main/java/de/starwit/application/config/CorsConfig.java @@ -1,4 +1,4 @@ -package de.starwit.application.config; +package de.city.application.config; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; @@ -13,9 +13,10 @@ public class CorsConfig implements WebMvcConfigurer { public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedMethods("*") - .allowedOrigins("http://localhost:8080", "http://localhost:8081") + .allowedOrigins("http://localhost:8080", "http://localhost:8081", "http://localhost:8083", + "http://localhost:3001", "http://localhost:3000", "*") .allowedHeaders("*") .allowCredentials(false); } -} \ No newline at end of file +} diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/DisableSecurityConfig.java b/reacthookspring/application/src/main/java/de/starwit/application/config/DisableSecurityConfig.java index 678bc42..b6a82a9 100644 --- a/reacthookspring/application/src/main/java/de/starwit/application/config/DisableSecurityConfig.java +++ b/reacthookspring/application/src/main/java/de/starwit/application/config/DisableSecurityConfig.java @@ -1,4 +1,4 @@ -package de.starwit.application.config; +package de.city.application.config; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/IAuthenticationFacade.java b/reacthookspring/application/src/main/java/de/starwit/application/config/IAuthenticationFacade.java new file mode 100644 index 0000000..10c8d45 --- /dev/null +++ b/reacthookspring/application/src/main/java/de/starwit/application/config/IAuthenticationFacade.java @@ -0,0 +1,5 @@ +package de.city.application.config; + +public interface IAuthenticationFacade { + LocalRoleAuthenticationToken getAuthentication(); +} diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/KeycloakConfig.java b/reacthookspring/application/src/main/java/de/starwit/application/config/KeycloakConfig.java deleted file mode 100644 index 76350a7..0000000 --- a/reacthookspring/application/src/main/java/de/starwit/application/config/KeycloakConfig.java +++ /dev/null @@ -1,76 +0,0 @@ -package de.starwit.application.config; - -import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider; -import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Profile; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper; -import org.springframework.security.core.session.SessionRegistryImpl; -import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy; -import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; -import org.springframework.security.web.csrf.CookieCsrfTokenRepository; - -@Profile("!dev") -@EnableWebSecurity -public class KeycloakConfig extends KeycloakWebSecurityConfigurerAdapter { - - private static final String[] AUTH_WHITELIST = { - // -- Swagger UI v2 - "/v2/api-docs", - "/swagger-resources", - "/swagger-resources/**", - "/configuration/ui", - "/configuration/security", - "/swagger-ui.html", - "/webjars/**", - // -- Swagger UI v3 (OpenAPI) - "/v3/api-docs/**", - "/swagger-ui/**" - // other public endpoints of your API may be appended to this array - }; - - @Autowired - public void configureGlobal(AuthenticationManagerBuilder auth) { - SimpleAuthorityMapper grantedAuthorityMapper = new SimpleAuthorityMapper(); - grantedAuthorityMapper.setPrefix("ROLE_"); - - KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider(); - keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(grantedAuthorityMapper); - auth.authenticationProvider(keycloakAuthenticationProvider); - } - - @Bean - @Override - protected SessionAuthenticationStrategy sessionAuthenticationStrategy() { - return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl()); - } - - @Override - public void configure(AuthenticationManagerBuilder auth) { - KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider(); - auth.authenticationProvider(keycloakAuthenticationProvider); - } - - @Override - protected void configure(HttpSecurity http) throws Exception { - super.configure(http); - - http - // .csrf().disable() - .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) - .and() - .authorizeRequests() - .antMatchers(AUTH_WHITELIST).permitAll() - .antMatchers("/logout.html").permitAll() - .antMatchers("/api/user/logout").permitAll() - .antMatchers("/**").hasAnyRole("admin", "user", "reader") - .anyRequest().authenticated() - .and() - .logout().logoutSuccessUrl("/myLogout.html") - .and().exceptionHandling().accessDeniedPage("/accessDenied.html"); - } -} diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/KeycloakResolverConfig.java b/reacthookspring/application/src/main/java/de/starwit/application/config/KeycloakResolverConfig.java deleted file mode 100644 index 4b121a7..0000000 --- a/reacthookspring/application/src/main/java/de/starwit/application/config/KeycloakResolverConfig.java +++ /dev/null @@ -1,14 +0,0 @@ -package de.starwit.application.config; - -import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class KeycloakResolverConfig { - - @Bean - public KeycloakSpringBootConfigResolver keycloakConfigResolver() { - return new KeycloakSpringBootConfigResolver(); - } -} diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/LocalRoleAuthenticationConverter.java b/reacthookspring/application/src/main/java/de/starwit/application/config/LocalRoleAuthenticationConverter.java new file mode 100644 index 0000000..642bc3f --- /dev/null +++ b/reacthookspring/application/src/main/java/de/starwit/application/config/LocalRoleAuthenticationConverter.java @@ -0,0 +1,41 @@ +package de.city.application.config; + +import de.city.application.dto.PrincipalDto; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class LocalRoleAuthenticationConverter implements Converter { + + + private final Logger logger = LoggerFactory.getLogger(LocalRoleAuthenticationConverter.class); + + public PrincipalDto parseToken(Jwt token) { + PrincipalDto principalDto = new PrincipalDto(); + + List roles = (List) token.getClaimAsMap("realm_access").get("roles"); + List scopes = Arrays.stream(token.getClaimAsString("scope").split(" ")).toList(); + //Warning: Roles must be prefixed with ROLE_ and Scopes with SCOPE_ when created in Keycloak + principalDto.setRoles(roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList())); + principalDto.setScopes(scopes.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList())); + + return principalDto; + } + + public AbstractAuthenticationToken convert(Jwt jwt) { + PrincipalDto principalDto = parseToken(jwt); + + this.logger.info("Saving updated known user information"); + List mergedAuthorities = principalDto.getRoles(); + mergedAuthorities.addAll(principalDto.getScopes()); + return new LocalRoleAuthenticationToken(principalDto, mergedAuthorities); + } +} diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/LocalRoleAuthenticationToken.java b/reacthookspring/application/src/main/java/de/starwit/application/config/LocalRoleAuthenticationToken.java new file mode 100644 index 0000000..71b75eb --- /dev/null +++ b/reacthookspring/application/src/main/java/de/starwit/application/config/LocalRoleAuthenticationToken.java @@ -0,0 +1,30 @@ +package de.city.application.config; + +import de.city.application.dto.PrincipalDto; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +public class LocalRoleAuthenticationToken extends AbstractAuthenticationToken { + + final private PrincipalDto user; + + public LocalRoleAuthenticationToken(PrincipalDto user, Collection authorities) { + super(authorities); + this.user = user; + super.setAuthenticated(true); + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return this.user; + } + + +} diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/MethodSecurityConfig.java b/reacthookspring/application/src/main/java/de/starwit/application/config/MethodSecurityConfig.java deleted file mode 100644 index da595d1..0000000 --- a/reacthookspring/application/src/main/java/de/starwit/application/config/MethodSecurityConfig.java +++ /dev/null @@ -1,16 +0,0 @@ -package de.starwit.application.config; - -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration; - -@Profile("!dev") -@Configuration -@EnableGlobalMethodSecurity( - prePostEnabled = true, - securedEnabled = true, - jsr250Enabled = true) -public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration { - -} diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/ResourceServerConfig.java b/reacthookspring/application/src/main/java/de/starwit/application/config/ResourceServerConfig.java new file mode 100644 index 0000000..c7ebe56 --- /dev/null +++ b/reacthookspring/application/src/main/java/de/starwit/application/config/ResourceServerConfig.java @@ -0,0 +1,76 @@ +package de.city.application.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Profile; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.ArrayList; +import java.util.List; + +@Profile("!dev") +@EnableWebSecurity +public class ResourceServerConfig extends WebSecurityConfigurerAdapter { + + private static final String[] AUTH_WHITELIST = { + // -- Swagger UI v2 + "/v2/api-docs", + "/swagger-resources", + "/swagger-resources/**", + "/configuration/ui", + "/configuration/security", + "/swagger-ui.html", + "/webjars/**", + // -- Swagger UI v3 (OpenAPI) + "/v3/api-docs/**", + "/swagger-ui/**" + // other public endpoints of your API may be appended to this array + }; + + @Bean + public LocalRoleAuthenticationConverter keycloakAuthenticationConverter() { + return new LocalRoleAuthenticationConverter(); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + + http + .cors().and() + // .csrf().disable() + .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + .and() + .authorizeRequests() + .antMatchers(AUTH_WHITELIST).permitAll() + .antMatchers("/logout.html").permitAll() + .antMatchers("/api/user/logout").permitAll() + .antMatchers("/**").hasAnyRole("admin", "user", "reader") + .anyRequest().authenticated() + .and() + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt + .jwtAuthenticationConverter(this.keycloakAuthenticationConverter()))); + } + + @Bean + CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + + List allowedOrigins = new ArrayList<>(); + allowedOrigins.add("*"); + + configuration.setAllowedOriginPatterns(allowedOrigins); + configuration.addAllowedHeader("*"); + configuration.addAllowedMethod("*"); + configuration.setAllowCredentials(true); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + +} diff --git a/reacthookspring/application/src/main/java/de/starwit/application/dto/PrincipalDto.java b/reacthookspring/application/src/main/java/de/starwit/application/dto/PrincipalDto.java new file mode 100644 index 0000000..7a84673 --- /dev/null +++ b/reacthookspring/application/src/main/java/de/starwit/application/dto/PrincipalDto.java @@ -0,0 +1,84 @@ +package de.city.application.dto; + +import org.springframework.security.core.GrantedAuthority; + +import java.util.ArrayList; +import java.util.List; + +public class PrincipalDto { + + private Boolean emailVerified; + + private String email; + private String familyName; + private String givenName; + private String userName; + private String userId; + private List roles = new ArrayList<>(); + + private List scopes = new ArrayList<>(); + + public Boolean getEmailVerified() { + return emailVerified; + } + + public void setEmailVerified(Boolean emailVerified) { + this.emailVerified = emailVerified; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getFamilyName() { + return familyName; + } + + public void setFamilyName(String familyName) { + this.familyName = familyName; + } + + public String getGivenName() { + return givenName; + } + + public void setGivenName(String givenName) { + this.givenName = givenName; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } + + public List getScopes() { + return scopes; + } + + public void setScopes(List scopes) { + this.scopes = scopes; + } +} From c0afa7f0b8b0b0087ea3cd35b3404340da99fed6 Mon Sep 17 00:00:00 2001 From: Cedric Rische Date: Fri, 24 Mar 2023 22:45:03 +0100 Subject: [PATCH 03/22] Updated poms to remove oauth2 client and added resource-server deps removed keycloak deps --- reacthookspring/application/pom.xml | 6 +----- reacthookspring/pom.xml | 9 +-------- reacthookspring/rest/pom.xml | 4 ---- 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/reacthookspring/application/pom.xml b/reacthookspring/application/pom.xml index 40b1f7e..e664cb6 100644 --- a/reacthookspring/application/pom.xml +++ b/reacthookspring/application/pom.xml @@ -19,17 +19,13 @@ de.starwit persistence - - org.keycloak - keycloak-spring-boot-starter - org.springframework.boot spring-boot-starter-actuator org.springframework.boot - spring-boot-starter-oauth2-client + spring-boot-starter-oauth2-resource-server org.springframework.boot diff --git a/reacthookspring/pom.xml b/reacthookspring/pom.xml index 21cfce6..31cd1a9 100644 --- a/reacthookspring/pom.xml +++ b/reacthookspring/pom.xml @@ -38,13 +38,6 @@ spring-core ${spring-version} - - org.keycloak.bom - keycloak-adapter-bom - 15.0.2 - pom - import - org.springframework.boot spring-boot-starter @@ -58,7 +51,7 @@ org.springframework.boot - spring-boot-starter-oauth2-client + spring-boot-starter-oauth2-resource-server ${spring-boot-version} diff --git a/reacthookspring/rest/pom.xml b/reacthookspring/rest/pom.xml index 8edae42..69f239f 100644 --- a/reacthookspring/rest/pom.xml +++ b/reacthookspring/rest/pom.xml @@ -15,10 +15,6 @@ org.springframework.security spring-security-web - - org.keycloak - keycloak-spring-security-adapter - org.springframework.boot spring-boot-starter-validation From 671c5dcddf0c72dd38c3c0e6d59f40142756b58c Mon Sep 17 00:00:00 2001 From: Cedric Rische Date: Fri, 24 Mar 2023 23:16:24 +0100 Subject: [PATCH 04/22] Rolled back reacthookspring --- .../webclient/EntityRestStandalone.ftl | 33 ++ .../.github/workflows/buildpublish.yml | 59 +++ .../.github/workflows/codeql-analysis.yml | 70 +++ .../.github/workflows/createRelease.yml | 102 ++++ .../.github/workflows/getReleaseType.sh | 8 + .../.github/workflows/readme.md | 12 + reacthookspring-backend/.gitignore | 55 +++ reacthookspring-backend/README.MD | 94 ++++ .../application/.gitignore | 29 ++ reacthookspring-backend/application/pom.xml | 114 +++++ .../de/starwit/application/Application.java | 31 ++ .../config/AuthenticationFacade.java | 0 .../application/config/CorsConfig.java | 22 + .../config/DisableSecurityConfig.java | 19 + .../config/IAuthenticationFacade.java | 0 .../LocalRoleAuthenticationConverter.java | 0 .../config/LocalRoleAuthenticationToken.java | 0 .../config/ResourceServerConfig.java | 0 .../starwit/application/dto/PrincipalDto.java | 0 .../src/main/resources/application.properties | 57 +++ .../application/src/main/resources/banner.txt | 17 + .../application/src/main/resources/log4j2.xml | 34 ++ .../application/src/test/resources/log4j2.xml | 6 + .../deployment/helm/.gitignore | 1 + .../helm/reacthook-spring/.gitignore | 6 + .../helm/reacthook-spring/.helmignore | 26 + .../helm/reacthook-spring/Chart.yaml | 28 ++ .../helm/reacthook-spring/Readme.md | 30 ++ .../reacthook-spring/mariadb-parameter.md | 464 ++++++++++++++++++ .../helm/reacthook-spring/templates/NOTES.txt | 22 + .../reacthook-spring/templates/_helpers.tpl | 62 +++ .../templates/deployment.yaml | 84 ++++ .../templates/githubPullSecret.yaml | 16 + .../helm/reacthook-spring/templates/hpa.yaml | 28 ++ .../reacthook-spring/templates/ingress.yaml | 39 ++ .../templates/oauthSecret.yaml | 12 + .../helm/reacthook-spring/templates/pv.yaml | 15 + .../reacthook-spring/templates/service.yaml | 15 + .../templates/serviceaccount.yaml | 12 + .../templates/tests/test-connection.yaml | 15 + .../values-template4private.yaml | 15 + .../helm/reacthook-spring/values.yaml | 99 ++++ .../deployment/https/docker-compose.yml | 29 ++ .../deployment/https/env-docker-compose.yml | 72 +++ .../deployment/https/env.sh | 9 + .../deployment/https/imports/realm.json | 131 +++++ .../deployment/https/init-letsencrypt.sh | 79 +++ .../https/nginx-init-letsencrypt.conf | 65 +++ .../deployment/https/nginx.conf | 100 ++++ .../https/reacthookspring-docker-compose.yml | 48 ++ .../deployment/https/readme.md | 39 ++ .../deployment/keycloak/imports/realm.json | 131 +++++ .../deployment/keycloak/local-test-users.json | 49 ++ .../deployment/keycloak/readme.md | 19 + .../deployment/localenv-docker-compose.yml | 77 +++ .../deployment/mysqllocal-docker-compose.yml | 33 ++ reacthookspring-backend/dockerfile | 7 + .../persistence/.gitignore | 26 + reacthookspring-backend/persistence/pom.xml | 59 +++ .../persistence/PersistenceApplication.java | 13 + .../DatabasePhysicalNamingStrategy.java | 42 ++ .../converter/ListToStringConverter.java | 25 + .../persistence/entity/AbstractEntity.java | 37 ++ .../exception/NotificationException.java | 36 ++ .../serializer/ZonedDateTimeDeserializer.java | 16 + .../serializer/ZonedDateTimeSerializer.java | 19 + .../src/main/resources/application.properties | 4 + .../persistence/src/main/resources/banner.txt | 6 + .../resources/db/migration/V0_0__empty.sql | 0 .../src/test/resources/application.properties | 6 + .../test/resources/db/test/V0_0__empty.sql | 0 reacthookspring-backend/pom.xml | 211 ++++++++ reacthookspring-backend/rest/.gitignore | 26 + reacthookspring-backend/rest/pom.xml | 56 +++ .../java/de/starwit/allowedroles/IsAdmin.java | 15 + .../de/starwit/allowedroles/IsReader.java | 15 + .../java/de/starwit/allowedroles/IsUser.java | 15 + .../java/de/starwit/rest/RestApplication.java | 19 + .../rest/controller/UserController.java | 27 + .../exception/ControllerExceptionHandler.java | 147 ++++++ .../rest/exception/NotificationDto.java | 31 ++ .../rest/exception/WrongAppIdException.java | 9 + .../src/main/resources/application.properties | 6 + .../rest/src/main/resources/banner.txt | 5 + .../AbstractControllerAcceptanceTest.java | 164 +++++++ .../AbstractControllerIntegrationTest.java | 78 +++ reacthookspring-backend/service/.gitignore | 25 + reacthookspring-backend/service/pom.xml | 44 ++ .../starwit/service/ServiceApplication.java | 13 + .../service/impl/ServiceInterface.java | 46 ++ .../src/main/resources/application.properties | 1 + .../service/src/main/resources/banner.txt | 5 + reacthookspring-frontend/.env.development | 2 + reacthookspring-frontend/.eslintrc.json | 127 +++++ reacthookspring-frontend/.gitignore | 23 + .../.public-env | 0 .../Dockerfile | 0 .../app => reacthookspring-frontend}/env.sh | 0 reacthookspring-frontend/package.json | 59 +++ reacthookspring-frontend/public/favicon.ico | Bin 0 -> 4189 bytes reacthookspring-frontend/public/index.html | 44 ++ reacthookspring-frontend/public/manifest.json | 15 + reacthookspring-frontend/src/app/App.jsx | 26 + reacthookspring-frontend/src/app/App.test.js | 9 + reacthookspring-frontend/src/app/AppConfig.js | 3 + .../src/app/MainContentRouter.jsx | 17 + .../app/assets/icons/icon-camera-green.svg | 27 + .../src/app/assets/icons/icon-device.svg | 17 + .../app/assets/icons/icon-hamburger-menu.svg | 21 + .../src/app/assets/icons/icon-zoom-in.svg | 31 ++ .../src/app/assets/icons/icon-zoom-out.svg | 33 ++ .../app/assets/images/DefaultAppTemplate.jpg | Bin 0 -> 30402 bytes .../src/app/assets/images/logo-black.png | Bin 0 -> 5555 bytes .../src/app/assets/images/logo-color.png | Bin 0 -> 4189 bytes .../src/app/assets/images/logo-white.png | Bin 0 -> 11269 bytes .../src/app/assets/styles/HeaderStyles.js | 73 +++ .../src/app/assets/themes/ColorTheme.js | 63 +++ .../src/app/assets/themes/ComponentTheme.js | 30 ++ .../src/app/assets/themes/MainTheme.jsx | 13 + .../commons/BearerProvider/BearerProvider.jsx | 29 ++ .../src/app/commons/appHeader/AppHeader.jsx | 38 ++ .../src/app/commons/navigation/Navigation.jsx | 45 ++ .../commons/navigation/NavigationStyles.js | 8 + .../navigation/appHeader/AppHeader.jsx | 37 ++ .../sidebarNavigation/SidebarNavigation.jsx | 72 +++ .../src/app/features/home/Home.jsx | 18 + .../src/app/modifiers/HomeModifier.js | 40 ++ .../src/app/services/CrudRest.js | 29 ++ .../src/app/services/VersionRest.js | 13 + reacthookspring-frontend/src/index.css | 18 + reacthookspring-frontend/src/index.js | 42 ++ .../src/localization/i18n.js | 27 + .../src/localization/translation-en-EN.js | 6 + reacthookspring-frontend/src/serviceWorker.js | 135 +++++ reacthookspring/application/pom.xml | 6 +- .../application/config/CorsConfig.java | 7 +- .../config/DisableSecurityConfig.java | 2 +- .../application/config/KeycloakConfig.java | 76 +++ .../config/KeycloakResolverConfig.java | 14 + .../config/MethodSecurityConfig.java | 16 + reacthookspring/pom.xml | 9 +- reacthookspring/rest/pom.xml | 4 + reacthookspring/webclient/app/package.json | 1 - template-config-frontend.json | 18 +- 144 files changed, 5212 insertions(+), 17 deletions(-) create mode 100755 generator-templates/webclient/EntityRestStandalone.ftl create mode 100644 reacthookspring-backend/.github/workflows/buildpublish.yml create mode 100644 reacthookspring-backend/.github/workflows/codeql-analysis.yml create mode 100644 reacthookspring-backend/.github/workflows/createRelease.yml create mode 100644 reacthookspring-backend/.github/workflows/getReleaseType.sh create mode 100644 reacthookspring-backend/.github/workflows/readme.md create mode 100755 reacthookspring-backend/.gitignore create mode 100755 reacthookspring-backend/README.MD create mode 100644 reacthookspring-backend/application/.gitignore create mode 100644 reacthookspring-backend/application/pom.xml create mode 100644 reacthookspring-backend/application/src/main/java/de/starwit/application/Application.java rename {reacthookspring => reacthookspring-backend}/application/src/main/java/de/starwit/application/config/AuthenticationFacade.java (100%) create mode 100644 reacthookspring-backend/application/src/main/java/de/starwit/application/config/CorsConfig.java create mode 100644 reacthookspring-backend/application/src/main/java/de/starwit/application/config/DisableSecurityConfig.java rename {reacthookspring => reacthookspring-backend}/application/src/main/java/de/starwit/application/config/IAuthenticationFacade.java (100%) rename {reacthookspring => reacthookspring-backend}/application/src/main/java/de/starwit/application/config/LocalRoleAuthenticationConverter.java (100%) rename {reacthookspring => reacthookspring-backend}/application/src/main/java/de/starwit/application/config/LocalRoleAuthenticationToken.java (100%) rename {reacthookspring => reacthookspring-backend}/application/src/main/java/de/starwit/application/config/ResourceServerConfig.java (100%) rename {reacthookspring => reacthookspring-backend}/application/src/main/java/de/starwit/application/dto/PrincipalDto.java (100%) create mode 100644 reacthookspring-backend/application/src/main/resources/application.properties create mode 100644 reacthookspring-backend/application/src/main/resources/banner.txt create mode 100644 reacthookspring-backend/application/src/main/resources/log4j2.xml create mode 100644 reacthookspring-backend/application/src/test/resources/log4j2.xml create mode 100644 reacthookspring-backend/deployment/helm/.gitignore create mode 100644 reacthookspring-backend/deployment/helm/reacthook-spring/.gitignore create mode 100644 reacthookspring-backend/deployment/helm/reacthook-spring/.helmignore create mode 100644 reacthookspring-backend/deployment/helm/reacthook-spring/Chart.yaml create mode 100644 reacthookspring-backend/deployment/helm/reacthook-spring/Readme.md create mode 100644 reacthookspring-backend/deployment/helm/reacthook-spring/mariadb-parameter.md create mode 100644 reacthookspring-backend/deployment/helm/reacthook-spring/templates/NOTES.txt create mode 100644 reacthookspring-backend/deployment/helm/reacthook-spring/templates/_helpers.tpl create mode 100644 reacthookspring-backend/deployment/helm/reacthook-spring/templates/deployment.yaml create mode 100644 reacthookspring-backend/deployment/helm/reacthook-spring/templates/githubPullSecret.yaml create mode 100644 reacthookspring-backend/deployment/helm/reacthook-spring/templates/hpa.yaml create mode 100644 reacthookspring-backend/deployment/helm/reacthook-spring/templates/ingress.yaml create mode 100644 reacthookspring-backend/deployment/helm/reacthook-spring/templates/oauthSecret.yaml create mode 100644 reacthookspring-backend/deployment/helm/reacthook-spring/templates/pv.yaml create mode 100644 reacthookspring-backend/deployment/helm/reacthook-spring/templates/service.yaml create mode 100644 reacthookspring-backend/deployment/helm/reacthook-spring/templates/serviceaccount.yaml create mode 100644 reacthookspring-backend/deployment/helm/reacthook-spring/templates/tests/test-connection.yaml create mode 100644 reacthookspring-backend/deployment/helm/reacthook-spring/values-template4private.yaml create mode 100644 reacthookspring-backend/deployment/helm/reacthook-spring/values.yaml create mode 100644 reacthookspring-backend/deployment/https/docker-compose.yml create mode 100644 reacthookspring-backend/deployment/https/env-docker-compose.yml create mode 100644 reacthookspring-backend/deployment/https/env.sh create mode 100644 reacthookspring-backend/deployment/https/imports/realm.json create mode 100644 reacthookspring-backend/deployment/https/init-letsencrypt.sh create mode 100644 reacthookspring-backend/deployment/https/nginx-init-letsencrypt.conf create mode 100644 reacthookspring-backend/deployment/https/nginx.conf create mode 100644 reacthookspring-backend/deployment/https/reacthookspring-docker-compose.yml create mode 100644 reacthookspring-backend/deployment/https/readme.md create mode 100644 reacthookspring-backend/deployment/keycloak/imports/realm.json create mode 100644 reacthookspring-backend/deployment/keycloak/local-test-users.json create mode 100644 reacthookspring-backend/deployment/keycloak/readme.md create mode 100644 reacthookspring-backend/deployment/localenv-docker-compose.yml create mode 100644 reacthookspring-backend/deployment/mysqllocal-docker-compose.yml create mode 100755 reacthookspring-backend/dockerfile create mode 100755 reacthookspring-backend/persistence/.gitignore create mode 100644 reacthookspring-backend/persistence/pom.xml create mode 100755 reacthookspring-backend/persistence/src/main/java/de/starwit/persistence/PersistenceApplication.java create mode 100644 reacthookspring-backend/persistence/src/main/java/de/starwit/persistence/config/DatabasePhysicalNamingStrategy.java create mode 100644 reacthookspring-backend/persistence/src/main/java/de/starwit/persistence/converter/ListToStringConverter.java create mode 100755 reacthookspring-backend/persistence/src/main/java/de/starwit/persistence/entity/AbstractEntity.java create mode 100755 reacthookspring-backend/persistence/src/main/java/de/starwit/persistence/exception/NotificationException.java create mode 100644 reacthookspring-backend/persistence/src/main/java/de/starwit/persistence/serializer/ZonedDateTimeDeserializer.java create mode 100644 reacthookspring-backend/persistence/src/main/java/de/starwit/persistence/serializer/ZonedDateTimeSerializer.java create mode 100644 reacthookspring-backend/persistence/src/main/resources/application.properties create mode 100755 reacthookspring-backend/persistence/src/main/resources/banner.txt create mode 100644 reacthookspring-backend/persistence/src/main/resources/db/migration/V0_0__empty.sql create mode 100644 reacthookspring-backend/persistence/src/test/resources/application.properties create mode 100644 reacthookspring-backend/persistence/src/test/resources/db/test/V0_0__empty.sql create mode 100644 reacthookspring-backend/pom.xml create mode 100755 reacthookspring-backend/rest/.gitignore create mode 100644 reacthookspring-backend/rest/pom.xml create mode 100644 reacthookspring-backend/rest/src/main/java/de/starwit/allowedroles/IsAdmin.java create mode 100644 reacthookspring-backend/rest/src/main/java/de/starwit/allowedroles/IsReader.java create mode 100644 reacthookspring-backend/rest/src/main/java/de/starwit/allowedroles/IsUser.java create mode 100644 reacthookspring-backend/rest/src/main/java/de/starwit/rest/RestApplication.java create mode 100644 reacthookspring-backend/rest/src/main/java/de/starwit/rest/controller/UserController.java create mode 100644 reacthookspring-backend/rest/src/main/java/de/starwit/rest/exception/ControllerExceptionHandler.java create mode 100644 reacthookspring-backend/rest/src/main/java/de/starwit/rest/exception/NotificationDto.java create mode 100644 reacthookspring-backend/rest/src/main/java/de/starwit/rest/exception/WrongAppIdException.java create mode 100755 reacthookspring-backend/rest/src/main/resources/application.properties create mode 100755 reacthookspring-backend/rest/src/main/resources/banner.txt create mode 100644 reacthookspring-backend/rest/src/test/java/de/starwit/rest/acceptance/AbstractControllerAcceptanceTest.java create mode 100644 reacthookspring-backend/rest/src/test/java/de/starwit/rest/integration/AbstractControllerIntegrationTest.java create mode 100755 reacthookspring-backend/service/.gitignore create mode 100644 reacthookspring-backend/service/pom.xml create mode 100755 reacthookspring-backend/service/src/main/java/de/starwit/service/ServiceApplication.java create mode 100644 reacthookspring-backend/service/src/main/java/de/starwit/service/impl/ServiceInterface.java create mode 100755 reacthookspring-backend/service/src/main/resources/application.properties create mode 100755 reacthookspring-backend/service/src/main/resources/banner.txt create mode 100755 reacthookspring-frontend/.env.development create mode 100644 reacthookspring-frontend/.eslintrc.json create mode 100755 reacthookspring-frontend/.gitignore rename {reacthookspring/webclient/app => reacthookspring-frontend}/.public-env (100%) rename {reacthookspring/webclient/app => reacthookspring-frontend}/Dockerfile (100%) rename {reacthookspring/webclient/app => reacthookspring-frontend}/env.sh (100%) create mode 100644 reacthookspring-frontend/package.json create mode 100644 reacthookspring-frontend/public/favicon.ico create mode 100644 reacthookspring-frontend/public/index.html create mode 100755 reacthookspring-frontend/public/manifest.json create mode 100644 reacthookspring-frontend/src/app/App.jsx create mode 100755 reacthookspring-frontend/src/app/App.test.js create mode 100644 reacthookspring-frontend/src/app/AppConfig.js create mode 100644 reacthookspring-frontend/src/app/MainContentRouter.jsx create mode 100755 reacthookspring-frontend/src/app/assets/icons/icon-camera-green.svg create mode 100755 reacthookspring-frontend/src/app/assets/icons/icon-device.svg create mode 100755 reacthookspring-frontend/src/app/assets/icons/icon-hamburger-menu.svg create mode 100755 reacthookspring-frontend/src/app/assets/icons/icon-zoom-in.svg create mode 100755 reacthookspring-frontend/src/app/assets/icons/icon-zoom-out.svg create mode 100644 reacthookspring-frontend/src/app/assets/images/DefaultAppTemplate.jpg create mode 100644 reacthookspring-frontend/src/app/assets/images/logo-black.png create mode 100644 reacthookspring-frontend/src/app/assets/images/logo-color.png create mode 100644 reacthookspring-frontend/src/app/assets/images/logo-white.png create mode 100644 reacthookspring-frontend/src/app/assets/styles/HeaderStyles.js create mode 100644 reacthookspring-frontend/src/app/assets/themes/ColorTheme.js create mode 100644 reacthookspring-frontend/src/app/assets/themes/ComponentTheme.js create mode 100644 reacthookspring-frontend/src/app/assets/themes/MainTheme.jsx create mode 100644 reacthookspring-frontend/src/app/commons/BearerProvider/BearerProvider.jsx create mode 100644 reacthookspring-frontend/src/app/commons/appHeader/AppHeader.jsx create mode 100644 reacthookspring-frontend/src/app/commons/navigation/Navigation.jsx create mode 100644 reacthookspring-frontend/src/app/commons/navigation/NavigationStyles.js create mode 100644 reacthookspring-frontend/src/app/commons/navigation/appHeader/AppHeader.jsx create mode 100644 reacthookspring-frontend/src/app/commons/navigation/sidebarNavigation/SidebarNavigation.jsx create mode 100644 reacthookspring-frontend/src/app/features/home/Home.jsx create mode 100644 reacthookspring-frontend/src/app/modifiers/HomeModifier.js create mode 100755 reacthookspring-frontend/src/app/services/CrudRest.js create mode 100755 reacthookspring-frontend/src/app/services/VersionRest.js create mode 100755 reacthookspring-frontend/src/index.css create mode 100755 reacthookspring-frontend/src/index.js create mode 100755 reacthookspring-frontend/src/localization/i18n.js create mode 100644 reacthookspring-frontend/src/localization/translation-en-EN.js create mode 100755 reacthookspring-frontend/src/serviceWorker.js create mode 100644 reacthookspring/application/src/main/java/de/starwit/application/config/KeycloakConfig.java create mode 100644 reacthookspring/application/src/main/java/de/starwit/application/config/KeycloakResolverConfig.java create mode 100644 reacthookspring/application/src/main/java/de/starwit/application/config/MethodSecurityConfig.java diff --git a/generator-templates/webclient/EntityRestStandalone.ftl b/generator-templates/webclient/EntityRestStandalone.ftl new file mode 100755 index 0000000..f644c71 --- /dev/null +++ b/generator-templates/webclient/EntityRestStandalone.ftl @@ -0,0 +1,33 @@ +<#assign addRestCalls = false> +<#if entity.relationships??> + <#list (entity.relationships) as relation> + <#if relation.relationshipType == "OneToOne" || relation.relationshipType == "ManyToOne"> + <#assign addRestCalls = true> + + + +import CrudRest from "./CrudRest"; +<#if addRestCalls> +import axios from "axios"; + + +class ${entity.name}Rest extends CrudRest { + constructor() { + super(window._env_.API_URL + "api/${entity.name?lower_case}"); + } +<#if entity.relationships??> + <#list (entity.relationships) as relation> + <#if relation.relationshipType == "OneToOne" || relation.relationshipType == "ManyToOne"> + + findAllWithout${relation.relationshipName?cap_first}(selected) { + if (isNaN(selected)) { + return axios.get(this.baseUrl + "/find-without-${relation.relationshipName}/"); + } else { + return axios.get(this.baseUrl + "/find-without-other-${relation.relationshipName}/" + selected); + } + } + + + +} +export default ${entity.name}Rest; diff --git a/reacthookspring-backend/.github/workflows/buildpublish.yml b/reacthookspring-backend/.github/workflows/buildpublish.yml new file mode 100644 index 0000000..4fce7e7 --- /dev/null +++ b/reacthookspring-backend/.github/workflows/buildpublish.yml @@ -0,0 +1,59 @@ +name: Build and Publish + +on: + pull_request: + branches: [ develop ] + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + name: "Building project with Java 17" + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 17 + uses: actions/setup-java@v2 + with: + distribution: 'zulu' + java-version: 17 + - name: npm install + run: | + if [ -d "webclient/app" ]; then + cd webclient/app + npm install --legacy-peer-deps + fi + - name: Build with Maven + run: mvn clean -B package -P frontend --file pom.xml + env: + CI: false + - name: Upload Maven build artifact + uses: actions/upload-artifact@v1 + with: + name: artifact + path: application/target/application-0.0.1-SNAPSHOT.jar + - name: Log in to the Container registry + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/reacthookspring-backend/.github/workflows/codeql-analysis.yml b/reacthookspring-backend/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..a90147c --- /dev/null +++ b/reacthookspring-backend/.github/workflows/codeql-analysis.yml @@ -0,0 +1,70 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ develop] + pull_request: + # The branches below must be a subset of the branches above + branches: [ develop ] + schedule: + - cron: '23 22 * * 2' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'java', 'javascript' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://git.io/codeql-language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/reacthookspring-backend/.github/workflows/createRelease.yml b/reacthookspring-backend/.github/workflows/createRelease.yml new file mode 100644 index 0000000..ef74841 --- /dev/null +++ b/reacthookspring-backend/.github/workflows/createRelease.yml @@ -0,0 +1,102 @@ + +name: Create release and tag + +on: + push: + branches: [ main ] + workflow_dispatch: + inputs: + release-type: # id of input + description: "prerelease, patch, minor or major" + required: true + default: "prerelease" + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + release-type: ${{ github.event.inputs.release-type }} + +jobs: + build: + name: "Creating changelog and release" + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: '0' + + - name: generate tag and release body + run: | + RELEASE_TYPE=$(sh ${{ github.workspace }}/.github/workflows/getReleaseType.sh ${{ env.release-type }}) + git config user.name github-reacthookspring + git config user.email code@starwit.de + npx standard-version -i CHANGELOG.md --release-as $RELEASE_TYPE + + - name: Read CHANGELOG.md + id: package + uses: juliangruber/read-file-action@v1 + with: + path: ./CHANGELOG.md + - name: Echo CHANGELOG.md + run: echo '${{ steps.package.outputs.content }}' + + - name: publish tag + id: publish_tag + run: | + git push --follow-tags + echo ::set-output name=tag_name::$(git describe HEAD --abbrev=0) + + - name: create release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + release_name: Release ${{ steps.publish_tag.outputs.tag_name }} + tag_name: ${{ steps.publish_tag.outputs.tag_name }} + body_path: CHANGELOG.md + + - name: Set up JDK 17 + uses: actions/setup-java@v2 + with: + distribution: 'zulu' + java-version: 17 + - name: npm install + run: | + if [ -d "webclient/app" ]; then + cd webclient/app + npm install --legacy-peer-deps + fi + - name: Build with Maven + run: mvn -B package -P frontend --file pom.xml + env: + CI: false + - name: Upload Maven build artifact + uses: actions/upload-artifact@v1 + with: + name: artifact + path: application/target/application-0.0.1-SNAPSHOT.jar + + - name: Log in to the Container registry + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc + with: + context: . + push: true + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.publish_tag.outputs.tag_name }} + + - name: Build docker image with tag version and push to dockerhub + uses: docker/build-push-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + repository: starwitorg/reacthookspring + tags: ${{ steps.publish_tag.outputs.tag_name }} + path: . + dockerfile: ./Dockerfile diff --git a/reacthookspring-backend/.github/workflows/getReleaseType.sh b/reacthookspring-backend/.github/workflows/getReleaseType.sh new file mode 100644 index 0000000..49ee94c --- /dev/null +++ b/reacthookspring-backend/.github/workflows/getReleaseType.sh @@ -0,0 +1,8 @@ +#!/bin/bash +case "$1" in + prerelease | patch | minor | major ) + TYPE=$1 ;; + * ) + TYPE='patch' ;; + esac +echo "$TYPE" diff --git a/reacthookspring-backend/.github/workflows/readme.md b/reacthookspring-backend/.github/workflows/readme.md new file mode 100644 index 0000000..8970cdc --- /dev/null +++ b/reacthookspring-backend/.github/workflows/readme.md @@ -0,0 +1,12 @@ +# Workflow Description + +Workflows for software quality check, build, packaging, and release creation were established: + +* **buildpublish.yml (Build and Publish)** to test docker builds and pushes on your own branch. +* **createRelease.yml (Create release and tag)** + * use `prerelease` to check release creation or create a release for testing + * use `major` or `minor` and execute script on main branch: to build additional major and minor release + * :bangbang: If you are executing patch or higher release on another branch than main, release numbering might get broken. +* **codeql-analysis.yml:** to ensure code quality, the code is checked for build errors, linting and security issues + +Code-analysis and build publish scripts are executed at each pull request change to develop. Code-analysis is additionally executed each Sunday at 23:30. diff --git a/reacthookspring-backend/.gitignore b/reacthookspring-backend/.gitignore new file mode 100755 index 0000000..1a7f0be --- /dev/null +++ b/reacthookspring-backend/.gitignore @@ -0,0 +1,55 @@ +HELP.md +/target/ +!.mvn/wrapper/maven-wrapper.jar +*.log + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ + +### webclient (npm builds) ### +application/src/main/resources/static + +# dependencies +webclient/src/main/app/node_modules +webclient/src/main/app/.pnp +webclient/src/main/app/.pnp.js + +# testing +webclient/src/main/app/coverage + +# production +webclient/src/main/app/build + +# misc +webclient/src/main/app/.DS_Store +webclient/src/main/app/.env.local +webclient/src/main/app/.env.development.local +webclient/src/main/app/.env.test.local +webclient/src/main/app/.env.production.local + +webclient/src/main/app/npm-debug.log* +webclient/src/main/app/yarn-debug.log* +webclient/src/main/app/yarn-error.log* + +webclient/src/main/app/package-lock.json +test.log diff --git a/reacthookspring-backend/README.MD b/reacthookspring-backend/README.MD new file mode 100755 index 0000000..ad11541 --- /dev/null +++ b/reacthookspring-backend/README.MD @@ -0,0 +1,94 @@ +### Prerequisites + +* Java JDK 14 or later +* Maven 3 +* NodeJs (16.9.1) and NPM (8.3.2) - [NodeJS Install](https://nodejs.org/en/download/package-manager/) +* mariaDB 10.6 (available for development via docker-compose scripts) +* using Keycloak is optional + +### Installation Steps + +:exclamation: Each step is executed from the project home directory. + +1) go to the deployment folder and start the environment (database and keycloak) via docker-compose: + + ```bash + cd deployment + docker-compose -f localenv-docker-compose.yml up + ``` + +2) go to `webclient/app` and install the frontend applications dependencies + + ```bash + cd webclient/app + npm install --legacy-peer-deps + ``` + +3) build the project + + ```bash + mvn clean install -P frontend + ``` + +4) start project + + ```bash + java -jar application/target/application-0.0.1-SNAPSHOT.jar + ``` + You can also run the main-class via Visual Studio Code. + + +* **Project Builder can be reached under http://localhost:8081/starwit/** +* **If you are using keycloak:** + * **default user/password is admin/admin** + * **keycloak can be reached under http://localost:8081/auth** + +### Debugging + +#### Frontend Debugging + +For debugging, you can start the frontend separately. + +```shell +cd webclient/app +npm start +``` +NPM server starts under localhost:3000/starwit/ by default + +! If you are using the installation with keycloak, make sure you are logged in before first usage - just go to localhost:8081/starwit in your browser. + +#### Backend Debugging + +You can start the spring boot application in debug mode. See Spring Boot documentation for further details. The easiest way is, to use debug functionality integrated with your IDE like VS Code. + +### MariaDB Client + +The database is available under localhost:3006 + +``` +Username:starwit +Database:starwit +Password:starwit +``` +MySQLWorkbench is recommended to access database for development purpose. + +### Starting without keycloak + +If you want to start your application without keycloak, you need to change spring boot profile to dev in application\src\main\resources\application.properties. + +```properties +spring.profiles.active=dev +``` + +or define env-variable + +```bash +SPRING_PROFILES_ACTIVE=dev +``` + +Start the database without keycloak: + +```bash +cd deployment +docker-compose -f mysqllocal-docker-compose.yml up +``` diff --git a/reacthookspring-backend/application/.gitignore b/reacthookspring-backend/application/.gitignore new file mode 100644 index 0000000..b4c09bd --- /dev/null +++ b/reacthookspring-backend/application/.gitignore @@ -0,0 +1,29 @@ +HELP.md +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ + +### built npm frontend ### +src/main/resources/static \ No newline at end of file diff --git a/reacthookspring-backend/application/pom.xml b/reacthookspring-backend/application/pom.xml new file mode 100644 index 0000000..b95150d --- /dev/null +++ b/reacthookspring-backend/application/pom.xml @@ -0,0 +1,114 @@ + + + 4.0.0 + + de.starwit + reacthookspring + 0.0.1-SNAPSHOT + + + application + jar + + + + de.starwit + rest + + + de.starwit + persistence + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + org.springframework.boot + spring-boot-starter-security + + + org.springdoc + springdoc-openapi-ui + + + org.springdoc + springdoc-openapi-security + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot-version} + + + + build-info + + + + + + + pl.project13.maven + git-commit-id-plugin + 2.2.4 + + false + false + dd.MM.yyyy '@' HH:mm:ss z + ${user.timezone} + false + + git.commit.id + git.commit.message.full + git.commit.time + git.commit.user.email + git.commit.user.name + + + git.commit.id.abbrev + git.commit.id.describe* + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 3.0.0 + + + prepare-package + + + + + + + + + + + + + + + + + run + + + + + + + + \ No newline at end of file diff --git a/reacthookspring-backend/application/src/main/java/de/starwit/application/Application.java b/reacthookspring-backend/application/src/main/java/de/starwit/application/Application.java new file mode 100644 index 0000000..6455899 --- /dev/null +++ b/reacthookspring-backend/application/src/main/java/de/starwit/application/Application.java @@ -0,0 +1,31 @@ +package de.starwit.application; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter; +import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +/** + * Main SpringApplication to start the whole project + */ +@SpringBootApplication(scanBasePackages = { "de.starwit.rest", "de.starwit.service", "de.starwit.persistence", + "de.starwit.application.config" }) +public class Application { + public static void main(String[] args) { + SpringApplication.run(new Class[] { Application.class }, args); + } + + @Bean + public ObjectMapper mapper() { + ObjectMapper mapper = new ObjectMapper(); + + SimpleFilterProvider filterProvider = new SimpleFilterProvider(); + filterProvider.addFilter("filterId", SimpleBeanPropertyFilter.filterOutAllExcept("id")); + filterProvider.addFilter("filterIdName", SimpleBeanPropertyFilter.filterOutAllExcept("id", "name", "title")); + mapper.setFilterProvider(filterProvider); + return mapper; + } +} diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/AuthenticationFacade.java b/reacthookspring-backend/application/src/main/java/de/starwit/application/config/AuthenticationFacade.java similarity index 100% rename from reacthookspring/application/src/main/java/de/starwit/application/config/AuthenticationFacade.java rename to reacthookspring-backend/application/src/main/java/de/starwit/application/config/AuthenticationFacade.java diff --git a/reacthookspring-backend/application/src/main/java/de/starwit/application/config/CorsConfig.java b/reacthookspring-backend/application/src/main/java/de/starwit/application/config/CorsConfig.java new file mode 100644 index 0000000..b26384d --- /dev/null +++ b/reacthookspring-backend/application/src/main/java/de/starwit/application/config/CorsConfig.java @@ -0,0 +1,22 @@ +package de.city.application.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Profile("!dev") +@Configuration +public class CorsConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedMethods("*") + .allowedOrigins("http://localhost:8080", "http://localhost:8081", "http://localhost:8083", + "http://localhost:3001", "http://localhost:3000", "*") + .allowedHeaders("*") + .allowCredentials(false); + } + +} diff --git a/reacthookspring-backend/application/src/main/java/de/starwit/application/config/DisableSecurityConfig.java b/reacthookspring-backend/application/src/main/java/de/starwit/application/config/DisableSecurityConfig.java new file mode 100644 index 0000000..b6a82a9 --- /dev/null +++ b/reacthookspring-backend/application/src/main/java/de/starwit/application/config/DisableSecurityConfig.java @@ -0,0 +1,19 @@ +package de.city.application.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +@Profile("dev") +@EnableWebSecurity +@Configuration +public class DisableSecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + + http.authorizeRequests().antMatchers("/**").permitAll().anyRequest().authenticated().and().csrf().disable(); + } +} diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/IAuthenticationFacade.java b/reacthookspring-backend/application/src/main/java/de/starwit/application/config/IAuthenticationFacade.java similarity index 100% rename from reacthookspring/application/src/main/java/de/starwit/application/config/IAuthenticationFacade.java rename to reacthookspring-backend/application/src/main/java/de/starwit/application/config/IAuthenticationFacade.java diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/LocalRoleAuthenticationConverter.java b/reacthookspring-backend/application/src/main/java/de/starwit/application/config/LocalRoleAuthenticationConverter.java similarity index 100% rename from reacthookspring/application/src/main/java/de/starwit/application/config/LocalRoleAuthenticationConverter.java rename to reacthookspring-backend/application/src/main/java/de/starwit/application/config/LocalRoleAuthenticationConverter.java diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/LocalRoleAuthenticationToken.java b/reacthookspring-backend/application/src/main/java/de/starwit/application/config/LocalRoleAuthenticationToken.java similarity index 100% rename from reacthookspring/application/src/main/java/de/starwit/application/config/LocalRoleAuthenticationToken.java rename to reacthookspring-backend/application/src/main/java/de/starwit/application/config/LocalRoleAuthenticationToken.java diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/ResourceServerConfig.java b/reacthookspring-backend/application/src/main/java/de/starwit/application/config/ResourceServerConfig.java similarity index 100% rename from reacthookspring/application/src/main/java/de/starwit/application/config/ResourceServerConfig.java rename to reacthookspring-backend/application/src/main/java/de/starwit/application/config/ResourceServerConfig.java diff --git a/reacthookspring/application/src/main/java/de/starwit/application/dto/PrincipalDto.java b/reacthookspring-backend/application/src/main/java/de/starwit/application/dto/PrincipalDto.java similarity index 100% rename from reacthookspring/application/src/main/java/de/starwit/application/dto/PrincipalDto.java rename to reacthookspring-backend/application/src/main/java/de/starwit/application/dto/PrincipalDto.java diff --git a/reacthookspring-backend/application/src/main/resources/application.properties b/reacthookspring-backend/application/src/main/resources/application.properties new file mode 100644 index 0000000..f03df3c --- /dev/null +++ b/reacthookspring-backend/application/src/main/resources/application.properties @@ -0,0 +1,57 @@ +spring.profiles.active=@spring.profiles.active@ +spring.banner.location=classpath:banner.txt +server.servlet.context-path=/reacthookspring +rest.base-path=/api +server.port=8081 + +# actuator +management.endpoints.web.base-path=/monitoring +management.endpoint.health.show-details=always +management.endpoints.web.exposure.include=* + +# show full git properties +management.info.git.mode=full + +# MySQL +spring.datasource.hikari.connection-timeout=10000 +spring.datasource.driver-class-name=org.mariadb.jdbc.Driver +spring.datasource.url=jdbc:mariadb://localhost:3306/reacthookspring?useLegacyDatetimeCode=false&serverTimezone=CET +spring.jpa.hibernate.naming.physical-strategy=de.starwit.persistence.config.DatabasePhysicalNamingStrategy +spring.datasource.username=reacthookspring +spring.datasource.password=reacthookspring + +# Flyway +spring.flyway.user=${spring.datasource.username} +spring.flyway.password=${spring.datasource.password} +spring.flyway.url=${spring.datasource.url} +spring.flyway.baselineOnMigrate=true +spring.flyway.locations=classpath:db/migration +spring.flyway.encoding=UTF-8 +spring.flyway.placeholder-replacement=false + +springdoc.swagger-ui.disable-swagger-default-url=true +#spring.profiles.active=dev +springdoc.api-docs.path=/api-docs +springdoc.swagger-ui.path=/swagger-ui.html +springdoc.swagger-ui.csrf.enabled=true +#logging.level.org.springframework=DEBUG +#spring.jpa.generate-ddl=true +#spring.jpa.show-sql=true +#spring.jpa.hibernate.ddl-auto=create-drop +#spring.jpa.properties.javax.persistence.schema-generation.create-source=metadata +#spring.jpa.properties.javax.persistence.schema-generation.scripts.action=create +#spring.jpa.properties.javax.persistence.schema-generation.scripts.create-target=/home/anett/create.sql + +#logging.level.org.springframework.security=DEBUG + +keycloak.auth-server-url=http://localhost:8080/auth +keycloak.realm=reacthookspring +keycloak.resource=reacthookspring +keycloak.principal-attribute=preferred_username +keycloak.public-client=true +keycloak.enabled=true + + + + + diff --git a/reacthookspring-backend/application/src/main/resources/banner.txt b/reacthookspring-backend/application/src/main/resources/banner.txt new file mode 100644 index 0000000..a9d7388 --- /dev/null +++ b/reacthookspring-backend/application/src/main/resources/banner.txt @@ -0,0 +1,17 @@ + + _ _ _ _ _ _ + | | (_) | | | | | | | + ___| |_ __ _ _ ____ ___| |_ | |_ ___ _ __ ___ _ __ | | __ _| |_ ___ + / __| __/ _` | '__\ \ /\ / / | __| | __/ _ \ '_ ` _ \| '_ \| |/ _` | __/ _ \ + \__ \ || (_| | | \ V V /| | |_ | || __/ | | | | | |_) | | (_| | || __/ + |___/\__\__,_|_| \_/\_/ |_|\__| \__\___|_| |_| |_| .__/|_|\__,_|\__\___| + | | + |_| + _ _ _ _ _ _ + | | | | | | (_) | | | | + _ __ ___ __ _ ___| |_| |__ ___ ___ | | __ ______ ___ _ __ _ __ _ _ __ __ _ | |__ ___ ___ | |_ +| '__/ _ \/ _` |/ __| __| '_ \ / _ \ / _ \| |/ / |______| / __| '_ \| '__| | '_ \ / _` | | '_ \ / _ \ / _ \| __| +| | | __/ (_| | (__| |_| | | | (_) | (_) | < \__ \ |_) | | | | | | | (_| | | |_) | (_) | (_) | |_ +|_| \___|\__,_|\___|\__|_| |_|\___/ \___/|_|\_\ |___/ .__/|_| |_|_| |_|\__, | |_.__/ \___/ \___/ \__| + | | __/ | + |_| |___/ diff --git a/reacthookspring-backend/application/src/main/resources/log4j2.xml b/reacthookspring-backend/application/src/main/resources/log4j2.xml new file mode 100644 index 0000000..3e5f1d8 --- /dev/null +++ b/reacthookspring-backend/application/src/main/resources/log4j2.xml @@ -0,0 +1,34 @@ + + + + test + [%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n + + + + + ${pattern} + + + + + ${pattern} + + + + + + + + + + + + + + + + + + diff --git a/reacthookspring-backend/application/src/test/resources/log4j2.xml b/reacthookspring-backend/application/src/test/resources/log4j2.xml new file mode 100644 index 0000000..b196876 --- /dev/null +++ b/reacthookspring-backend/application/src/test/resources/log4j2.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/reacthookspring-backend/deployment/helm/.gitignore b/reacthookspring-backend/deployment/helm/.gitignore new file mode 100644 index 0000000..2bf561d --- /dev/null +++ b/reacthookspring-backend/deployment/helm/.gitignore @@ -0,0 +1 @@ +**/*-private.yaml diff --git a/reacthookspring-backend/deployment/helm/reacthook-spring/.gitignore b/reacthookspring-backend/deployment/helm/reacthook-spring/.gitignore new file mode 100644 index 0000000..b1a6556 --- /dev/null +++ b/reacthookspring-backend/deployment/helm/reacthook-spring/.gitignore @@ -0,0 +1,6 @@ +charts +Chart.lock + +custom* +**/*dry* +**/values-private.yaml diff --git a/reacthookspring-backend/deployment/helm/reacthook-spring/.helmignore b/reacthookspring-backend/deployment/helm/reacthook-spring/.helmignore new file mode 100644 index 0000000..7df441b --- /dev/null +++ b/reacthookspring-backend/deployment/helm/reacthook-spring/.helmignore @@ -0,0 +1,26 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ + +custom* +dry* \ No newline at end of file diff --git a/reacthookspring-backend/deployment/helm/reacthook-spring/Chart.yaml b/reacthookspring-backend/deployment/helm/reacthook-spring/Chart.yaml new file mode 100644 index 0000000..a6b75d9 --- /dev/null +++ b/reacthookspring-backend/deployment/helm/reacthook-spring/Chart.yaml @@ -0,0 +1,28 @@ +apiVersion: v2 +name: reacthookspring +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +appVersion: latest + +dependencies: + - name: mariadb + version: "9.0.1" + repository: https://charts.bitnami.com/bitnami diff --git a/reacthookspring-backend/deployment/helm/reacthook-spring/Readme.md b/reacthookspring-backend/deployment/helm/reacthook-spring/Readme.md new file mode 100644 index 0000000..0837182 --- /dev/null +++ b/reacthookspring-backend/deployment/helm/reacthook-spring/Readme.md @@ -0,0 +1,30 @@ +Example customValues.yaml + +```yaml +ingress: + enabled: true + hosts: + - host: + paths: + - /reacthookspring + tls: + - secretName: + hosts: + - + +mariadb: + image: + registry: + pullSecrets: + - + auth: + rootPassword: root #change + database: reacthookspring #change + username: reacthookspring #change + password: reacthookspring #change + +github: + registry: + username: + pat: #you only need to be able to read packages +``` \ No newline at end of file diff --git a/reacthookspring-backend/deployment/helm/reacthook-spring/mariadb-parameter.md b/reacthookspring-backend/deployment/helm/reacthook-spring/mariadb-parameter.md new file mode 100644 index 0000000..0ea4969 --- /dev/null +++ b/reacthookspring-backend/deployment/helm/reacthook-spring/mariadb-parameter.md @@ -0,0 +1,464 @@ +# MariaDB + +[MariaDB](https://mariadb.org) is one of the most popular database servers in the world. It’s made by the original developers of MySQL and guaranteed to stay open source. Notable users include Wikipedia, Facebook and Google. + +MariaDB is developed as open source software and as a relational database it provides an SQL interface for accessing data. The latest versions of MariaDB also include GIS and JSON features. + +## TL;DR + +```bash +$ helm repo add bitnami https://charts.bitnami.com/bitnami +$ helm install my-release bitnami/mariadb +``` + +## Introduction + +This chart bootstraps a [MariaDB](https://github.com/bitnami/bitnami-docker-mariadb) replication cluster deployment on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager. + +Bitnami charts can be used with [Kubeapps](https://kubeapps.com/) for deployment and management of Helm Charts in clusters. This chart has been tested to work with NGINX Ingress, cert-manager, fluentd and Prometheus on top of the [BKPR](https://kubeprod.io/). + +## Prerequisites + +- Kubernetes 1.12+ +- Helm 3.0-beta3+ +- PV provisioner support in the underlying infrastructure + +## Installing the Chart + +To install the chart with the release name `my-release`: + +```bash +$ helm install my-release bitnami/mariadb +``` + +The command deploys MariaDB on the Kubernetes cluster in the default configuration. The [Parameters](#parameters) section lists the parameters that can be configured during installation. + +> **Tip**: List all releases using `helm list` + +## Uninstalling the Chart + +To uninstall/delete the `my-release` deployment: + +```bash +$ helm delete my-release +``` + +The command removes all the Kubernetes components associated with the chart and deletes the release. + +## Parameters + +The following table lists the configurable parameters of the MariaDB chart and their default values. + +| Parameter | Description | Default | +|---------------------------------------------|----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------| +| `global.imageRegistry` | Global Docker Image registry | `nil` | +| `global.imagePullSecrets` | Global Docker registry secret names as an array | `[]` (does not add image pull secrets to deployed pods) | +| `global.storageClass` | Global storage class for dynamic provisioning | `nil` | + +### Common parameters + +| Parameter | Description | Default | +|---------------------------------------------|----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------| +| `nameOverride` | String to partially override mariadb.fullname | `nil` | +| `fullnameOverride` | String to fully override mariadb.fullname | `nil` | +| `clusterDomain` | Default Kubernetes cluster domain | `cluster.local` | +| `commonLabels` | Labels to add to all deployed objects | `nil` | +| `commonAnnotations` | Annotations to add to all deployed objects | `[]` | +| `schedulerName` | Name of the scheduler (other than default) to dispatch pods | `nil` | +| `extraDeploy` | Array of extra objects to deploy with the release (evaluated as a template) | `nil` | + +### MariaDB common parameters + +| Parameter | Description | Default | +|---------------------------------------------|----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------| +| `image.registry` | MariaDB image registry | `docker.io` | +| `image.repository` | MariaDB image name | `bitnami/mariadb` | +| `image.tag` | MariaDB image tag | `{TAG_NAME}` | +| `image.pullPolicy` | MariaDB image pull policy | `IfNotPresent` | +| `image.pullSecrets` | Specify docker-registry secret names as an array | `[]` (does not add image pull secrets to deployed pods) | +| `image.debug` | Specify if debug logs should be enabled | `false` | +| `architecture` | MariaDB architecture (`standalone` or `replication`) | `standalone` | +| `auth.rootPassword` | Password for the `root` user. Ignored if existing secret is provided. | _random 10 character alphanumeric string_ | +| `auth.database` | Name for a custom database to create | `my_database` | +| `auth.username` | Name for a custom user to create | `""` | +| `auth.password` | Password for the new user. Ignored if existing secret is provided | _random 10 character long alphanumeric string_ | +| `auth.replicationUser` | MariaDB replication user | `nil` | +| `auth.replicationPassword` | MariaDB replication user password. Ignored if existing secret is provided | _random 10 character long alphanumeric string_ | +| `auth.forcePassword` | Force users to specify required passwords | `false` | +| `auth.usePasswordFiles` | Mount credentials as a files instead of using an environment variable | `false` | +| `auth.existingSecret` | Use existing secret for password details (`auth.rootPassword`, `auth.password`, `auth.replicationPassword` will be ignored and picked up from this secret). The secret has to contain the keys `mariadb-root-password`, `mariadb-replication-password` and `mariadb-password` | `nil` | +| `initdbScripts` | Dictionary of initdb scripts | `nil` | +| `initdbScriptsConfigMap` | ConfigMap with the initdb scripts (Note: Overrides `initdbScripts`) | `nil` | + +### MariaDB Primary parameters + +| Parameter | Description | Default | +|----------------------------------------------|----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------| +| `primary.command` | Override default container command on MariaDB Primary container(s) (useful when using custom images) | `nil` | +| `primary.args` | Override default container args on MariaDB Primary container(s) (useful when using custom images) | `nil` | +| `primary.configuration` | MariaDB Primary configuration to be injected as ConfigMap | Check `values.yaml` file | +| `primary.existingConfigmap` | Name of existing ConfigMap with MariaDB Primary configuration | `nil` | +| `primary.updateStrategy` | Update strategy type for the MariaDB primary statefulset | `RollingUpdate` | +| `primary.podAnnotations` | Additional pod annotations for MariaDB primary pods | `{}` (evaluated as a template) | +| `primary.podLabels` | Additional pod labels for MariaDB primary pods | `{}` (evaluated as a template) | +| `primary.podAffinityPreset` | MariaDB primary pod affinity preset. Ignored if `primary.affinity` is set. Allowed values: `soft` or `hard` | `""` | +| `primary.podAntiAffinityPreset` | MariaDB primary pod anti-affinity preset. Ignored if `primary.affinity` is set. Allowed values: `soft` or `hard` | `soft` | +| `primary.nodeAffinityPreset.type` | MariaDB primary node affinity preset type. Ignored if `primary.affinity` is set. Allowed values: `soft` or `hard` | `""` | +| `primary.nodeAffinityPreset.key` | MariaDB primary node label key to match Ignored if `primary.affinity` is set. | `""` | +| `primary.nodeAffinityPreset.values` | MariaDB primary node label values to match. Ignored if `primary.affinity` is set. | `[]` | +| `primary.affinity` | Affinity for MariaDB primary pods assignment | `{}` (evaluated as a template) | +| `primary.nodeSelector` | Node labels for MariaDB primary pods assignment | `{}` (evaluated as a template) | +| `primary.tolerations` | Tolerations for MariaDB primary pods assignment | `[]` (evaluated as a template) | +| `primary.podSecurityContext.enabled` | Enable security context for MariaDB primary pods | `true` | +| `primary.podSecurityContext.fsGroup` | Group ID for the mounted volumes' filesystem | `1001` | +| `primary.containerSecurityContext.enabled` | MariaDB primary container securityContext | `true` | +| `primary.containerSecurityContext.runAsUser` | User ID for the MariaDB primary container | `1001` | +| `primary.livenessProbe` | Liveness probe configuration for MariaDB primary containers | Check `values.yaml` file | +| `primary.readinessProbe` | Readiness probe configuration for MariaDB primary containers | Check `values.yaml` file | +| `primary.customLivenessProbe` | Override default liveness probe for MariaDB primary containers | `nil` | +| `primary.customReadinessProbe` | Override default readiness probe for MariaDB primary containers | `nil` | +| `primary.resources.limits` | The resources limits for MariaDB primary containers | `{}` | +| `primary.resources.requests` | The requested resources for MariaDB primary containers | `{}` | +| `primary.extraEnvVars` | Extra environment variables to be set on MariaDB primary containers | `{}` | +| `primary.extraEnvVarsCM` | Name of existing ConfigMap containing extra env vars for MariaDB primary containers | `nil` | +| `primary.extraEnvVarsSecret` | Name of existing Secret containing extra env vars for MariaDB primary containers | `nil` | +| `primary.extraFlags` | MariaDB primary additional command line flags | `nil` | +| `primary.persistence.enabled` | Enable persistence on MariaDB primary replicas using a `PersistentVolumeClaim` | `true` | +| `primary.persistence.existingClaim` | Name of an existing `PersistentVolumeClaim` for MariaDB primary replicas | `nil` | +| `primary.persistence.annotations` | MariaDB primary persistent volume claim annotations | `{}` (evaluated as a template) | +| `primary.persistence.storageClass` | MariaDB primary persistent volume storage Class | `nil` | +| `primary.persistence.accessModes` | MariaDB primary persistent volume access Modes | `[ReadWriteOnce]` | +| `primary.persistence.size` | MariaDB primary persistent volume size | `8Gi` | +| `primary.persistence.selector` | Selector to match an existing Persistent Volume | `{}` (evaluated as a template) | +| `primary.initContainers` | Add additional init containers for the MariaDB Primary pod(s) | `{}` (evaluated as a template) | +| `primary.sidecars` | Add additional sidecar containers for the MariaDB Primary pod(s) | `{}` (evaluated as a template) | +| `primary.extraVolumeMounts` | Optionally specify extra list of additional volumeMounts for the MariaDB Primary container(s) | `{}` | +| `primary.extraVolumes` | Optionally specify extra list of additional volumes to the MariaDB Primary pod(s) | `{}` | +| `primary.service.type` | MariaDB Primary K8s service type | `ClusterIP` | +| `primary.service.clusterIP` | MariaDB Primary K8s service clusterIP IP | `nil` | +| `primary.service.port` | MariaDB Primary K8s service port | `3306` | +| `primary.service.nodePort` | MariaDB Primary K8s service node port | `nil` | +| `primary.service.loadBalancerIP` | MariaDB Primary loadBalancerIP if service type is `LoadBalancer` | `nil` | +| `primary.service.loadBalancerSourceRanges` | Address that are allowed when MariaDB Primary service is LoadBalancer | `[]` | +| `primary.pdb.create` | Enable/disable a Pod Disruption Budget creation for MariaDB primary pods | `false` | +| `primary.pdb.minAvailable` | Minimum number/percentage of MariaDB primary pods that should remain scheduled | `1` | +| `primary.pdb.maxUnavailable` | Maximum number/percentage of MariaDB primary pods that may be made unavailable | `nil` | + +### MariaDB Secondary parameters + +| Parameter | Description | Default | +|------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------| +| `secondary.command` | Override default container command on MariaDB Secondary container(s) (useful when using custom images) | `nil` | +| `secondary.args` | Override default container args on MariaDB Secondary container(s) (useful when using custom images) | `nil` | +| `secondary.configuration` | MariaDB Secondary configuration to be injected as ConfigMap | Check `values.yaml` file | +| `secondary.existingConfigmap` | Name of existing ConfigMap with MariaDB Secondary configuration | `nil` | +| `secondary.replicaCount` | Number of MariaDB secondary replicas | `1` | +| `secondary.updateStrategy` | Update strategy type for the MariaDB secondary statefulset | `RollingUpdate` | +| `secondary.podAnnotations` | Additional pod annotations for MariaDB secondary pods | `{}` (evaluated as a template) | +| `secondary.podLabels` | Additional pod labels for MariaDB secondary pods | `{}` (evaluated as a template) | +| `secondary.podAffinityPreset` | MariaDB secondary pod affinity preset. Ignored if `secondary.affinity` is set. Allowed values: `soft` or `hard` | `""` | +| `secondary.podAntiAffinityPreset` | MariaDB secondary pod anti-affinity preset. Ignored if `secondary.affinity` is set. Allowed values: `soft` or `hard` | `soft` | +| `secondary.nodeAffinityPreset.type` | MariaDB secondary node affinity preset type. Ignored if `secondary.affinity` is set. Allowed values: `soft` or `hard` | `""` | +| `secondary.nodeAffinityPreset.key` | MariaDB secondary node label key to match Ignored if `secondary.affinity` is set. | `""` | +| `secondary.nodeAffinityPreset.values` | MariaDB secondary node label values to match. Ignored if `secondary.affinity` is set. | `[]` | +| `secondary.affinity` | Affinity for MariaDB secondary pods assignment | `{}` (evaluated as a template) | +| `secondary.nodeSelector` | Node labels for MariaDB secondary pods assignment | `{}` (evaluated as a template) | +| `secondary.tolerations` | Tolerations for MariaDB secondary pods assignment | `[]` (evaluated as a template) | +| `secondary.podSecurityContext.enabled` | Enable security context for MariaDB secondary pods | `true` | +| `secondary.podSecurityContext.fsGroup` | Group ID for the mounted volumes' filesystem | `1001` | +| `secondary.containerSecurityContext.enabled` | MariaDB secondary container securityContext | `true` | +| `secondary.containerSecurityContext.runAsUser` | User ID for the MariaDB secondary container | `1001` | +| `secondary.livenessProbe` | Liveness probe configuration for MariaDB secondary containers | Check `values.yaml` file | +| `secondary.readinessProbe` | Readiness probe configuration for MariaDB secondary containers | Check `values.yaml` file | +| `secondary.customLivenessProbe` | Override default liveness probe for MariaDB secondary containers | `nil` | +| `secondary.customReadinessProbe` | Override default readiness probe for MariaDB secondary containers | `nil` | +| `secondary.resources.limits` | The resources limits for MariaDB secondary containers | `{}` | +| `secondary.resources.requests` | The requested resources for MariaDB secondary containers | `{}` | +| `secondary.extraEnvVars` | Extra environment variables to be set on MariaDB secondary containers | `{}` | +| `secondary.extraEnvVarsCM` | Name of existing ConfigMap containing extra env vars for MariaDB secondary containers | `nil` | +| `secondary.extraEnvVarsSecret` | Name of existing Secret containing extra env vars for MariaDB secondary containers | `nil` | +| `secondary.extraFlags` | MariaDB secondary additional command line flags | `nil` | +| `secondary.extraFlags` | MariaDB secondary additional command line flags | `nil` | +| `secondary.persistence.enabled` | Enable persistence on MariaDB secondary replicas using a `PersistentVolumeClaim` | `true` | +| `secondary.persistence.annotations` | MariaDB secondary persistent volume claim annotations | `{}` (evaluated as a template) | +| `secondary.persistence.storageClass` | MariaDB secondary persistent volume storage Class | `nil` | +| `secondary.persistence.accessModes` | MariaDB secondary persistent volume access Modes | `[ReadWriteOnce]` | +| `secondary.persistence.size` | MariaDB secondary persistent volume size | `8Gi` | +| `secondary.persistence.selector` | Selector to match an existing Persistent Volume | `{}` (evaluated as a template) | +| `secondary.initContainers` | Add additional init containers for the MariaDB secondary pod(s) | `{}` (evaluated as a template) | +| `secondary.sidecars` | Add additional sidecar containers for the MariaDB secondary pod(s) | `{}` (evaluated as a template) | +| `secondary.extraVolumeMounts` | Optionally specify extra list of additional volumeMounts for the MariaDB secondary container(s) | `{}` | +| `secondary.extraVolumes` | Optionally specify extra list of additional volumes to the MariaDB secondary pod(s) | `{}` | +| `secondary.service.type` | MariaDB secondary K8s service type | `ClusterIP` | +| `secondary.service.clusterIP` | MariaDB secondary K8s service clusterIP IP | `nil` | +| `secondary.service.port` | MariaDB secondary K8s service port | `3306` | +| `secondary.service.nodePort` | MariaDB secondary K8s service node port | `nil` | +| `secondary.service.loadBalancerIP` | MariaDB secondary loadBalancerIP if service type is `LoadBalancer` | `nil` | +| `secondary.service.loadBalancerSourceRanges` | Address that are allowed when MariaDB secondary service is LoadBalancer | `[]` | +| `secondary.pdb.create` | Enable/disable a Pod Disruption Budget creation for MariaDB secondary pods | `false` | +| `secondary.pdb.minAvailable` | Minimum number/percentage of MariaDB secondary pods that should remain scheduled | `1` | +| `secondary.pdb.maxUnavailable` | Maximum number/percentage of MariaDB secondary pods that may be made unavailable | `nil` | + +### RBAC parameters + +| Parameter | Description | Default | +|---------------------------------------------|----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------| +| `serviceAccount.create` | Enable the creation of a ServiceAccount for MariaDB pods | `true` | +| `serviceAccount.name` | Name of the created ServiceAccount | Generated using the `mariadb.fullname` template | +| `serviceAccount.annotations` | Annotations for MariaDB Service Account | `{}` (evaluated as a template) | +| `rbac.create` | Weather to create & use RBAC resources or not | `false` | + +### Volume Permissions parameters + +| Parameter | Description | Default | +|---------------------------------------------|----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------| +| `volumePermissions.enabled` | Enable init container that changes the owner and group of the persistent volume(s) mountpoint to `runAsUser:fsGroup` | `false` | +| `volumePermissions.image.registry` | Init container volume-permissions image registry | `docker.io` | +| `volumePermissions.image.repository` | Init container volume-permissions image name | `bitnami/minideb` | +| `volumePermissions.image.tag` | Init container volume-permissions image tag | `buster` | +| `volumePermissions.image.pullPolicy` | Init container volume-permissions image pull policy | `Always` | +| `volumePermissions.image.pullSecrets` | Specify docker-registry secret names as an array | `[]` (does not add image pull secrets to deployed pods) | +| `volumePermissions.resources.limits` | Init container volume-permissions resource limits | `{}` | +| `volumePermissions.resources.requests` | Init container volume-permissions resource requests | `{}` | + +### Metrics parameters + +| Parameter | Description | Default | +|---------------------------------------------|----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------| +| `metrics.enabled` | Start a side-car prometheus exporter | `false` | +| `metrics.image.registry` | Exporter image registry | `docker.io` | +| `metrics.image.repository` | Exporter image name | `bitnami/mysqld-exporter` | +| `metrics.image.tag` | Exporter image tag | `{TAG_NAME}` | +| `metrics.image.pullPolicy` | Exporter image pull policy | `IfNotPresent` | +| `metrics.extraArgs.primary` | Extra args to be passed to mysqld_exporter on Primary pods | `[]` | +| `metrics.extraArgs.secondary` | Extra args to be passed to mysqld_exporter on Secondary pods | `[]` | +| `metrics.resources.limits` | The resources limits for MariaDB prometheus exporter containers | `{}` | +| `metrics.resources.requests` | The requested resources for MariaDB prometheus exporter containers | `{}` | +| `metrics.livenessProbe` | Liveness probe configuration for MariaDB prometheus exporter containers | Check `values.yaml` file | +| `metrics.readinessProbe` | Readiness probe configuration for MariaDB prometheus exporter containers | Check `values.yaml` file | +| `metrics.serviceMonitor.enabled` | Create ServiceMonitor Resource for scraping metrics using PrometheusOperator | `false` | +| `metrics.serviceMonitor.namespace` | Namespace which Prometheus is running in | `nil` | +| `metrics.serviceMonitor.interval` | Interval at which metrics should be scraped | `30s` | +| `metrics.serviceMonitor.scrapeTimeout` | Specify the timeout after which the scrape is ended | `nil` | +| `metrics.serviceMonitor.relabellings` | Specify Metric Relabellings to add to the scrape endpoint | `nil` | +| `metrics.serviceMonitor.honorLabels` | honorLabels chooses the metric's labels on collisions with target labels. | `false` | +| `metrics.serviceMonitor.additionalLabels` | Used to pass Labels that are required by the Installed Prometheus Operator | `{}` | +| `metrics.serviceMonitor.release` | Used to pass Labels release that sometimes should be custom for Prometheus Operator | `nil` | + +The above parameters map to the env variables defined in [bitnami/mariadb](http://github.com/bitnami/bitnami-docker-mariadb). For more information please refer to the [bitnami/mariadb](http://github.com/bitnami/bitnami-docker-mariadb) image documentation. + +Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example, + +```bash +$ helm install my-release \ + --set auth.rootPassword=secretpassword,auth.database=app_database \ + bitnami/mariadb +``` + +The above command sets the MariaDB `root` account password to `secretpassword`. Additionally it creates a database named `my_database`. + +Alternatively, a YAML file that specifies the values for the parameters can be provided while installing the chart. For example, + +```bash +$ helm install my-release -f values.yaml bitnami/mariadb +``` + +> **Tip**: You can use the default [values.yaml](values.yaml) + +## Configuration and installation details + +### [Rolling VS Immutable tags](https://docs.bitnami.com/containers/how-to/understand-rolling-tags-containers/) + +It is strongly recommended to use immutable tags in a production environment. This ensures your deployment does not change automatically if the same tag is updated with a different image. + +Bitnami will release a new chart updating its containers if a new version of the main container, significant changes, or critical vulnerabilities exist. + +### Production configuration + +This chart includes a `values-production.yaml` file where you can find some parameters oriented to production configuration in comparison to the regular `values.yaml`. You can use this file instead of the default one. + +- Force users to specify a password and mount secrets as volumes instead of using environment variables: + +```diff +- auth.forcePassword: false ++ auth.forcePassword: true +- auth.usePasswordFiles: false ++ auth.usePasswordFiles: true +``` + +- Use "replication" architecture: + +```diff +- architecture: standalone ++ architecture: replication +``` + +- Desired number of secondary replicas: + +```diff +- secondary.replicaCount: 1 ++ secondary.replicaCount: 2 +``` + +- Start a side-car prometheus exporter: + +```diff +- metrics.enabled: false ++ metrics.enabled: true +``` + +### Change MariaDB version + +To modify the MariaDB version used in this chart you can specify a [valid image tag](https://hub.docker.com/r/bitnami/mariadb/tags/) using the `image.tag` parameter. For example, `image.tag=X.Y.Z`. This approach is also applicable to other images like exporters. + +### Initialize a fresh instance + +The [Bitnami MariaDB](https://github.com/bitnami/bitnami-docker-mariadb) image allows you to use your custom scripts to initialize a fresh instance. In order to execute the scripts, you can specify custom scripts using the `initdbScripts` parameter as dict. + +In addition to this option, you can also set an external ConfigMap with all the initialization scripts. This is done by setting the `initdbScriptsConfigMap` parameter. Note that this will override the previous option. + +The allowed extensions are `.sh`, `.sql` and `.sql.gz`. + +Take into account those scripts are treated differently depending on the extension. While the `.sh` scripts are executed in all the nodes; the `.sql` and `.sql.gz` scripts are only executed in the primary nodes. The reason behind this differentiation is that the `.sh` scripts allow adding conditions to determine what is the node running the script, while these conditions can't be set using `.sql` nor `sql.gz` files. This way it is possible to cover different use cases depending on their needs. + +If using a `.sh` script you want to do a "one-time" action like creating a database, you need to add a condition in your `.sh` script to be executed only in one of the nodes, such as + +```yaml +initdbScripts: + my_init_script.sh: | + #!/bin/sh + if [[ $(hostname) == *primary* ]]; then + echo "Primary node" + mysql -P 3306 -uroot -prandompassword -e "create database new_database"; + else + echo "No primary node" + fi +``` + +### Sidecars and Init Containers + +If you have a need for additional containers to run within the same pod as MongoDB, you can do so via the `sidecars` config parameter. Simply define your container according to the Kubernetes container spec. + +```yaml +sidecars: + - name: your-image-name + image: your-image + imagePullPolicy: Always + ports: + - name: portname + containerPort: 1234 +``` + +Similarly, you can add extra init containers using the `initContainers` parameter. + +```yaml +initContainers: + - name: your-image-name + image: your-image + imagePullPolicy: Always + ports: + - name: portname + containerPort: 1234 +``` + +## Persistence + +The [Bitnami MariaDB](https://github.com/bitnami/bitnami-docker-mariadb) image stores the MariaDB data and configurations at the `/bitnami/mariadb` path of the container. + +The chart mounts a [Persistent Volume](https://kubernetes.io/docs/user-guide/persistent-volumes/) volume at this location. The volume is created using dynamic volume provisioning, by default. An existing PersistentVolumeClaim can be defined. + +### Adjust permissions of persistent volume mountpoint + +As the image run as non-root by default, it is necessary to adjust the ownership of the persistent volume so that the container can write data into it. + +By default, the chart is configured to use Kubernetes Security Context to automatically change the ownership of the volume. However, this feature does not work in all Kubernetes distributions. +As an alternative, this chart supports using an initContainer to change the ownership of the volume before mounting it in the final destination. + +You can enable this initContainer by setting `volumePermissions.enabled` to `true`. + +## Troubleshooting + +Find more information about how to deal with common errors related to Bitnami’s Helm charts in [this troubleshooting guide](https://docs.bitnami.com/general/how-to/troubleshoot-helm-chart-issues). + +## Upgrading + +It's necessary to set the `auth.rootPassword` parameter when upgrading for readiness/liveness probes to work properly. When you install this chart for the first time, some notes will be displayed providing the credentials you must use under the 'Administrator credentials' section. Please note down the password and run the command below to upgrade your chart: + +```bash +$ helm upgrade my-release bitnami/mariadb --set auth.rootPassword=[ROOT_PASSWORD] +``` + +| Note: you need to substitute the placeholder _[ROOT_PASSWORD]_ with the value obtained in the installation notes. + +### To 9.0.0 + +[On November 13, 2020, Helm v2 support was formally finished](https://github.com/helm/charts#status-of-the-project), this major version is the result of the required changes applied to the Helm Chart to be able to incorporate the different features added in Helm v3 and to be consistent with the Helm project itself regarding the Helm v2 EOL. + +**What changes were introduced in this major version?** + +- Previous versions of this Helm Chart use `apiVersion: v1` (installable by both Helm 2 and 3), this Helm Chart was updated to `apiVersion: v2` (installable by Helm 3 only). [Here](https://helm.sh/docs/topics/charts/#the-apiversion-field) you can find more information about the `apiVersion` field. +- Move dependency information from the *requirements.yaml* to the *Chart.yaml* +- After running `helm dependency update`, a *Chart.lock* file is generated containing the same structure used in the previous *requirements.lock* +- The different fields present in the *Chart.yaml* file has been ordered alphabetically in a homogeneous way for all the Bitnami Helm Charts + +**Considerations when upgrading to this version** + +- If you want to upgrade to this version from a previous one installed with Helm v3, you shouldn't face any issues +- If you want to upgrade to this version using Helm v2, this scenario is not supported as this version doesn't support Helm v2 anymore +- If you installed the previous version with Helm v2 and wants to upgrade to this version with Helm v3, please refer to the [official Helm documentation](https://helm.sh/docs/topics/v2_v3_migration/#migration-use-cases) about migrating from Helm v2 to v3 + +**Useful links** + +- https://docs.bitnami.com/tutorials/resolve-helm2-helm3-post-migration-issues/ +- https://helm.sh/docs/topics/v2_v3_migration/ +- https://helm.sh/blog/migrate-from-helm-v2-to-helm-v3/ + +### To 8.0.0 + +- Several parameters were renamed or dissapeared in favor of new ones on this major version: + - The terms *master* and *slave* have been replaced by the terms *primary* and *secondary*. Therefore, parameters prefixed with `master` or `slave` are now prefixed with `primary` or `secondary`, respectively. + - `securityContext.*` is deprecated in favor of `primary.podSecurityContext`, `primary.containerSecurityContext`, `secondary.podSecurityContext`, and `secondary.containerSecurityContext`. + - Credentials parameter are reorganized under the `auth` parameter. + - `replication.enabled` parameter is deprecated in favor of `architecture` parameter that accepts two values: `standalone` and `replication`. +- The default MariaDB version was updated from 10.3 to 10.5. According to the official documentation, upgrading from 10.3 should be painless. However, there are some things that have changed which could affect an upgrade: + - [Incompatible changes upgrading from MariaDB 10.3 to MariaDB 10.4](https://mariadb.com/kb/en/upgrading-from-mariadb-103-to-mariadb-104/#incompatible-changes-between-103-and-104). + - [Incompatible changes upgrading from MariaDB 10.4 to MariaDB 10.5](https://mariadb.com/kb/en/upgrading-from-mariadb-104-to-mariadb-105/#incompatible-changes-between-104-and-105). +- Chart labels were adapted to follow the [Helm charts standard labels](https://helm.sh/docs/chart_best_practices/labels/#standard-labels). +- This version also introduces `bitnami/common`, a [library chart](https://helm.sh/docs/topics/library_charts/#helm) as a dependency. More documentation about this new utility could be found [here](https://github.com/bitnami/charts/tree/master/bitnami/common#bitnami-common-library-chart). Please, make sure that you have updated the chart dependencies before executing any upgrade. + +Consequences: + +Backwards compatibility is not guaranteed. To upgrade to `8.0.0`, install a new release of the MariaDB chart, and migrate the data from your previous release. You have 2 alternatives to do so: + +- Create a backup of the database, and restore it on the new release using tools such as [mysqldump](https://mariadb.com/kb/en/mysqldump/). +- Reuse the PVC used to hold the master data on your previous release. To do so, use the `primary.persistence.existingClaim` parameter. The following example assumes that the release name is `mariadb`: + +```bash +$ helm install mariadb bitnami/mariadb --set auth.rootPassword=[ROOT_PASSWORD] --set primary.persistence.existingClaim=[EXISTING_PVC] +``` + +| Note: you need to substitute the placeholder _[EXISTING_PVC]_ with the name of the PVC used on your previous release, and _[ROOT_PASSWORD]_ with the root password used in your previous release. + +### To 7.0.0 + +Helm performs a lookup for the object based on its group (apps), version (v1), and kind (Deployment). Also known as its GroupVersionKind, or GVK. Changing the GVK is considered a compatibility breaker from Kubernetes' point of view, so you cannot "upgrade" those objects to the new GVK in-place. Earlier versions of Helm 3 did not perform the lookup correctly which has since been fixed to match the spec. + +In https://github.com/helm/charts/pull/17308 the `apiVersion` of the statefulset resources was updated to `apps/v1` in tune with the api's deprecated, resulting in compatibility breakage. + +This major version bump signifies this change. + +### To 6.0.0 + +MariaDB version was updated from 10.1 to 10.3, there are no changes in the chart itself. According to the official documentation, upgrading from 10.1 should be painless. However, there are some things that have changed which could affect an upgrade: + +- [Incompatible changes upgrading from MariaDB 10.1 to MariaDB 10.2](https://mariadb.com/kb/en/library/upgrading-from-mariadb-101-to-mariadb-102//#incompatible-changes-between-101-and-102) +- [Incompatible changes upgrading from MariaDB 10.2 to MariaDB 10.3](https://mariadb.com/kb/en/library/upgrading-from-mariadb-102-to-mariadb-103/#incompatible-changes-between-102-and-103) + +### To 5.0.0 + +Backwards compatibility is not guaranteed unless you modify the labels used on the chart's deployments. +Use the workaround below to upgrade from versions previous to 5.0.0. The following example assumes that the release name is mariadb: + +```console +$ kubectl delete statefulset opencart-mariadb --cascade=false +``` diff --git a/reacthookspring-backend/deployment/helm/reacthook-spring/templates/NOTES.txt b/reacthookspring-backend/deployment/helm/reacthook-spring/templates/NOTES.txt new file mode 100644 index 0000000..d985df7 --- /dev/null +++ b/reacthookspring-backend/deployment/helm/reacthook-spring/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "reacthookspring.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "reacthookspring.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "reacthookspring.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "reacthookspring.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/reacthookspring-backend/deployment/helm/reacthook-spring/templates/_helpers.tpl b/reacthookspring-backend/deployment/helm/reacthook-spring/templates/_helpers.tpl new file mode 100644 index 0000000..c679435 --- /dev/null +++ b/reacthookspring-backend/deployment/helm/reacthook-spring/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "reacthookspring.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "reacthookspring.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "reacthookspring.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "reacthookspring.labels" -}} +helm.sh/chart: {{ include "reacthookspring.chart" . }} +{{ include "reacthookspring.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "reacthookspring.selectorLabels" -}} +app.kubernetes.io/name: {{ include "reacthookspring.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "reacthookspring.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "reacthookspring.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/reacthookspring-backend/deployment/helm/reacthook-spring/templates/deployment.yaml b/reacthookspring-backend/deployment/helm/reacthook-spring/templates/deployment.yaml new file mode 100644 index 0000000..7d20ca4 --- /dev/null +++ b/reacthookspring-backend/deployment/helm/reacthook-spring/templates/deployment.yaml @@ -0,0 +1,84 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "reacthookspring.fullname" . }} + labels: + {{- include "reacthookspring.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "reacthookspring.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "reacthookspring.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "reacthookspring.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: SPRING_DATASOURCE_URL + value: jdbc:mysql://{{ .Release.Name }}-mariadb:3306/{{ .Values.mariadb.auth.database }}?useLegacyDatetimeCode=false&serverTimezone=CET + - name: SPRING_DATASOURCE_USERNAME + value: {{ .Values.mariadb.auth.username }} + - name: SPRING_DATASOURCE_PASSWORD + value: {{ .Values.mariadb.auth.password }} + - name: SERVER_SERVLET_CONTEXT_PATH + value: /{{ include "reacthookspring.fullname" . }} + - name: SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GITHUB_CLIENTID + valueFrom: + secretKeyRef: + key: clientid + name: {{ .Release.Name }}-oauth + - name: SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GITHUB_CLIENTSECRET + valueFrom: + secretKeyRef: + key: clientsecret + name: {{ .Release.Name }}-oauth + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + livenessProbe: + httpGet: + path: /{{ include "reacthookspring.fullname" . }}/monitoring/health + port: http + initialDelaySeconds: 30 + periodSeconds: 5 + readinessProbe: + httpGet: + path: /{{ include "reacthookspring.fullname" . }}/monitoring/health + port: http + initialDelaySeconds: 30 + periodSeconds: 5 + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/reacthookspring-backend/deployment/helm/reacthook-spring/templates/githubPullSecret.yaml b/reacthookspring-backend/deployment/helm/reacthook-spring/templates/githubPullSecret.yaml new file mode 100644 index 0000000..c51aa7f --- /dev/null +++ b/reacthookspring-backend/deployment/helm/reacthook-spring/templates/githubPullSecret.yaml @@ -0,0 +1,16 @@ +{{- if .Values.github.registry.enabled }} +{{- $username := required ".github.registry.username not set" .Values.github.registry.username }} +{{- $pat := required ".github.registry.pat not set" .Values.github.registry.pat }} + +{{- $auth := printf "%s:%s" $username .Values.github.registry.pat | b64enc -}} +{{- $dockerconfigjson := printf "{\"auths\":{\"docker.pkg.github.com\":{\"auth\":\"%s\"}}}" $auth | b64enc | quote -}} + +kind: Secret +type: kubernetes.io/dockerconfigjson +apiVersion: v1 +metadata: + name: github-docker-pull + namespace: {{ .Release.Namespace }} +data: + .dockerconfigjson: {{ $dockerconfigjson }} +{{- end }} \ No newline at end of file diff --git a/reacthookspring-backend/deployment/helm/reacthook-spring/templates/hpa.yaml b/reacthookspring-backend/deployment/helm/reacthook-spring/templates/hpa.yaml new file mode 100644 index 0000000..43f4b0b --- /dev/null +++ b/reacthookspring-backend/deployment/helm/reacthook-spring/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "reacthookspring.fullname" . }} + labels: + {{- include "reacthookspring.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "reacthookspring.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/reacthookspring-backend/deployment/helm/reacthook-spring/templates/ingress.yaml b/reacthookspring-backend/deployment/helm/reacthook-spring/templates/ingress.yaml new file mode 100644 index 0000000..21aad64 --- /dev/null +++ b/reacthookspring-backend/deployment/helm/reacthook-spring/templates/ingress.yaml @@ -0,0 +1,39 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "reacthookspring.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "reacthookspring.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + - path: /{{ $fullName }} + backend: + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} diff --git a/reacthookspring-backend/deployment/helm/reacthook-spring/templates/oauthSecret.yaml b/reacthookspring-backend/deployment/helm/reacthook-spring/templates/oauthSecret.yaml new file mode 100644 index 0000000..084d745 --- /dev/null +++ b/reacthookspring-backend/deployment/helm/reacthook-spring/templates/oauthSecret.yaml @@ -0,0 +1,12 @@ +{{- if .Values.oauth.enabled }} +{{- $clientid := required ".oauth.clientid not set" .Values.oauth.clientid }} +{{- $clientsecret := required ".oauth.clientsecret not set" .Values.oauth.clientsecret }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Release.Name }}-oauth + namespace: {{ .Release.Namespace }} +data: + clientid: {{ .Values.oauth.clientid | b64enc }} + clientsecret: {{ .Values.oauth.clientsecret | b64enc }} +{{- end }} diff --git a/reacthookspring-backend/deployment/helm/reacthook-spring/templates/pv.yaml b/reacthookspring-backend/deployment/helm/reacthook-spring/templates/pv.yaml new file mode 100644 index 0000000..1c1b1ff --- /dev/null +++ b/reacthookspring-backend/deployment/helm/reacthook-spring/templates/pv.yaml @@ -0,0 +1,15 @@ +{{- if .Values.mariadb.localstorage -}} +{{- $fullName := include "reacthookspring.fullname" . -}} +apiVersion: v1 +kind: PersistentVolume +metadata: + name: {{ $fullName }}-pv +spec: + storageClassName: local-storage + capacity: + storage: 8Gi + accessModes: + - ReadWriteOnce + hostPath: + path: "/opt/local-pv/ljpb-mariadb" +{{- end }} diff --git a/reacthookspring-backend/deployment/helm/reacthook-spring/templates/service.yaml b/reacthookspring-backend/deployment/helm/reacthook-spring/templates/service.yaml new file mode 100644 index 0000000..9d41a3c --- /dev/null +++ b/reacthookspring-backend/deployment/helm/reacthook-spring/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "reacthookspring.fullname" . }} + labels: + {{- include "reacthookspring.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "reacthookspring.selectorLabels" . | nindent 4 }} diff --git a/reacthookspring-backend/deployment/helm/reacthook-spring/templates/serviceaccount.yaml b/reacthookspring-backend/deployment/helm/reacthook-spring/templates/serviceaccount.yaml new file mode 100644 index 0000000..5c45604 --- /dev/null +++ b/reacthookspring-backend/deployment/helm/reacthook-spring/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "reacthookspring.serviceAccountName" . }} + labels: + {{- include "reacthookspring.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/reacthookspring-backend/deployment/helm/reacthook-spring/templates/tests/test-connection.yaml b/reacthookspring-backend/deployment/helm/reacthook-spring/templates/tests/test-connection.yaml new file mode 100644 index 0000000..d5e9402 --- /dev/null +++ b/reacthookspring-backend/deployment/helm/reacthook-spring/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "reacthookspring.fullname" . }}-test-connection" + labels: + {{- include "reacthookspring.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "reacthookspring.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/reacthookspring-backend/deployment/helm/reacthook-spring/values-template4private.yaml b/reacthookspring-backend/deployment/helm/reacthook-spring/values-template4private.yaml new file mode 100644 index 0000000..90259bf --- /dev/null +++ b/reacthookspring-backend/deployment/helm/reacthook-spring/values-template4private.yaml @@ -0,0 +1,15 @@ +# Private values for helm install. + +mariadb: + auth: + rootPassword: root #change + password: reacthookspring #change + +github: + registry: + username: change + pat: change + +oauth: + clientid: change + clientsecret: change diff --git a/reacthookspring-backend/deployment/helm/reacthook-spring/values.yaml b/reacthookspring-backend/deployment/helm/reacthook-spring/values.yaml new file mode 100644 index 0000000..9484339 --- /dev/null +++ b/reacthookspring-backend/deployment/helm/reacthook-spring/values.yaml @@ -0,0 +1,99 @@ +# Default values for reacthookspring. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: docker.pkg.github.com/starwit/reacthookspring/reacthookspring + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: + - name: github-docker-pull +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: false + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 8081 + +ingress: + enabled: true + annotations: + cert-manager.io/issuer: letsencrypt-kub.starwit.de + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: kub.starwit.de + paths: [/reacthookspring] + tls: + - secretName: kub.starwit.de + hosts: + - kub.starwit.de + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +mariadb: + auth: + rootPassword: root + database: reacthookspring + username: reacthookspring + password: reacthookspring + +github: + registry: + enabled: true + username: undefined + pat: undefined + +oauth: + enabled: true + clientid: undefined + clientsecret: undefined diff --git a/reacthookspring-backend/deployment/https/docker-compose.yml b/reacthookspring-backend/deployment/https/docker-compose.yml new file mode 100644 index 0000000..58e5f14 --- /dev/null +++ b/reacthookspring-backend/deployment/https/docker-compose.yml @@ -0,0 +1,29 @@ +version: "3.9" +services: + nginx: + image: nginx:latest + restart: always + volumes: + - ./nginx-init-letsencrypt.conf:/etc/nginx/nginx.conf + - ./data/certbot/conf:/etc/letsencrypt + - ./data/certbot/www:/var/www/certbot + - ./conf.d:/etc/nginx/conf.d + - ./content:/var/www/html + ports: + - 80:80 + - 443:443 + networks: # Networks to join (Services on the same network can communicate with each other using their name) + - backend + + certbot: + image: certbot/certbot:latest + entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'" + command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'" + volumes: + - ./data/certbot/conf:/etc/letsencrypt + - ./certbot/logs:/var/log/letsencrypt + - ./data/certbot/www:/var/www/certbot + + # Networks to be created to facilitate communication between containers +networks: + backend: \ No newline at end of file diff --git a/reacthookspring-backend/deployment/https/env-docker-compose.yml b/reacthookspring-backend/deployment/https/env-docker-compose.yml new file mode 100644 index 0000000..5749b62 --- /dev/null +++ b/reacthookspring-backend/deployment/https/env-docker-compose.yml @@ -0,0 +1,72 @@ +version: "3.9" +services: + nginx: + image: nginx:latest + restart: always + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + - ./data/certbot/conf:/etc/letsencrypt + - ./data/certbot/www:/var/www/certbot + - ./conf.d:/etc/nginx/conf.d + - ./content:/var/www/html + ports: + - 80:80 + - 443:443 + networks: # Networks to join (Services on the same network can communicate with each other using their name) + - backend + + certbot: + image: certbot/certbot:latest + entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'" + command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'" + volumes: + - ./data/certbot/conf:/etc/letsencrypt + - ./certbot/logs:/var/log/letsencrypt + - ./data/certbot/www:/var/www/certbot + + db-keycloak: + image: mariadb:latest + restart: on-failure + environment: + MYSQL_DATABASE: 'keycloak' + MYSQL_USER: 'keycloak' + MYSQL_PASSWORD: ${DB_PW_KEYCLOAK} + MYSQL_ALLOW_EMPTY_PASSWORD: 'no' + MYSQL_ROOT_PASSWORD: ${DB_PW_KC_ROOT} + MYSQL_TCP_PORT: 3307 + healthcheck: + test: mysqladmin ping -h 127.0.0.1 -P 3307 -u $$MYSQL_USER --password=$$MYSQL_PASSWORD + interval: 5s + timeout: 60s + retries: 30 + volumes: + - /mnt/lj-mariadb/keycloak:/var/lib/mysql + networks: + - backend + + keycloak: + image: jboss/keycloak + volumes: + - ./imports:/opt/jboss/keycloak/imports + depends_on: + db-keycloak: + condition: service_healthy + restart: on-failure + environment: + KEYCLOAK_IMPORT: /opt/jboss/keycloak/imports/realm.json + KEYCLOAK_USER: ${KEYCLOAK_USER} + KEYCLOAK_PASSWORD: ${KEYCLOAK_PW} + DB_VENDOR: mariadb + DB_ADDR: db-keycloak + DB_PORT: 3307 + DB_USER: 'keycloak' + DB_PASSWORD: ${DB_PW_KEYCLOAK} + PROXY_ADDRESS_FORWARDING: 'true' + KEYCLOAK_FRONTEND_URL: 'https://${DOMAIN}/auth' + networks: + - backend + + # Networks to be created to facilitate communication between containers +networks: + backend: + diff --git a/reacthookspring-backend/deployment/https/env.sh b/reacthookspring-backend/deployment/https/env.sh new file mode 100644 index 0000000..6228249 --- /dev/null +++ b/reacthookspring-backend/deployment/https/env.sh @@ -0,0 +1,9 @@ +#!/bin/bash +export DB_PW_reacthookspring= +export KEYCLOAK_PW= +export DB_PW_KEYCLOAK= +export DB_PW_PB_ROOT= +export DB_PW_KC_ROOT= +export DB_PW_PB_ROOT= +export KEYCLOAK_USER="reacthookspringadmin" +export DOMAIN="pb.starwit.de" diff --git a/reacthookspring-backend/deployment/https/imports/realm.json b/reacthookspring-backend/deployment/https/imports/realm.json new file mode 100644 index 0000000..c8a6430 --- /dev/null +++ b/reacthookspring-backend/deployment/https/imports/realm.json @@ -0,0 +1,131 @@ +{ + "id": "reacthookspring", + "realm": "reacthookspring", + "enabled": true, + "roles": { + "realm": [ + { + "name": "ROLE_user", + "composite": false, + "clientRole": false, + "containerId": "reacthookspring", + "attributes": {} + }, + { + "name": "ROLE_admin", + "composite": false, + "clientRole": false, + "containerId": "reacthookspring", + "attributes": {} + }, + { + "name": "ROLE_reader", + "composite": false, + "clientRole": false, + "containerId": "reacthookspring", + "attributes": {} + } + ], + "client": { + "reacthookspring": [] + } + }, + "groups": [ + { + "name": "reacthookspring-default", + "path": "/reacthookspring-default", + "attributes": {}, + "realmRoles": [ + "ROLE_reader" + ], + "clientRoles": {}, + "subGroups": [] + } + ], + "defaultGroups": [ + "/reacthookspring-default" + ], + "clients": [ + { + "clientId": "reacthookspring", + "name": "reacthookspring", + "rootUrl": "https://pb.starwit.de", + "baseUrl": "https://pb.starwit.de", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "https://pb.starwit.de/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "id.token.as.detached.signature": "false", + "saml.assertion.signature": "false", + "saml.force.post.binding": "false", + "saml.multivalued.roles": "false", + "saml.encrypt": "false", + "login_theme": "keycloak", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false", + "saml.server.signature": "false", + "saml.server.signature.keyinfo.ext": "false", + "use.refresh.tokens": "true", + "exclude.session.state.from.auth.response": "false", + "oidc.ciba.grant.enabled": "false", + "saml.artifact.binding": "false", + "backchannel.logout.session.required": "true", + "client_credentials.use_refresh_token": "false", + "saml_force_name_id_format": "false", + "require.pushed.authorization.requests": "false", + "saml.client.signature": "false", + "tls.client.certificate.bound.access.tokens": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-group-membership-mapper", + "consentRequired": false, + "config": { + "full.path": "false", + "id.token.claim": "false", + "access.token.claim": "true", + "claim.name": "groups", + "userinfo.token.claim": "true" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ] +} \ No newline at end of file diff --git a/reacthookspring-backend/deployment/https/init-letsencrypt.sh b/reacthookspring-backend/deployment/https/init-letsencrypt.sh new file mode 100644 index 0000000..4899d2c --- /dev/null +++ b/reacthookspring-backend/deployment/https/init-letsencrypt.sh @@ -0,0 +1,79 @@ +#!/bin/bash +# This script creates the first certificats by using docker-compose (nginx and cerbot is established). +if ! [ -x "$(command -v docker-compose)" ]; then + echo 'Error: docker-compose is not installed.' >&2 + exit 1 +fi + +domains=(pb.starwit.de) +rsa_key_size=4096 +data_path="./data/certbot" +email="anett.huebner@starwit.de" # Adding a valid address is strongly recommended +staging=0 # Set to 1 if you're testing your setup to avoid hitting request limits + +if [ -d "$data_path" ]; then + read -p "Existing data found for $domains. Continue and replace existing certificate? (y/N) " decision + if [ "$decision" != "Y" ] && [ "$decision" != "y" ]; then + exit + fi +fi + + +if [ ! -e "$data_path/conf/options-ssl-nginx.conf" ] || [ ! -e "$data_path/conf/ssl-dhparams.pem" ]; then + echo "### Downloading recommended TLS parameters ..." + mkdir -p "$data_path/conf" + curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf > "$data_path/conf/options-ssl-nginx.conf" + curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem > "$data_path/conf/ssl-dhparams.pem" + echo +fi + +echo "### Creating dummy certificate for $domains ..." +path="/etc/letsencrypt/live/$domains" +mkdir -p "$data_path/conf/live/$domains" +docker-compose run --rm --entrypoint "\ + openssl req -x509 -nodes -newkey rsa:$rsa_key_size -days 1\ + -keyout '$path/privkey.pem' \ + -out '$path/fullchain.pem' \ + -subj '/CN=localhost'" certbot +echo + + +echo "### Starting nginx ..." +docker-compose up --force-recreate -d nginx +echo + +echo "### Deleting dummy certificate for $domains ..." +docker-compose run --rm --entrypoint "\ + rm -Rf /etc/letsencrypt/live/$domains && \ + rm -Rf /etc/letsencrypt/archive/$domains && \ + rm -Rf /etc/letsencrypt/renewal/$domains.conf" certbot +echo + +echo "### Requesting Let's Encrypt certificate for $domains ..." +#Join $domains to -d args +domain_args="" +for domain in "${domains[@]}"; do + domain_args="$domain_args -d $domain" +done + +# Select appropriate email arg +case "$email" in + "") email_arg="--register-unsafely-without-email" ;; + *) email_arg="--email $email" ;; +esac + +# Enable staging mode if needed +if [ $staging != "0" ]; then staging_arg="--staging"; fi + +docker-compose run --rm --entrypoint "\ + certbot certonly --webroot -w /var/www/certbot \ + $staging_arg \ + $email_arg \ + $domain_args \ + --rsa-key-size $rsa_key_size \ + --agree-tos \ + --force-renewal" certbot +echo + +echo "### Reloading nginx ..." +docker-compose exec nginx nginx -s reload diff --git a/reacthookspring-backend/deployment/https/nginx-init-letsencrypt.conf b/reacthookspring-backend/deployment/https/nginx-init-letsencrypt.conf new file mode 100644 index 0000000..f10e76e --- /dev/null +++ b/reacthookspring-backend/deployment/https/nginx-init-letsencrypt.conf @@ -0,0 +1,65 @@ +events { + +} + +http { + + ## + # Basic Settings + ## + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + # server_tokens off; + + # server_names_hash_bucket_size 64; + # server_name_in_redirect off; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + ## + # SSL Settings + ## + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE + ssl_prefer_server_ciphers on; + + ## + # Logging Settings + ## + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + gzip on; + + server { + listen [::]:80; + listen 80; + + server_name pb.starwit.de; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + } + + server { + listen [::]:443 ssl http2; + listen 443 ssl http2; + + server_name pb.starwit.de; + + # SSL code + ssl_certificate /etc/letsencrypt/live/pb.starwit.de/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/pb.starwit.de/privkey.pem; + + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + } +} \ No newline at end of file diff --git a/reacthookspring-backend/deployment/https/nginx.conf b/reacthookspring-backend/deployment/https/nginx.conf new file mode 100644 index 0000000..e210636 --- /dev/null +++ b/reacthookspring-backend/deployment/https/nginx.conf @@ -0,0 +1,100 @@ +events { + +} + +http { + + ## + # Basic Settings + ## + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + # server_tokens off; + + # server_names_hash_bucket_size 64; + # server_name_in_redirect off; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + ## + # SSL Settings + ## + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE + ssl_prefer_server_ciphers on; + + ## + # Logging Settings + ## + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + gzip on; + + server { + listen [::]:80; + listen 80; + + server_name pb.starwit.de; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location /reacthookspring/ { + return 301 https://$host$request_uri; + } + + location /auth/ { + return 301 https://$host$request_uri; + } + + location / { + return 301 https://$host/reacthookspring/; + } + } + + server { + listen [::]:443 ssl http2; + listen 443 ssl http2; + + server_name pb.starwit.de; + + # SSL code + ssl_certificate /etc/letsencrypt/live/pb.starwit.de/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/pb.starwit.de/privkey.pem; + + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + location /reacthookspring/ { + proxy_pass http://reacthookspring:8081/reacthookspring/; + proxy_set_header X-Forwarded-Host $host:$server_port; + proxy_set_header X-Forwarded-Server $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + } + + location /auth/ { + proxy_pass http://keycloak:8080/auth/; + proxy_set_header X-Forwarded-Host $host:$server_port; + proxy_set_header X-Forwarded-Server $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + } + + location / { + proxy_pass http://reacthookspring:8081/reacthookspring/; + proxy_set_header X-Forwarded-Host $host:$server_port; + proxy_set_header X-Forwarded-Server $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + } + } +} \ No newline at end of file diff --git a/reacthookspring-backend/deployment/https/reacthookspring-docker-compose.yml b/reacthookspring-backend/deployment/https/reacthookspring-docker-compose.yml new file mode 100644 index 0000000..639195e --- /dev/null +++ b/reacthookspring-backend/deployment/https/reacthookspring-docker-compose.yml @@ -0,0 +1,48 @@ +version: "3.9" +services: + db: + image: mariadb:latest + restart: on-failure + environment: + MYSQL_DATABASE: 'reacthookspring' + # So you don't have to use root, but you can if you like + MYSQL_USER: 'reacthookspring' + # You can use whatever password you like + MYSQL_PASSWORD: ${DB_PW_reacthookspring} + # Password for root access + MYSQL_ALLOW_EMPTY_PASSWORD: 'no' + MYSQL_ROOT_PASSWORD: ${DB_PW_PB_ROOT} + healthcheck: + test: mysqladmin ping -h 127.0.0.1 -u $$MYSQL_USER --password=$$MYSQL_PASSWORD + interval: 5s + timeout: 60s + retries: 30 + expose: + # Opens port 3306 on the container + - '3306' + # Where our data will be persisted + volumes: + - /mnt/lj-mariadb/reacthookspring:/var/lib/mysql + networks: # Networks to join (Services on the same network can communicate with each other using their name) + - backend + + reacthookspring: + image: starwitorg/reacthookspring:v0.2.13-9 + depends_on: + db: + condition: service_healthy + restart: on-failure + environment: + SPRING_DATASOURCE_URL: jdbc:mariadb://db:3306/reacthookspring?useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false + SPRING_DATASOURCE_USERNAME: reacthookspring + SPRING_DATASOURCE_PASSWORD: ${DB_PW_reacthookspring} + KEYCLOAK_AUTH-SERVER-URL: https://pb.starwit.de/auth + SERVER_USE_FORWARD_HEADERS: "true" + SERVER_FORWARD_HEADERS_STRATEGY: FRAMEWORK + networks: # Networks to join (Services on the same network can communicate with each other using their name) + - backend + + # Networks to be created to facilitate communication between containers +networks: + backend: + diff --git a/reacthookspring-backend/deployment/https/readme.md b/reacthookspring-backend/deployment/https/readme.md new file mode 100644 index 0000000..b6cbd3c --- /dev/null +++ b/reacthookspring-backend/deployment/https/readme.md @@ -0,0 +1,39 @@ +This is configured for running on server for domain pb.starwit.de. + +* 1. if you change domain configure / check domain in all files of this folder +* 2. create server on hetzner +* 3. create volume on named lj-mariadb +* 4. login to server via ssh +* 5. setup machine and install docker compose: + +```bash + apt update + apt install \ + apt-transport-https \ + ca-certificates \ + curl \ + gnupg-agent \ + software-properties-common + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - + add-apt-repository \ + "deb [arch=amd64] https://download.docker.com/linux/ubuntu \ + $(lsb_release -cs) \ + stable" + apt update + + # Install correct version of docker-compose + curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + chmod +x /usr/local/bin/docker-compose + ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose + + apt install docker-ce docker-ce-cli containerd.io docker-compose +``` +* 6. restart machine and relogin +* 7. checkout project and go to `/reacthookspring/deployment/https` +* 8. set passwords in env.sh and execute `source env.sh` to set environment variables dor docker compose scripts +* 9. execute `bash init-letsencrypt.sh` to get initial credentials. It uses docker-compose.yml (nginx + certbot) in order to get initial letsencrypt certificates. +* 10. execute `reacthookspring-docker-compose up -d` and `env-docker-compose up -d` + * !!! if you change database passwords after initial starting of docker compose scripts, it wont't work +* 11. chech network and open ports `netstat -tulpn` +* go to https://pb.startwit.de/auth, login and create user for reacthookspring +* check prohectbuilder on https://pb.startwit.de/ diff --git a/reacthookspring-backend/deployment/keycloak/imports/realm.json b/reacthookspring-backend/deployment/keycloak/imports/realm.json new file mode 100644 index 0000000..b63daf2 --- /dev/null +++ b/reacthookspring-backend/deployment/keycloak/imports/realm.json @@ -0,0 +1,131 @@ +{ + "id": "reacthookspring", + "realm": "reacthookspring", + "enabled": true, + "roles": { + "realm": [ + { + "name": "ROLE_user", + "composite": false, + "clientRole": false, + "containerId": "reacthookspring", + "attributes": {} + }, + { + "name": "ROLE_admin", + "composite": false, + "clientRole": false, + "containerId": "reacthookspring", + "attributes": {} + }, + { + "name": "ROLE_reader", + "composite": false, + "clientRole": false, + "containerId": "reacthookspring", + "attributes": {} + } + ], + "client": { + "reacthookspring": [] + } + }, + "groups": [ + { + "name": "public", + "path": "/public", + "attributes": {}, + "realmRoles": [ + "ROLE_reader" + ], + "clientRoles": {}, + "subGroups": [] + } + ], + "defaultGroups": [ + "/public" + ], + "clients": [ + { + "clientId": "reacthookspring", + "name": "reacthookspring", + "rootUrl": "", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "id.token.as.detached.signature": "false", + "saml.assertion.signature": "false", + "saml.force.post.binding": "false", + "saml.multivalued.roles": "false", + "saml.encrypt": "false", + "login_theme": "keycloak", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false", + "saml.server.signature": "false", + "saml.server.signature.keyinfo.ext": "false", + "use.refresh.tokens": "true", + "exclude.session.state.from.auth.response": "false", + "oidc.ciba.grant.enabled": "false", + "saml.artifact.binding": "false", + "backchannel.logout.session.required": "true", + "client_credentials.use_refresh_token": "false", + "saml_force_name_id_format": "false", + "require.pushed.authorization.requests": "false", + "saml.client.signature": "false", + "tls.client.certificate.bound.access.tokens": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-group-membership-mapper", + "consentRequired": false, + "config": { + "full.path": "false", + "id.token.claim": "false", + "access.token.claim": "true", + "claim.name": "groups", + "userinfo.token.claim": "true" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ] +} \ No newline at end of file diff --git a/reacthookspring-backend/deployment/keycloak/local-test-users.json b/reacthookspring-backend/deployment/keycloak/local-test-users.json new file mode 100644 index 0000000..c765796 --- /dev/null +++ b/reacthookspring-backend/deployment/keycloak/local-test-users.json @@ -0,0 +1,49 @@ +[ { + "realm" : "reacthookspring", + "users" : [ { + "username" : "admin", + "enabled" : true, + "credentials" : [ { + "type" : "password", + "secretData" : "{\"value\":\"o2sxyw56EK8jKnh0CQSV2FiN9plXTSzwOWNlt7SiIUd/Ox+huiDWYGlCL4gqSM0R/3Q+PONjG0Dl7dQrvtfU2A==\",\"salt\":\"m63ldw2Y1VagMqRIEhQZOg==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":100000,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "realmRoles" : [ "ROLE_admin" ] + } ] +}, { + "realm" : "reacthookspring", + "users" : [ { + "username" : "user", + "enabled" : true, + "credentials" : [ { + "type" : "password", + "secretData" : "{\"value\":\"o2sxyw56EK8jKnh0CQSV2FiN9plXTSzwOWNlt7SiIUd/Ox+huiDWYGlCL4gqSM0R/3Q+PONjG0Dl7dQrvtfU2A==\",\"salt\":\"m63ldw2Y1VagMqRIEhQZOg==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":100000,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "realmRoles" : [ "ROLE_user" ] + } ] +}, { + "realm" : "reacthookspring", + "users" : [ { + "username" : "reader", + "enabled" : true, + "credentials" : [ { + "type" : "password", + "secretData" : "{\"value\":\"o2sxyw56EK8jKnh0CQSV2FiN9plXTSzwOWNlt7SiIUd/Ox+huiDWYGlCL4gqSM0R/3Q+PONjG0Dl7dQrvtfU2A==\",\"salt\":\"m63ldw2Y1VagMqRIEhQZOg==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":100000,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "realmRoles" : [ "ROLE_reader" ] + } ] +}, { + "realm" : "master", + "users" : [ { + "username" : "admin", + "enabled" : true, + "credentials" : [ { + "type" : "password", + "secretData" : "{\"value\":\"o2sxyw56EK8jKnh0CQSV2FiN9plXTSzwOWNlt7SiIUd/Ox+huiDWYGlCL4gqSM0R/3Q+PONjG0Dl7dQrvtfU2A==\",\"salt\":\"m63ldw2Y1VagMqRIEhQZOg==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":100000,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "realmRoles" : [ "admin" ] + } ] +} ] \ No newline at end of file diff --git a/reacthookspring-backend/deployment/keycloak/readme.md b/reacthookspring-backend/deployment/keycloak/readme.md new file mode 100644 index 0000000..453f15f --- /dev/null +++ b/reacthookspring-backend/deployment/keycloak/readme.md @@ -0,0 +1,19 @@ +# info keycloak + +run local keycloak with docker command: + +```bash +docker run -d --name keycloak -p 8080:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin jboss/keycloak +``` +Click add realm and import mapcollector-realm.json + +start/stop container with +```bash +docker stop keycloak +docker start keycloak +``` +In keycloak: + +* add user +* set password under tab credentials +* assign roles tu user (Role Mappings) \ No newline at end of file diff --git a/reacthookspring-backend/deployment/localenv-docker-compose.yml b/reacthookspring-backend/deployment/localenv-docker-compose.yml new file mode 100644 index 0000000..44777b2 --- /dev/null +++ b/reacthookspring-backend/deployment/localenv-docker-compose.yml @@ -0,0 +1,77 @@ +version: "3.9" +services: + reacthookspring-db: + image: mariadb:latest + restart: on-failure + environment: + MYSQL_DATABASE: 'reacthookspring' + # So you don't have to use root, but you can if you like + MYSQL_USER: 'reacthookspring' + # You can use whatever password you like + MYSQL_PASSWORD: 'reacthookspring' + # Password for root access + MYSQL_ALLOW_EMPTY_PASSWORD: 'yes' + ports: + # : < MySQL Port running inside container> + - '3306:3306' + healthcheck: + test: ["CMD", "mysql" ,"-h", "localhost", "-P", "3306", "-u", "root", "-e", "select 1", "reacthookspring"] + interval: 5s + timeout: 60s + retries: 30 + volumes: + - reacthookspringv2-db-data:/var/lib/mysql + networks: # Networks to join (Services on the same network can communicate with each other using their name) + - backend + + reacthookspring-db-keycloak: + image: mariadb:latest + restart: on-failure + environment: + MYSQL_DATABASE: 'keycloak' + MYSQL_USER: 'keycloak' + MYSQL_PASSWORD: 'keycloak' + MYSQL_ALLOW_EMPTY_PASSWORD: 'yes' + MYSQL_TCP_PORT: 3307 + healthcheck: + test: ["CMD", "mysql" ,"-h", "localhost", "-P", "3307", "-u", "root", "-e", "select 1", "keycloak"] + interval: 5s + timeout: 60s + retries: 30 + volumes: + - reacthookspring-keycloak-db-data:/var/lib/mysql + networks: + - backend + + reacthookspring-keycloak: + image: jboss/keycloak + volumes: + - ./keycloak/imports:/opt/jboss/keycloak/imports + - ./keycloak/local-test-users.json:/opt/jboss/keycloak/standalone/configuration/keycloak-add-user.json + depends_on: + reacthookspring-db-keycloak: + condition: service_healthy + restart: on-failure + environment: + KEYCLOAK_IMPORT: /opt/jboss/keycloak/imports/realm.json + DB_VENDOR: mariadb + DB_ADDR: reacthookspring-db-keycloak + DB_PORT: 3307 + DB_USER: 'keycloak' + DB_PASSWORD: 'keycloak' + PROXY_ADDRESS_FORWARDING: 'true' + KEYCLOAK_FRONTEND_URL: 'http://localhost:8080/auth' + ports: + # : < MySQL Port running inside container> + - '8080:8080' + networks: + - backend + +# Names our volume +volumes: + reacthookspringv2-db-data: + reacthookspring-keycloak-db-data: + + # Networks to be created to facilitate communication between containers +networks: + backend: diff --git a/reacthookspring-backend/deployment/mysqllocal-docker-compose.yml b/reacthookspring-backend/deployment/mysqllocal-docker-compose.yml new file mode 100644 index 0000000..dfe72cd --- /dev/null +++ b/reacthookspring-backend/deployment/mysqllocal-docker-compose.yml @@ -0,0 +1,33 @@ +version: "3.9" +services: + db: + image: mariadb:latest + restart: on-failure + environment: + MYSQL_DATABASE: 'reacthookspring' + # So you don't have to use root, but you can if you like + MYSQL_USER: 'reacthookspring' + # You can use whatever password you like + MYSQL_PASSWORD: 'reacthookspring' + # Password for root access + MYSQL_ALLOW_EMPTY_PASSWORD: 'yes' + ports: + # : < MySQL Port running inside container> + - '3306:3306' + healthcheck: + test: ["CMD", "mysql" ,"-h", "localhost", "-P", "3306", "-u", "root", "-e", "select 1", "reacthookspring"] + interval: 5s + timeout: 60s + retries: 30 + volumes: + - reacthookspringv2-db-data:/var/lib/mysql + networks: # Networks to join (Services on the same network can communicate with each other using their name) + - backend + +# Names our volume +volumes: + reacthookspringv2-db-data: + + # Networks to be created to facilitate communication between containers +networks: + backend: diff --git a/reacthookspring-backend/dockerfile b/reacthookspring-backend/dockerfile new file mode 100755 index 0000000..3f967af --- /dev/null +++ b/reacthookspring-backend/dockerfile @@ -0,0 +1,7 @@ +FROM openjdk:11-jre-slim-buster +# copy application JAR (with libraries inside) + +ADD application.jar /opt/application.jar +RUN chmod +x /opt/application.jar +# specify default command +CMD ["/usr/local/openjdk-11/bin/java", "-jar", "/opt/application.jar"] diff --git a/reacthookspring-backend/persistence/.gitignore b/reacthookspring-backend/persistence/.gitignore new file mode 100755 index 0000000..ba5cb5c --- /dev/null +++ b/reacthookspring-backend/persistence/.gitignore @@ -0,0 +1,26 @@ +HELP.md +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ diff --git a/reacthookspring-backend/persistence/pom.xml b/reacthookspring-backend/persistence/pom.xml new file mode 100644 index 0000000..bd1f785 --- /dev/null +++ b/reacthookspring-backend/persistence/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + de.starwit + reacthookspring + 0.0.1-SNAPSHOT + + + persistence + jar + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-validation + + + com.h2database + h2 + test + + + org.mariadb.jdbc + mariadb-java-client + runtime + + + org.flywaydb + flyway-mysql + + + org.flywaydb + flyway-core + + + org.springframework.boot + spring-boot-starter-log4j2 + + + org.springframework.boot + spring-boot-starter-test + test + + + com.fasterxml.jackson.core + jackson-annotations + + + org.springdoc + springdoc-openapi-security + + + + diff --git a/reacthookspring-backend/persistence/src/main/java/de/starwit/persistence/PersistenceApplication.java b/reacthookspring-backend/persistence/src/main/java/de/starwit/persistence/PersistenceApplication.java new file mode 100755 index 0000000..b41556c --- /dev/null +++ b/reacthookspring-backend/persistence/src/main/java/de/starwit/persistence/PersistenceApplication.java @@ -0,0 +1,13 @@ +package de.starwit.persistence; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class PersistenceApplication { + + public static void main(String[] args) { + SpringApplication.run(PersistenceApplication.class, args); + } + +} diff --git a/reacthookspring-backend/persistence/src/main/java/de/starwit/persistence/config/DatabasePhysicalNamingStrategy.java b/reacthookspring-backend/persistence/src/main/java/de/starwit/persistence/config/DatabasePhysicalNamingStrategy.java new file mode 100644 index 0000000..199a2a4 --- /dev/null +++ b/reacthookspring-backend/persistence/src/main/java/de/starwit/persistence/config/DatabasePhysicalNamingStrategy.java @@ -0,0 +1,42 @@ +package de.starwit.persistence.config; + +import org.hibernate.boot.model.naming.Identifier; +import org.hibernate.boot.model.naming.PhysicalNamingStrategy; +import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; + +public class DatabasePhysicalNamingStrategy implements PhysicalNamingStrategy { + + @Override + public Identifier toPhysicalCatalogName(final Identifier identifier, final JdbcEnvironment jdbcEnv) { + return identifier; + } + + @Override + public Identifier toPhysicalColumnName(final Identifier identifier, final JdbcEnvironment jdbcEnv) { + return toUpperSnakeCase(identifier); + } + + @Override + public Identifier toPhysicalSchemaName(final Identifier identifier, final JdbcEnvironment jdbcEnv) { + return identifier; + } + + @Override + public Identifier toPhysicalSequenceName(final Identifier identifier, final JdbcEnvironment jdbcEnv) { + return toUpperSnakeCase(identifier); + } + + @Override + public Identifier toPhysicalTableName(final Identifier identifier, final JdbcEnvironment jdbcEnv) { + return toUpperSnakeCase(identifier); + } + + private Identifier toUpperSnakeCase(final Identifier identifier) { + final String regex = "([a-z])([A-Z])"; + final String replacement = "$1_$2"; + final String upperSnakeIdentifier = identifier.getText() + .replaceAll(regex, replacement) + .toUpperCase(); + return Identifier.toIdentifier(upperSnakeIdentifier); + } +} diff --git a/reacthookspring-backend/persistence/src/main/java/de/starwit/persistence/converter/ListToStringConverter.java b/reacthookspring-backend/persistence/src/main/java/de/starwit/persistence/converter/ListToStringConverter.java new file mode 100644 index 0000000..41aab98 --- /dev/null +++ b/reacthookspring-backend/persistence/src/main/java/de/starwit/persistence/converter/ListToStringConverter.java @@ -0,0 +1,25 @@ +package de.starwit.persistence.converter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.persistence.AttributeConverter; + +public class ListToStringConverter implements AttributeConverter, String> { + @Override + public String convertToDatabaseColumn(List attribute) { + return attribute == null ? null : "," + String.join(",", attribute) + ","; + } + + @Override + public List convertToEntityAttribute(String dbData) { + ArrayList items = new ArrayList<>(); + if (dbData != null) { + Collections.addAll(items, dbData.split(",")); + } + items.removeIf(item -> item == null || "".equals(item)); + items.sort(String.CASE_INSENSITIVE_ORDER); + return items; + } +} diff --git a/reacthookspring-backend/persistence/src/main/java/de/starwit/persistence/entity/AbstractEntity.java b/reacthookspring-backend/persistence/src/main/java/de/starwit/persistence/entity/AbstractEntity.java new file mode 100755 index 0000000..4af94a8 --- /dev/null +++ b/reacthookspring-backend/persistence/src/main/java/de/starwit/persistence/entity/AbstractEntity.java @@ -0,0 +1,37 @@ +package de.starwit.persistence.entity; + +import java.io.Serializable; + +import javax.persistence.Column; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.MappedSuperclass; + +@MappedSuperclass +public abstract class AbstractEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", unique = true, nullable = false) + protected PK id; + + /** + * + */ + public AbstractEntity() { + super(); + } + + /** + * @return + */ + public PK getId() { + return this.id; + } + + public void setId(PK id) { + this.id = id; + } + +} diff --git a/reacthookspring-backend/persistence/src/main/java/de/starwit/persistence/exception/NotificationException.java b/reacthookspring-backend/persistence/src/main/java/de/starwit/persistence/exception/NotificationException.java new file mode 100755 index 0000000..e19c582 --- /dev/null +++ b/reacthookspring-backend/persistence/src/main/java/de/starwit/persistence/exception/NotificationException.java @@ -0,0 +1,36 @@ +package de.starwit.persistence.exception; + +public class NotificationException extends Exception { + + private static final long serialVersionUID = 1L; + private String exceptionKey; + private String exceptionMessage; + private String[] parameters; + + public NotificationException(String exceptionKey, String exceptionMessage, String... parameters) { + super("Error during app setup or generation."); + this.exceptionKey = exceptionKey; + this.exceptionMessage = exceptionMessage; + this.parameters = parameters; + } + + public String getExceptionKey() { + return exceptionKey; + } + + public void setExceptionKey(String exceptionKey) { + this.exceptionKey = exceptionKey; + } + + public String getExceptionMessage() { + return exceptionMessage; + } + + public void setExceptionMessage(String exceptionMessage) { + this.exceptionMessage = exceptionMessage; + } + + public String[] getParameters() { + return this.parameters; + } +} diff --git a/reacthookspring-backend/persistence/src/main/java/de/starwit/persistence/serializer/ZonedDateTimeDeserializer.java b/reacthookspring-backend/persistence/src/main/java/de/starwit/persistence/serializer/ZonedDateTimeDeserializer.java new file mode 100644 index 0000000..6db156c --- /dev/null +++ b/reacthookspring-backend/persistence/src/main/java/de/starwit/persistence/serializer/ZonedDateTimeDeserializer.java @@ -0,0 +1,16 @@ +package de.starwit.persistence.serializer; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.time.ZonedDateTime; + +public class ZonedDateTimeDeserializer extends JsonDeserializer { + + @Override + public ZonedDateTime deserialize(JsonParser arg0, DeserializationContext arg1) throws IOException { + return ZonedDateTime.parse(arg0.getText()); + } +} diff --git a/reacthookspring-backend/persistence/src/main/java/de/starwit/persistence/serializer/ZonedDateTimeSerializer.java b/reacthookspring-backend/persistence/src/main/java/de/starwit/persistence/serializer/ZonedDateTimeSerializer.java new file mode 100644 index 0000000..55e1031 --- /dev/null +++ b/reacthookspring-backend/persistence/src/main/java/de/starwit/persistence/serializer/ZonedDateTimeSerializer.java @@ -0,0 +1,19 @@ +package de.starwit.persistence.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +public class ZonedDateTimeSerializer extends JsonSerializer { + + @Override + public void serialize(ZonedDateTime date, JsonGenerator jgen, SerializerProvider provider) throws IOException { + jgen.writeString(date != null ? ZonedDateTime.ofInstant(date.toInstant(), ZoneId.of("UTC")) + .format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")) : null); + } +} diff --git a/reacthookspring-backend/persistence/src/main/resources/application.properties b/reacthookspring-backend/persistence/src/main/resources/application.properties new file mode 100644 index 0000000..74f5089 --- /dev/null +++ b/reacthookspring-backend/persistence/src/main/resources/application.properties @@ -0,0 +1,4 @@ +spring.flyway.baselineOnMigrate=true +spring.flyway.locations=classpath:db/migration +spring.flyway.encoding=UTF-8 +spring.flyway.placeholder-replacement=false diff --git a/reacthookspring-backend/persistence/src/main/resources/banner.txt b/reacthookspring-backend/persistence/src/main/resources/banner.txt new file mode 100755 index 0000000..4842295 --- /dev/null +++ b/reacthookspring-backend/persistence/src/main/resources/banner.txt @@ -0,0 +1,6 @@ + ,--. ,--. + ,---. ,---. ,--.--. ,---. `--' ,---. ,-' '-. ,---. ,--,--, ,---. ,---. +| .-. | | .-. : | .--' ( .-' ,--. ( .-' '-. .-' | .-. : | \ | .--' | .-. : +| '-' ' \ --. | | .-' `) | | .-' `) | | \ --. | || | \ `--. \ --. +| |-' `----' `--' `----' `--' `----' `--' `----' `--''--' `---' `----' +`--' \ No newline at end of file diff --git a/reacthookspring-backend/persistence/src/main/resources/db/migration/V0_0__empty.sql b/reacthookspring-backend/persistence/src/main/resources/db/migration/V0_0__empty.sql new file mode 100644 index 0000000..e69de29 diff --git a/reacthookspring-backend/persistence/src/test/resources/application.properties b/reacthookspring-backend/persistence/src/test/resources/application.properties new file mode 100644 index 0000000..99303fa --- /dev/null +++ b/reacthookspring-backend/persistence/src/test/resources/application.properties @@ -0,0 +1,6 @@ +spring.flyway.baselineOnMigrate=true +spring.flyway.locations=classpath:db/test +spring.flyway.encoding=UTF-8 +spring.flyway.placeholder-replacement=false +spring.datasource.url=jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=MYSQL +spring.flyway.url=${spring.datasource.url} diff --git a/reacthookspring-backend/persistence/src/test/resources/db/test/V0_0__empty.sql b/reacthookspring-backend/persistence/src/test/resources/db/test/V0_0__empty.sql new file mode 100644 index 0000000..e69de29 diff --git a/reacthookspring-backend/pom.xml b/reacthookspring-backend/pom.xml new file mode 100644 index 0000000..fc2ce7e --- /dev/null +++ b/reacthookspring-backend/pom.xml @@ -0,0 +1,211 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.6.6 + + + de.starwit + reacthookspring + 0.0.1-SNAPSHOT + App template from test @ spring + pom + + + UTF-8 + UTF-8 + 2.6.6 + 5.3.20 + 2.17.2 + 1.6.7 + 2.7.4 + + + + application + rest + service + persistence + + + + + + org.springframework + spring-core + ${spring-version} + + + org.springframework.boot + spring-boot-starter + ${spring-boot-version} + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + ${spring-boot-version} + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-security + ${spring-boot-version} + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-test + test + ${spring-boot-version} + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-actuator + ${spring-boot-version} + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-validation + ${spring-boot-version} + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-data-jpa + ${spring-boot-version} + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-data-rest + ${spring-boot-version} + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-log4j2 + ${spring-boot-version} + + + org.springdoc + springdoc-openapi-ui + ${openapi-version} + + + org.springdoc + springdoc-openapi-security + ${openapi-version} + + + com.h2database + h2 + 2.1.212 + test + + + org.mariadb.jdbc + mariadb-java-client + 3.0.4 + runtime + + + org.flywaydb + flyway-core + 8.5.8 + + + org.flywaydb + flyway-mysql + 8.5.8 + + + org.freemarker + freemarker + 2.3.31 + + + + commons-beanutils + commons-beanutils + 1.9.4 + + + + commons-io + commons-io + 2.11.0 + + + + ${project.groupId} + application + ${project.version} + + + ${project.groupId} + rest + ${project.version} + + + ${project.groupId} + service + ${project.version} + + + ${project.groupId} + generator + ${project.version} + + + ${project.groupId} + persistence + ${project.version} + + + + diff --git a/reacthookspring-backend/rest/.gitignore b/reacthookspring-backend/rest/.gitignore new file mode 100755 index 0000000..ba5cb5c --- /dev/null +++ b/reacthookspring-backend/rest/.gitignore @@ -0,0 +1,26 @@ +HELP.md +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ diff --git a/reacthookspring-backend/rest/pom.xml b/reacthookspring-backend/rest/pom.xml new file mode 100644 index 0000000..69f239f --- /dev/null +++ b/reacthookspring-backend/rest/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + de.starwit + reacthookspring + 0.0.1-SNAPSHOT + + + rest + jar + + + + org.springframework.security + spring-security-web + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.security + spring-security-config + + + org.springframework.boot + spring-boot-starter-test + + + org.springframework.security + spring-security-test + + + org.springdoc + springdoc-openapi-ui + + + org.springframework.boot + spring-boot-starter-data-rest + + + org.springframework.boot + spring-boot-starter-actuator + + + com.h2database + h2 + test + + + de.starwit + service + + + \ No newline at end of file diff --git a/reacthookspring-backend/rest/src/main/java/de/starwit/allowedroles/IsAdmin.java b/reacthookspring-backend/rest/src/main/java/de/starwit/allowedroles/IsAdmin.java new file mode 100644 index 0000000..cc6aa43 --- /dev/null +++ b/reacthookspring-backend/rest/src/main/java/de/starwit/allowedroles/IsAdmin.java @@ -0,0 +1,15 @@ +package de.starwit.allowedroles; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.security.access.prepost.PreAuthorize; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@PreAuthorize("hasRole('admin')") +public @interface IsAdmin { + +} diff --git a/reacthookspring-backend/rest/src/main/java/de/starwit/allowedroles/IsReader.java b/reacthookspring-backend/rest/src/main/java/de/starwit/allowedroles/IsReader.java new file mode 100644 index 0000000..c627476 --- /dev/null +++ b/reacthookspring-backend/rest/src/main/java/de/starwit/allowedroles/IsReader.java @@ -0,0 +1,15 @@ +package de.starwit.allowedroles; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.security.access.prepost.PreAuthorize; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@PreAuthorize("hasRole('reader')") +public @interface IsReader { + +} diff --git a/reacthookspring-backend/rest/src/main/java/de/starwit/allowedroles/IsUser.java b/reacthookspring-backend/rest/src/main/java/de/starwit/allowedroles/IsUser.java new file mode 100644 index 0000000..e8aebd1 --- /dev/null +++ b/reacthookspring-backend/rest/src/main/java/de/starwit/allowedroles/IsUser.java @@ -0,0 +1,15 @@ +package de.starwit.allowedroles; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.security.access.prepost.PreAuthorize; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@PreAuthorize("hasRole('user')") +public @interface IsUser { + +} diff --git a/reacthookspring-backend/rest/src/main/java/de/starwit/rest/RestApplication.java b/reacthookspring-backend/rest/src/main/java/de/starwit/rest/RestApplication.java new file mode 100644 index 0000000..18b02b0 --- /dev/null +++ b/reacthookspring-backend/rest/src/main/java/de/starwit/rest/RestApplication.java @@ -0,0 +1,19 @@ +package de.starwit.rest; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Base RestApplication + * + * Disable default HATEOAS with exclude org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration + * + */ +@SpringBootApplication(scanBasePackages = { "de.starwit.rest", "de.starwit.service", "de.starwit.persistence", "de.starwit.application.config"}) +public class RestApplication { + + public static void main(String[] args) { + SpringApplication.run(RestApplication.class, args); + } + +} diff --git a/reacthookspring-backend/rest/src/main/java/de/starwit/rest/controller/UserController.java b/reacthookspring-backend/rest/src/main/java/de/starwit/rest/controller/UserController.java new file mode 100644 index 0000000..ffd8d66 --- /dev/null +++ b/reacthookspring-backend/rest/src/main/java/de/starwit/rest/controller/UserController.java @@ -0,0 +1,27 @@ +package de.starwit.rest.controller; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("${rest.base-path}/user") +public class UserController { + + private Logger log = LoggerFactory.getLogger(this.getClass()); + + @GetMapping(value = "/logout") + public void logout(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String contextPath = request.getContextPath(); + request.logout(); + response.sendRedirect(contextPath + "/"); + } +} diff --git a/reacthookspring-backend/rest/src/main/java/de/starwit/rest/exception/ControllerExceptionHandler.java b/reacthookspring-backend/rest/src/main/java/de/starwit/rest/exception/ControllerExceptionHandler.java new file mode 100644 index 0000000..2540bc0 --- /dev/null +++ b/reacthookspring-backend/rest/src/main/java/de/starwit/rest/exception/ControllerExceptionHandler.java @@ -0,0 +1,147 @@ +package de.starwit.rest.exception; + +import java.sql.SQLIntegrityConstraintViolationException; +import java.util.HashMap; +import java.util.Map; + +import javax.persistence.EntityNotFoundException; + +import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; + +import org.hibernate.HibernateException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.orm.jpa.JpaSystemException; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.util.ClassUtils; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.client.HttpClientErrorException.Unauthorized; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import de.starwit.persistence.exception.NotificationException; + +@ControllerAdvice(basePackages = "de.starwit.rest") +public class ControllerExceptionHandler extends ResponseEntityExceptionHandler { + private static final Logger LOG = LoggerFactory.getLogger(ControllerExceptionHandler.class); + + @ExceptionHandler(value = { Exception.class }) + public ResponseEntity handleException(Exception ex) { + LOG.error(ex.getClass() + " " + ex.getMessage(), ex.fillInStackTrace()); + NotificationDto output = new NotificationDto("error.internalServerError", "Internal Server Error"); + return new ResponseEntity<>(output, HttpStatus.INTERNAL_SERVER_ERROR); + } + + @ExceptionHandler(value = { InvalidDefinitionException.class }) + public ResponseEntity handleInvalidDefinitionException(Exception ex) { + LOG.error(ex.getClass() + " " + ex.getMessage(), ex.fillInStackTrace()); + String outputString = "Invalid Definition: " + ex.getMessage() + "."; + NotificationDto output = new NotificationDto("error.invalidDefinition", outputString); + return new ResponseEntity<>(output, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(value = { Unauthorized.class }) + public ResponseEntity handleUnauthorizedException(Unauthorized ex) { + LOG.info("Unauthorized Exception: {}", ex.getMessage()); + NotificationDto output = new NotificationDto("error.unauthorized", "Unauthorized requests"); + return new ResponseEntity<>(output, HttpStatus.UNAUTHORIZED); + } + + @ExceptionHandler(value = { MethodArgumentTypeMismatchException.class }) + public ResponseEntity handleException(MethodArgumentTypeMismatchException ex) { + LOG.info("Wrong input value {}", ex.getMessage()); + Object exValue = ex.getValue(); + Class exType = ex.getRequiredType(); + String className = exValue != null ? ClassUtils.getShortName(exValue.getClass()) : ""; + String reqType = exType != null ? ClassUtils.getShortName(exType) : ""; + String outputString = "Wrong input value " + ex.getValue() + ". Failed to convert value of type " + + className + " to required type " + + reqType + "."; + NotificationDto output = new NotificationDto("error.wrongInputValue", outputString); + return new ResponseEntity<>(output, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(value = { NotificationException.class }) + public ResponseEntity handleException(NotificationException ex) { + LOG.info("Wrong input value {}", ex.getMessage()); + NotificationDto output = new NotificationDto(ex.getExceptionKey(), ex.getExceptionMessage()); + return new ResponseEntity<>(output, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(value = { InvalidDataAccessApiUsageException.class }) + public ResponseEntity handleException(InvalidDataAccessApiUsageException ex) { + LOG.info("{} Check if there is an ID declared while object shoud be created.", ex.getMessage()); + NotificationDto output = new NotificationDto("error.badrequest", + ex.getMessage() + " Check if there is an unvalid ID declared while object shoud be created."); + return new ResponseEntity<>(output, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(value = { EntityNotFoundException.class }) + public ResponseEntity handleException(EntityNotFoundException ex) { + LOG.info("Entity not found Exception: {}", ex.getMessage()); + NotificationDto output = new NotificationDto("error.notfound", "Entity not found"); + return new ResponseEntity<>(output, HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(value = { EmptyResultDataAccessException.class }) + public ResponseEntity handleException(EmptyResultDataAccessException ex) { + LOG.info(ex.getMessage()); + NotificationDto output = new NotificationDto("error.notexists", "Does not exists and cannot be deleted."); + return new ResponseEntity<>(output, HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(value = { AccessDeniedException.class }) + public ResponseEntity handleException(AccessDeniedException ex) { + LOG.info(ex.getMessage()); + NotificationDto output = new NotificationDto("error.accessdenied", "access denied"); + output.setMessageKey("error.accessdenied"); + return new ResponseEntity<>(output, HttpStatus.FORBIDDEN); + } + + @ExceptionHandler(value = { JpaSystemException.class }) + public ResponseEntity handleException(HibernateException ex) { + if (ex.getMessage().contains("More than one row with the given identifier was found")) { + LOG.info(ex.getMessage()); + NotificationDto output = new NotificationDto("error.inUse", + "More than one row with the given identifier was found"); + return new ResponseEntity<>(output, HttpStatus.BAD_REQUEST); + } + NotificationDto output = new NotificationDto("error.internalServerError", "Internal Server Error"); + return new ResponseEntity<>(output, HttpStatus.INTERNAL_SERVER_ERROR); + } + + @ExceptionHandler(value = { DataIntegrityViolationException.class }) + public ResponseEntity handleException(SQLIntegrityConstraintViolationException ex) { + NotificationDto output = new NotificationDto("error.sqlIntegrityConstaint", + "Given data is not in the right format to be saved."); + if (ex.getMessage().contains("Duplicate entry")) { + output.setMessageKey("error.unique"); + } + return new ResponseEntity<>(output, HttpStatus.BAD_REQUEST); + } + + @Override + protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, + HttpHeaders headers, HttpStatus status, WebRequest request) { + + Map errors = new HashMap<>(); + ex.getBindingResult().getAllErrors().forEach((error) -> { + + String fieldName = ((FieldError) error).getField(); + String message = error.getDefaultMessage(); + errors.put(fieldName, message); + }); + return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST); + } + +} diff --git a/reacthookspring-backend/rest/src/main/java/de/starwit/rest/exception/NotificationDto.java b/reacthookspring-backend/rest/src/main/java/de/starwit/rest/exception/NotificationDto.java new file mode 100644 index 0000000..14a0ae9 --- /dev/null +++ b/reacthookspring-backend/rest/src/main/java/de/starwit/rest/exception/NotificationDto.java @@ -0,0 +1,31 @@ +package de.starwit.rest.exception; + +public class NotificationDto { + + private String messageKey; + private String message; + + public NotificationDto() { + } + + public NotificationDto(String messageKey, String message) { + this.messageKey = messageKey; + this.message = message; + } + + public String getMessageKey() { + return messageKey; + } + + public void setMessageKey(String messageKey) { + this.messageKey = messageKey; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/reacthookspring-backend/rest/src/main/java/de/starwit/rest/exception/WrongAppIdException.java b/reacthookspring-backend/rest/src/main/java/de/starwit/rest/exception/WrongAppIdException.java new file mode 100644 index 0000000..db7c694 --- /dev/null +++ b/reacthookspring-backend/rest/src/main/java/de/starwit/rest/exception/WrongAppIdException.java @@ -0,0 +1,9 @@ +package de.starwit.rest.exception; + +public class WrongAppIdException extends RuntimeException { + + public WrongAppIdException(Long appId, String entityName) { + super("Entity with name " + entityName + "is not assigned to app with id " + appId + "."); + } + +} diff --git a/reacthookspring-backend/rest/src/main/resources/application.properties b/reacthookspring-backend/rest/src/main/resources/application.properties new file mode 100755 index 0000000..8001236 --- /dev/null +++ b/reacthookspring-backend/rest/src/main/resources/application.properties @@ -0,0 +1,6 @@ +# is only required for direct test executing. +rest.base-path=/api +spring.flyway.baseline-on-migrate=true +spring.flyway.locations=classpath:db/migration +spring.flyway.encoding=UTF-8 +spring.flyway.placeholder-replacement=false diff --git a/reacthookspring-backend/rest/src/main/resources/banner.txt b/reacthookspring-backend/rest/src/main/resources/banner.txt new file mode 100755 index 0000000..2775897 --- /dev/null +++ b/reacthookspring-backend/rest/src/main/resources/banner.txt @@ -0,0 +1,5 @@ + ,--. +,--.--. ,---. ,---. ,-' '-. +| .--' | .-. : ( .-' '-. .-' +| | \ --. .-' `) | | +`--' `----' `----' `--' diff --git a/reacthookspring-backend/rest/src/test/java/de/starwit/rest/acceptance/AbstractControllerAcceptanceTest.java b/reacthookspring-backend/rest/src/test/java/de/starwit/rest/acceptance/AbstractControllerAcceptanceTest.java new file mode 100644 index 0000000..5a7e3ee --- /dev/null +++ b/reacthookspring-backend/rest/src/test/java/de/starwit/rest/acceptance/AbstractControllerAcceptanceTest.java @@ -0,0 +1,164 @@ +package de.starwit.rest.acceptance; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.net.URL; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import de.starwit.persistence.entity.AbstractEntity; + +public abstract class AbstractControllerAcceptanceTest> { + + final static Logger LOG = LoggerFactory.getLogger(AbstractControllerAcceptanceTest.class); + + @Autowired + protected MockMvc mvc; + + @Autowired + protected ObjectMapper mapper; + + @BeforeEach + public void setup() { + // create Object Mapper + mapper = new ObjectMapper(); + JacksonTester.initFields(this, new ObjectMapper()); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + public abstract Class getEntityClass(); + + public abstract String getRestPath(); + + public abstract JacksonTester getJsonTester(); + + @Test + public abstract void canCreate() throws Exception; + + @Test + public abstract void canRetrieveById() throws Exception; + + @Test + public abstract void canUpdate() throws Exception; + + @Test + public abstract void canDelete() throws Exception; + + protected ENTITY readFromFile(String path) throws Exception { + try { + URL res = getClass().getClassLoader().getResource(path); + File file = new File(res.getFile()); + ENTITY entity = mapper.readValue(file, getEntityClass()); + return entity; + } catch (IOException e) { + LOG.error("JSON mapper failed", e); + throw new Exception("JSON mapper failed"); + } + } + + protected String readJsonStringFromFile(String path) throws Exception { + try { + URL res = getClass().getClassLoader().getResource(path); + File file = new File(res.getFile()); + + StringBuilder contentBuilder = new StringBuilder(); + try (BufferedReader br = new BufferedReader(new FileReader(file))) { + + String sCurrentLine; + while ((sCurrentLine = br.readLine()) != null) { + contentBuilder.append(sCurrentLine); + } + } + return contentBuilder.toString(); + } catch (IOException e) { + LOG.error("JSON mapper failed", e); + throw new Exception("JSON mapper failed"); + } + } + + protected MockHttpServletResponse create(ENTITY entity) throws Exception { + String applicationString = getJsonTester().write(entity).getJson(); + MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.post(getRestPath()) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON) + .characterEncoding("UTF-8") + .content(applicationString); + + MockHttpServletResponse response = mvc.perform(builder) + .andReturn().getResponse(); + + LOG.info(response.getContentAsString()); + return response; + } + + protected MockHttpServletResponse createFromString(String applicationString) throws Exception { + MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.post(getRestPath()) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON) + .characterEncoding("UTF-8") + .content(applicationString); + + MockHttpServletResponse response = mvc.perform(builder) + .andReturn().getResponse(); + + LOG.info(response.getContentAsString()); + return response; + } + + protected MockHttpServletResponse update(ENTITY entity) throws Exception { + String applicationString = getJsonTester().write(entity).getJson(); + MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.put(getRestPath()) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON) + .characterEncoding("UTF-8") + .content(applicationString); + + MockHttpServletResponse response = mvc.perform(builder) + .andReturn().getResponse(); + assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); + + LOG.info(response.getContentAsString()); + return response; + } + + protected MockHttpServletResponse retrieveById(Long id) throws Exception { + MockHttpServletResponse response = mvc.perform( + get(getRestPath() + id) + .contentType(MediaType.APPLICATION_JSON)) + .andReturn().getResponse(); + + LOG.info(response.getContentAsString()); + return response; + } + + protected MockHttpServletResponse delete(Long id) throws Exception { + MockHttpServletResponse response = mvc.perform( + MockMvcRequestBuilders.delete(getRestPath() + id) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn().getResponse(); + + LOG.info(response.getContentAsString()); + return response; + } +} diff --git a/reacthookspring-backend/rest/src/test/java/de/starwit/rest/integration/AbstractControllerIntegrationTest.java b/reacthookspring-backend/rest/src/test/java/de/starwit/rest/integration/AbstractControllerIntegrationTest.java new file mode 100644 index 0000000..9a7a7af --- /dev/null +++ b/reacthookspring-backend/rest/src/test/java/de/starwit/rest/integration/AbstractControllerIntegrationTest.java @@ -0,0 +1,78 @@ +package de.starwit.rest.integration; + +import java.io.File; +import java.io.IOException; +import java.net.URL; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.BeforeEach; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import de.starwit.persistence.entity.AbstractEntity; + +@WithMockUser(username = "admin", roles = { "ADMIN", "PBUSER" }) +@WebMvcTest() +@Import({}) +public abstract class AbstractControllerIntegrationTest> { + + final static Logger LOG = LoggerFactory.getLogger(AbstractControllerIntegrationTest.class); + + @Autowired + protected MockMvc mvc; + + @Autowired + protected ObjectMapper mapper; + + @BeforeEach + public void setup() { + // create Object Mapper + mapper = new ObjectMapper(); + JacksonTester.initFields(this, new ObjectMapper()); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + public abstract Class getEntityClass(); + + public abstract String getRestPath(); + + public abstract void canRetrieveById() throws Exception; + + protected ENTITY readFromFile(String path) throws Exception { + try { + URL res = getClass().getClassLoader().getResource(path); + File file = new File(res.getFile()); + ENTITY entity = mapper.readValue(file, getEntityClass()); + return entity; + } catch (IOException e) { + LOG.error("JSON mapper failed", e); + throw new Exception("JSON mapper failed"); + } + } + + protected MockHttpServletResponse retrieveById(Long id) throws Exception { + // when + MockHttpServletResponse response = mvc.perform( + get(getRestPath() + id) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn().getResponse(); + + LOG.info(response.getContentAsString()); + return response; + } + +} diff --git a/reacthookspring-backend/service/.gitignore b/reacthookspring-backend/service/.gitignore new file mode 100755 index 0000000..c456c4a --- /dev/null +++ b/reacthookspring-backend/service/.gitignore @@ -0,0 +1,25 @@ +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ diff --git a/reacthookspring-backend/service/pom.xml b/reacthookspring-backend/service/pom.xml new file mode 100644 index 0000000..7b75a12 --- /dev/null +++ b/reacthookspring-backend/service/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + + de.starwit + reacthookspring + 0.0.1-SNAPSHOT + + + service + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-test + + + de.starwit + persistence + + + org.springframework.security + spring-security-core + + + org.springframework.security + spring-security-oauth2-core + + + org.springdoc + springdoc-openapi-ui + + + com.h2database + h2 + test + + + + \ No newline at end of file diff --git a/reacthookspring-backend/service/src/main/java/de/starwit/service/ServiceApplication.java b/reacthookspring-backend/service/src/main/java/de/starwit/service/ServiceApplication.java new file mode 100755 index 0000000..6158dea --- /dev/null +++ b/reacthookspring-backend/service/src/main/java/de/starwit/service/ServiceApplication.java @@ -0,0 +1,13 @@ +package de.starwit.service; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication(scanBasePackages = "de.starwit.persistence") +public class ServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(ServiceApplication.class, args); + } + +} diff --git a/reacthookspring-backend/service/src/main/java/de/starwit/service/impl/ServiceInterface.java b/reacthookspring-backend/service/src/main/java/de/starwit/service/impl/ServiceInterface.java new file mode 100644 index 0000000..f8a0807 --- /dev/null +++ b/reacthookspring-backend/service/src/main/java/de/starwit/service/impl/ServiceInterface.java @@ -0,0 +1,46 @@ +package de.starwit.service.impl; + +import java.util.List; + +import javax.persistence.EntityNotFoundException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.jpa.repository.JpaRepository; + +import de.starwit.persistence.entity.AbstractEntity; +import de.starwit.persistence.exception.NotificationException; + +/** + * AbstractService used as template for all service implementations and + * implements the basic + * functionality (create, read, update, delete, and other basic stuff). + * + * @author Anett + * + * @param + */ +public interface ServiceInterface, R extends JpaRepository> { + + static Logger LOG = LoggerFactory.getLogger(ServiceInterface.class); + + public R getRepository(); + + public default List findAll() { + return this.getRepository().findAll(); + } + + public default E findById(Long id) { + return this.getRepository().findById(id).orElseThrow(() -> new EntityNotFoundException(String.valueOf(id))); + } + + public default E saveOrUpdate(E entity) { + entity = this.getRepository().save(entity); + return entity; + } + + public default void delete(Long id) throws NotificationException { + this.getRepository().deleteById(id); + } + +} diff --git a/reacthookspring-backend/service/src/main/resources/application.properties b/reacthookspring-backend/service/src/main/resources/application.properties new file mode 100755 index 0000000..8b13789 --- /dev/null +++ b/reacthookspring-backend/service/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/reacthookspring-backend/service/src/main/resources/banner.txt b/reacthookspring-backend/service/src/main/resources/banner.txt new file mode 100755 index 0000000..11d8556 --- /dev/null +++ b/reacthookspring-backend/service/src/main/resources/banner.txt @@ -0,0 +1,5 @@ + ,--. + ,---. ,---. ,--.--. ,--. ,--. `--' ,---. ,---. +( .-' | .-. : | .--' \ `' / ,--. | .--' | .-. : +.-' `) \ --. | | \ / | | \ `--. \ --. +`----' `----' `--' `--' `--' `---' `----' \ No newline at end of file diff --git a/reacthookspring-frontend/.env.development b/reacthookspring-frontend/.env.development new file mode 100755 index 0000000..0491b07 --- /dev/null +++ b/reacthookspring-frontend/.env.development @@ -0,0 +1,2 @@ +HOST=0.0.0.0 +BROWSER=none diff --git a/reacthookspring-frontend/.eslintrc.json b/reacthookspring-frontend/.eslintrc.json new file mode 100644 index 0000000..096adae --- /dev/null +++ b/reacthookspring-frontend/.eslintrc.json @@ -0,0 +1,127 @@ +{ + "env": { + "browser": true, + "es2021": true + }, + "extends": [ + "plugin:react/recommended", + "google" + ], + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": [ + "react" + ], + "ignorePatterns": ["**/*.ico", "**/*.css", "**/translations*.js","**/*.svg","**/*.png"], + "rules": { + "max-len": [ + "warn", {"code": 120, "tabWidth": 4, + "ignoreUrls": true + } + ], + "indent": [ + "warn", + 4 + ], + "quotes": [ + "warn", + "double" + ], + "react/prop-types": 0, + "comma-dangle": [ + "warn", + "never" + ], + "new-cap": [ + "warn", + { + "capIsNew": false + } + ], + "require-jsdoc": [ + "warn", + { + "require": { + "FunctionDeclaration": false, + "MethodDefinition": false, + "ClassDeclaration": false, + "ArrowFunctionExpression": false, + "FunctionExpression": false + } + } + ], + "padded-blocks": ["warn", "never"], + "eol-last": ["warn", "always"], + "object-curly-spacing": ["warn", "never"], + "semi": ["warn", "always"], + "arrow-parens": ["warn", "as-needed"], + "no-trailing-spaces":"warn", + "no-cond-assign": 0, // eslint:recommended + "no-irregular-whitespace": 1, // eslint:recommended + "no-unexpected-multiline": 1, // eslint:recommended + "curly": [1, "multi-line"], + "guard-for-in": 1, + "no-caller": 1, + "no-extend-native": 1, + "no-extra-bind": 1, + "no-invalid-this": 1, + "no-multi-spaces": 1, + "no-multi-str": 1, + "no-new-wrappers": 1, + "no-throw-literal": 1, // eslint:recommended + "no-with": 1, + "prefer-promise-reject-errors": 1, + "no-unused-vars": [1, {"args": "none"}], // eslint:recommended + "array-bracket-newline": 0, // eslint:recommended + "array-bracket-spacing": [1, "never"], + "array-element-newline": 0, // eslint:recommended + "block-spacing": [1, "never"], + "brace-style": [1, "1tbs", { "allowSingleLine": true }], + "camelcase": [1, {"properties": "never"}], + "comma-spacing": 1, + "comma-style": 1, + "computed-property-spacing": 1, + "func-call-spacing": 1, + "key-spacing": 1, + "keyword-spacing": 1, + "linebreak-style": 1, + "no-array-constructor": 1, + "no-mixed-spaces-and-tabs": 1, // eslint:recommended + "no-multiple-empty-lines": [1, {"max": 1}], + "no-new-object": 1, + "no-tabs": 1, + "one-var": [1, { + "var": "never", + "let": "never", + "const": "never" + }], + "operator-linebreak": [1, "after"], + // "padding-line-between-statements": 0, + "quote-props": [1, "consistent"], + "semi-spacing": 1, + "space-before-blocks": 1, + "space-before-function-paren": [1, { + "asyncArrow": "always", + "anonymous": "never", + "named": "never" + }], + "spaced-comment": [1, "always"], + "switch-colon-spacing": 1, + "constructor-super": 1, // eslint:recommended + "generator-star-spacing": [1, "after"], + "no-new-symbol": 1, // eslint:recommended + "no-this-before-super": 1, // eslint:recommended + "no-var": 1, + "prefer-const": [1, {"destructuring": "all"}], + "prefer-rest-params": 1, + "prefer-spread": 1, + "rest-spread-spacing": 1, + "yield-star-spacing": [1, "after"], + "react/react-in-jsx-scope": "off" + } +} \ No newline at end of file diff --git a/reacthookspring-frontend/.gitignore b/reacthookspring-frontend/.gitignore new file mode 100755 index 0000000..4d29575 --- /dev/null +++ b/reacthookspring-frontend/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/reacthookspring/webclient/app/.public-env b/reacthookspring-frontend/.public-env similarity index 100% rename from reacthookspring/webclient/app/.public-env rename to reacthookspring-frontend/.public-env diff --git a/reacthookspring/webclient/app/Dockerfile b/reacthookspring-frontend/Dockerfile similarity index 100% rename from reacthookspring/webclient/app/Dockerfile rename to reacthookspring-frontend/Dockerfile diff --git a/reacthookspring/webclient/app/env.sh b/reacthookspring-frontend/env.sh similarity index 100% rename from reacthookspring/webclient/app/env.sh rename to reacthookspring-frontend/env.sh diff --git a/reacthookspring-frontend/package.json b/reacthookspring-frontend/package.json new file mode 100644 index 0000000..2cfbd10 --- /dev/null +++ b/reacthookspring-frontend/package.json @@ -0,0 +1,59 @@ +{ + "name": "app", + "version": "0.1.0", + "private": true, + "dependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "@mui/icons-material": "^5.0.0", + "@mui/lab": "^5.0.0-alpha.63", + "@mui/material": "^5.0.0", + "@mui/styles": "^5.0.0", + "@starwit/react-starwit": "0.0.6-11", + "axios": "^0.24.0", + "i18next": "^21.0.1", + "i18next-browser-languagedetector": "^6.1.2", + "immer": "^9.0.12", + "notistack": "^2.0.3", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-draggable": "^4.4.4", + "react-i18next": "^11.12.0", + "react-lineto": "^3.3.0", + "react-router-dom": "^5.3.0", + "react-scripts": "^5.0.1", + "react-syntax-highlighter": "^15.4.4", + "use-immer": "^0.7.0", + "@date-io/moment": "^2.15.0", + "@mui/x-date-pickers": "^5.0.1", + "moment": "^2.29.4", + "oidc-react": "^3.0.3" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build --loglevel=error", + "test": "react-scripts test", + "eject": "react-scripts eject", + "stop": "taskkill -F -IM node.exe" + }, + "eslintConfig": { + "extends": "react-app" + }, + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" + ], + "proxy": "http://localhost:8081", + "homepage": "./", + "devDependencies": { + "eslint": "^8.13.0", + "eslint-config-google": "^0.14.0", + "eslint-plugin-import": "^2.25.4", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^6.0.0", + "eslint-plugin-react": "^7.29.4", + "prop-types": "^15.7.2" + } +} diff --git a/reacthookspring-frontend/public/favicon.ico b/reacthookspring-frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..2490fe3fdb0332fa512c6d91ebe470a520938804 GIT binary patch literal 4189 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 G5dD9w$H%V# literal 0 HcmV?d00001 diff --git a/reacthookspring-frontend/public/index.html b/reacthookspring-frontend/public/index.html new file mode 100644 index 0000000..72e12de --- /dev/null +++ b/reacthookspring-frontend/public/index.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + reacthookspring + + + +
+ + + \ No newline at end of file diff --git a/reacthookspring-frontend/public/manifest.json b/reacthookspring-frontend/public/manifest.json new file mode 100755 index 0000000..1f2f141 --- /dev/null +++ b/reacthookspring-frontend/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/reacthookspring-frontend/src/app/App.jsx b/reacthookspring-frontend/src/app/App.jsx new file mode 100644 index 0000000..6b5b286 --- /dev/null +++ b/reacthookspring-frontend/src/app/App.jsx @@ -0,0 +1,26 @@ +import React from "react"; +import MainContentRouter from "./MainContentRouter"; +import {CssBaseline} from "@mui/material"; +import {ErrorHandler} from "@starwit/react-starwit"; +import {useTranslation} from "react-i18next"; +import {appItems} from "./AppConfig"; +import Navigation from "./commons/navigation/Navigation"; +import logo from "./assets/images/logo-white.png"; + + +function App() { + const {t} = useTranslation(); + + return ( + + + + + + + + + ); +} + +export default App; diff --git a/reacthookspring-frontend/src/app/App.test.js b/reacthookspring-frontend/src/app/App.test.js new file mode 100755 index 0000000..b88856f --- /dev/null +++ b/reacthookspring-frontend/src/app/App.test.js @@ -0,0 +1,9 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import App from "./App"; + +it("renders without crashing", () => { + const div = document.createElement("div"); + ReactDOM.render(, div); + ReactDOM.unmountComponentAtNode(div); +}); diff --git a/reacthookspring-frontend/src/app/AppConfig.js b/reacthookspring-frontend/src/app/AppConfig.js new file mode 100644 index 0000000..a7a8180 --- /dev/null +++ b/reacthookspring-frontend/src/app/AppConfig.js @@ -0,0 +1,3 @@ +const appItems = []; + +export {appItems}; diff --git a/reacthookspring-frontend/src/app/MainContentRouter.jsx b/reacthookspring-frontend/src/app/MainContentRouter.jsx new file mode 100644 index 0000000..43aa19f --- /dev/null +++ b/reacthookspring-frontend/src/app/MainContentRouter.jsx @@ -0,0 +1,17 @@ +import React from "react"; +import {Route} from "react-router-dom"; +import Home from "./features/home/Home"; + +function MainContentRouter() { + return ( + <> + + { + window.location.href = window.location.pathname + "api/user/logout"; + return null; + }}/> + + ); +} + +export default MainContentRouter; diff --git a/reacthookspring-frontend/src/app/assets/icons/icon-camera-green.svg b/reacthookspring-frontend/src/app/assets/icons/icon-camera-green.svg new file mode 100755 index 0000000..f5afb9b --- /dev/null +++ b/reacthookspring-frontend/src/app/assets/icons/icon-camera-green.svg @@ -0,0 +1,27 @@ + + + + icon/media/camera + Created with Sketch. + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/reacthookspring-frontend/src/app/assets/icons/icon-device.svg b/reacthookspring-frontend/src/app/assets/icons/icon-device.svg new file mode 100755 index 0000000..40b73e4 --- /dev/null +++ b/reacthookspring-frontend/src/app/assets/icons/icon-device.svg @@ -0,0 +1,17 @@ + + + + icon-device + Created with Sketch. + + + + + + + + + + + \ No newline at end of file diff --git a/reacthookspring-frontend/src/app/assets/icons/icon-hamburger-menu.svg b/reacthookspring-frontend/src/app/assets/icons/icon-hamburger-menu.svg new file mode 100755 index 0000000..b781681 --- /dev/null +++ b/reacthookspring-frontend/src/app/assets/icons/icon-hamburger-menu.svg @@ -0,0 +1,21 @@ + + + + icon-hamburger-menu + Created with Sketch. + + + + + + + + + + + + \ No newline at end of file diff --git a/reacthookspring-frontend/src/app/assets/icons/icon-zoom-in.svg b/reacthookspring-frontend/src/app/assets/icons/icon-zoom-in.svg new file mode 100755 index 0000000..699a273 --- /dev/null +++ b/reacthookspring-frontend/src/app/assets/icons/icon-zoom-in.svg @@ -0,0 +1,31 @@ + + + + icon/basics/zoom-in + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/reacthookspring-frontend/src/app/assets/icons/icon-zoom-out.svg b/reacthookspring-frontend/src/app/assets/icons/icon-zoom-out.svg new file mode 100755 index 0000000..be367c5 --- /dev/null +++ b/reacthookspring-frontend/src/app/assets/icons/icon-zoom-out.svg @@ -0,0 +1,33 @@ + + + + icon/basics/zoom-out + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/reacthookspring-frontend/src/app/assets/images/DefaultAppTemplate.jpg b/reacthookspring-frontend/src/app/assets/images/DefaultAppTemplate.jpg new file mode 100644 index 0000000000000000000000000000000000000000..64cb70cfda11a151fc8864284bead84e3cae794d GIT binary patch literal 30402 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 literal 0 HcmV?d00001 diff --git a/reacthookspring-frontend/src/app/assets/images/logo-black.png b/reacthookspring-frontend/src/app/assets/images/logo-black.png new file mode 100644 index 0000000000000000000000000000000000000000..2c0fe1e7a7f40ca04e39f3ff45fdb034700e7b52 GIT binary patch literal 5555 zcmV;k6-?@hP)J00009a7bBm000id z000id0mpBsWB>pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H16+lTu zK~#90?VW#+WL0&?KlgP{&+N|bvOnfmzwX@~*o9pf6_?*42oewx2`Gjiq98#eh9a7( z#6(F5P?m|QBobpV28@zKix6UAp@K>xMg;<{CK94VvcT?i54$YPE-qh(%lI`AbR-nLGKkVt41a9ww~ z*81zfF;!mr4&ZZuYJIQ5NhGu~jE#*YfO~*bYkbQkz^7W*qi_=mtu)TQefz!$ybaKG zyzhg+4Xx=?xQT>zD9Ux++qBjfH~9XK16TOjp-vzYS`n`6-UM9TL=y`5W8j_s^r$n4 zgl3RTCa(oP>O)fw@EPDOKD5;lL_*7xN~PYTl=9P=?*VQHj`Ok2&=Ls^a9#Ht;5H6j zo~UOcP>N;1-vFDMY$x<|z0gvr)G1o)FKDgfEDEbxftjoZ?gU&P+YB9DEi~74w*q%4 zr4lXvsA1p^U|owp$6C5pXvt)9qf+WFt@WxledGq<^T5hBeV$cxrO<|khK7MJ0|RaT z=%au?16plCt)uINHaa@GIv$VTsgz39_!d9?HGt!hP*u$5cCDtXgf==l+BZ8p`&lZ_ z0$VX5DCTtFM!F~n-%Ud6nVXyYb6{JU=UXYPQoJ4btu}vVV04Sn9M^SkWoafxJ1kc< zE(R_S@blY7R|qYcOnwwN8$c-)q_9eHEt1dH(R94flF8(Ulv0-j-H!?=B))oj&>d+d z9WAtEGWl+$)RpC)_dKuMQ|%sB9pLxq;J9eV3e9!hvmM8&HwiJwVKol?0kAc|KD3OE z6j~~kItBQc*1F>NGyFAM{I*_1Z0Vpo;x`>9v}7`QwAT7YN-v1oCahAd0&WHd1MY{f zbd=CWMn*;*$GHWu3??=Us}yU2o9Lk8E6+$XfwLk%_&wB@SC zF~IeN{I?2q=f}pz60un9V_NIcI^Jfxuu8EFsmuy(5I)R8i%m{WegxQ3%Zn`xJ=M#p zz@s~}Y6C;lwqOi?!RYD;{=o^FXNIir~Xd@#dXDFr4gVwH*X>GmU9N_)b z=%3m^XoNO0GIFBhIL)T}Sg)Wuhyfo!*4nHm6hhmuVZ&G~7Q2G_r}s?{iI)3o3E(QA z(KW=jVZG3j$>hddE_Y?MOpH8!`dT&%%%mS#WAh^_(lV?T+Th^eh*IjBYL(|;noRl^XARHy}iBfQ%bGDf;F=IQb(A?If^XmTJ_sn z)fu4Yz<~poYpsVHZNTUB>@W$;Vt8D}1eP5}3)Ez!5=qAW)&D)9KfHo_A8wZQq3Dv)>kG6q4~$>z;7F6PVD_ z>GYAF=bi4;EHScv)o~E9wR@M#gCE{@T-V)znDb_H;QdT;>@;DOq6avK0P02CF0_%6 zkwK-@Sv3a9i_v+fJ{l4mrw8u#>XsjpSb0&tnOI*FCYf^J*lqHPgcI-OqD)6?@-rBta*46ttt zgKn{YYk@b@h8@D&A+#RP^G-+h#5jm$zE-ygs}zI43ADI4xz$38jf{+(YV3(&#ZpG( zuN^{?$~Cf0(@LSKR4R3%QfeJO2&>ev8u?njvYr-JRkccJ%5~j0HnH(al#LM#n~;1q z-)V`^T-V*Al&bgGK;MSddPHi`hRL>Y6fHRN(RZP(U%!5n*1DEjL)#J*yH!}F*ox0v zoBArWbUK|{SXel`%FC=0MVF-v)rvyWwtOq$^G#^O!^1rpR+@{jTn+N*>tF?X`u}c4z8G+oAc`shK5C9 zSVh}>zpxWWe72_QMVk^@KD2aXgP9mMRxCxsYH1QkWmUs*(T0UKHa3<3HhZ3T@WXSt zT&-`nMHG>+YNr=CoCY=1sL%L(UW0ZrX92aVa5*rA%x2iv#%qn0_tuL-wXs+w7V^L&Q_(>L4P<0!t5y4fZCZUavj;{7R zZ$Vr{SN2K!H%zvL)zok>T2-NK*|KHj{QP{~BFp7+w#^bF%hfv8aOlyMOQqN`Fua|Q+m4lZF#O-)G)J`5j042d;vm>l6B3Yi=% zS@D z39AiM6q+9s0xZfSGxz(57Gc}V5~20>_G(xZSTVD+vo$wti=Pzjvtb8*)^&E`EHM(P zPc$sFMlD4tr`{Ip=TjRIC8%3yVT}+}BJ$T-PgSAWwnhdAH%xA-jiP@eTFjxr&-_H7 z+=QS`tg?RQ|13U;HKGnoq1E+6&dtr0x^3Gm(H7sY>uGk@*@>cvgw+C?-O8XD{5YJ# zHlat9p(df(HcRxQjc6gay_6K1t$AckZkQYiD?pYAt*@`o7E58J&?6eB=@3+ytY0}Q z?Dt`|akt|*Un_duUjqM8ZwI>(KSEITY*$rG7<|7yz-8HN_I;U5=B1+No<`2V{0K1Z z!{>J_^$4w{q6p}4ii*}xDRrMx>V#}Id!KRpGT;ZmoAQr6;LFGXUXS?Dr;eno(0s~8 zD`1uHS3?^w0#{`+nX3#dHVoVY{5^0o@IBym7B>@q7PtubT_Wpt2!{wQ9*_Gbigw3G z%UQ)b`*D9f9zQLc&E9X^dpEEhxBytlUmCa?cpR~2g}(kHGMqkE$7gmtOF~fFw{LH> z9jmM#jScgAwbs{Wv)K>r+_`fye{Tr*3UC)v@}DblZ2-Ore3r${PIdy912+;`H=xN` zX9uMWJrIw_&zYE*c+j|a5n`3jXVHq4yFB1Z#78^L=*!)R73}(%X`{s{Wq;5bW}r4j z2=)TkXS3NG3@bK>_+ei}tXMUXnb&&YKHx7{^xbv=zYg3&7hAWALMsT%mXslU7^02$ zemjv!T$s&fA2Oc)Rb)naE~SUbTCN4)3S_eIW~1Nt0>1|QxY53cnUed)`ys4mH%wMa zneX*Yq+08j9LM?e`1tt4#&bi!p8#*qKW?bBzKAq3OZ^IB&8C11@L}LW;07ve^VDe+ zTQr*&P8)6GU|U-2f18<^xwJ@R=L6qGW{Qnku}aqMGT=MFsYZY91Ktn(#E0)03K~8M z!@$=ev55^;mI%wRiU2PGpP87L_@VLoLEw|X+wzb7uwvzKk!6p&^Pf2lWP#sCgnk_z zZr#fIXp@tZCGT4@OFT$5e@!X%{R0OMT%FBkO_7}kJOKO(^*Q^^Tpn-<@GTaj3L5wZ za4qnk4gLv2K~;-pbI`ns^?O+g~=@68!L!#EY*w?DYr002eyzs&cPZ;+GkRS8R{No_3SUHA)F9837 zti4Sk*&??fR`QQ_%RuaHwfWXf{mo0GtSX z3U~tft`7rGBJtFt0_#~5R1}(D5dy8XuWMvl>zB3GU!9nk__^_#0VEf!5cUa!6)VRe z;a(x0NokM#ET-e*`b z1-ujZ4J42qMk`j1cL2u&e~RS8Y2b&5b-M&P>pT?rG8urTSM>{&_1m=8lS-+3v)Szb z8n=gm-vN%xKMu1ME5`uhr+pKdp-uz)fjfX>k%@%$Gt-8JW-jD5DzF+S6$|1)k6Ayr4)@{c=dsI10ez)eUh(_aCv0k0zYbFV{&S}UzvRiQbKqZ>Ss!lV)F z_mlqq{)dXJ*ttj&VYgVZa-53zXMc^Xy8-++@EjtxO~G`lJckIax3^bMPfu6eFgYHt zs?Ub4pVoRm@L(pBdEU4^1Y8fiA^*6WtXMf#A@R{ifX^e=?KLC^ZY#1_ZOzQox5+@B znuJv@tfaO6+1j;h|NW__o+@mr6M)S~8d9Nl@|Q>o`RPdUOd!-0 z8E8?^{I#B*o*zB`{PPpLcI_&>W)QdrI41u%vSRhK5}B1h4txo*Y}3HEfy0rcOTS&F zs-JR^YZG=NNtWA?P+m)EL}>N;ZPUP$*=%;N zac=<09xbH2L_=lkS&k%8{uHUkngV8kM}ZVFyKk9wYiV(c)_PYallcXU)=DAH=tV?U zEGXW9ObYG+ewM$NL6R)T5F_9_ttw7A5Rb>7HukdsSO;8+Our!#S&%FTK7a(}??tTJ zEHY^r2Daeu$%lsut*@_7@87?_p~jR_+Hss0#>dBZu+)m3gk+8;h=xg<7y~u}UqfQ0 z0K0)nWTAJBUmxUKamw7>+-xqFd-mm*Uw*}SYAxdIAD(|4Suy)qhAfUhi9`~nfH~kP zWEef1{JH4O(5TRg{kFY(_Usu)sa!(ugVu+hm$P+B=2UqgH zYx2>0dU|rJR;}7+BqRc?Muy3B{&8f*I+p}8VjT(mD-thVK+a=WhSV}tqDo;v13O;xdqlDyWA9l zu#w2Rj|8HDO-L5V6fz-vCI8&AQg15dqs7iV^GqIo_+fJ>bPy|c0&Pkwi3kO=$lh-M zW8727M8d)F(MkyomadnrL3XpLYnBnwRqRBDQ}Y~!SpHg|gwRYJBo?-f$cjZ&XAViQ ze7fkq19;_xwi+o?ZAr>VL`O4@RAVhUC8pHSc{JjW`I;sY(N)YL36{?kZkR$l3@JwS zEj1*f>v@WN`jNT3u>y%@^%MDI5skAS89v7mJprZjjAFcNRivKG$rzINpF~6#4X@qDG*Y5Bfus%9Ort5f z4kEhY{1u6=b2)$n&tCxcHQGqix!{QC)*^x%BobXGFoTF|4^4>6#1EmFILPwIAmZCa zq6-hRNR7aY)ZlZ~i{C;sagYeZ05Z%*qO*@#~W zL|iXXW+|c-v`c6v4pO?c78z!vP*>}jLuQFDBRdMUogjo};vj_%Ymi|!5}kj{BjVae zK;kk9T4*K?5(Zn146~8w>L-T`hlSEOYp_XZCJqvFU6sEg(UoTb5!EY*^;N?ayk7Mh7d&Lmilh%d~d^AJ%@BcY(sip!)^gl1AG_gaRCFAU$& zLssbv#kd_IE|W0)2+g37qSih{d^UYwjfiRnSy1gbahcfkhi^3s*@z~Qzk>Mg8WC0D z>KeL9XeJ8D7VgPkt^7M^WFd8q#gk&Xj&2oNF>&Ma<8Pu_+zuc|hYyRj5DD#2V#ZZ3 z3&s^;5DBd^j&YU4Gp>j-L_(__W&B&q0c#Nk{}0gQ0ri5_&q4qI002ovPDHLkV1k74 Bl+pkI literal 0 HcmV?d00001 diff --git a/reacthookspring-frontend/src/app/assets/images/logo-color.png b/reacthookspring-frontend/src/app/assets/images/logo-color.png new file mode 100644 index 0000000000000000000000000000000000000000..2490fe3fdb0332fa512c6d91ebe470a520938804 GIT binary patch literal 4189 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 G5dD9w$H%V# literal 0 HcmV?d00001 diff --git a/reacthookspring-frontend/src/app/assets/images/logo-white.png b/reacthookspring-frontend/src/app/assets/images/logo-white.png new file mode 100644 index 0000000000000000000000000000000000000000..c80ac4eb48de8b1772f37064e5a0a9e723741739 GIT binary patch literal 11269 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 ({ + root: { + display: "flex" + }, + appBar: { + zIndex: theme.zIndex.modal + 1, + height: "3rem" + }, + drawer: { + [theme.breakpoints.up("sm")]: { + width: drawerWidth, + flexShrink: 0 + } + }, + menuTitle: { + display: "flex", + justifyContent: "left", + paddingLeft: 20, + [theme.breakpoints.up("sm")]: { + display: "flex", + width: "20%", + justifyContent: "center" + } + }, + menuLogo: { + display: "none", + [theme.breakpoints.up("sm")]: { + display: "flex", + width: 350 + } + }, + menuLogoResponsive: { + display: "flex", + width: 50, + [theme.breakpoints.up("sm")]: { + display: "none" + } + }, + menuLogoImg: { + height: "70%", + width: "auto" + }, + spacer: { + flexGrow: 1 + }, + menuButton: { + marginRight: 20, + [theme.breakpoints.up("sm")]: { + display: "none" + } + }, + content: { + flexGrow: 1, + padding: theme.spacing.unit * 3 + }, + toolbar: { + ...theme.mixins.toolbar, + height: "2rem" + }, + linkButton: { + marginRight: theme.spacing(3), + marginLeft: theme.spacing(3) + }, + contentSpacer: { + height: `calc(${appBarHeight} + ${theme.spacing(3)})` + } +})); +export default HeaderStyles; diff --git a/reacthookspring-frontend/src/app/assets/themes/ColorTheme.js b/reacthookspring-frontend/src/app/assets/themes/ColorTheme.js new file mode 100644 index 0000000..af0f288 --- /dev/null +++ b/reacthookspring-frontend/src/app/assets/themes/ColorTheme.js @@ -0,0 +1,63 @@ +import {createTheme} from "@mui/material"; + +const ColorTheme = createTheme({ + palette: { + type: "light", + primary: { + main: "#0B3954", + contrastText: "#fff" + }, + secondary: { + main: "#FF8552", + contrastText: "#000" + }, + background: { + default: "#f1f1f1", + paper: "#fff" + }, + line: { + width: 4 + } + }, + + mixins: { + toolbar: { + minHeight: 48 + } + }, + + typography: { + useNextVariants: true, + fontFamily: "Lato, sans-serif", + fontSize: 16, + body1: {}, + body2: {}, + h1: { + fontSize: "2rem", + fontWeight: 400, + textTransform: "uppercase" + }, + h2: { + fontSize: "2rem", + fontWeight: 400, + textTransform: "uppercase" + }, + h5: { + fontSize: "1.2rem" + }, + h6: { + fontSize: "1.2rem", + marginBottom: 5 + } + }, + + shape: { + borderRadius: 0 + }, + + overrides: {} + + +}) + +export default ColorTheme; diff --git a/reacthookspring-frontend/src/app/assets/themes/ComponentTheme.js b/reacthookspring-frontend/src/app/assets/themes/ComponentTheme.js new file mode 100644 index 0000000..eb67346 --- /dev/null +++ b/reacthookspring-frontend/src/app/assets/themes/ComponentTheme.js @@ -0,0 +1,30 @@ +import {createTheme} from "@mui/material"; +import ColorTheme from "./ColorTheme"; + +const ComponentTheme = createTheme(ColorTheme, + { + components: { + MuiContainer: {}, + MuiCard: { + defaultProps: { + elevation: 10 + } + }, + MuiFab: { + defaultProps: { + color: "secondary" + } + }, + MuiStack: { + defaultProps: { + spacing: 2 + } + }, + MuiIconButton: { + defaultProps: { + color: "inherit" + } + } + } + }); +export default ComponentTheme; diff --git a/reacthookspring-frontend/src/app/assets/themes/MainTheme.jsx b/reacthookspring-frontend/src/app/assets/themes/MainTheme.jsx new file mode 100644 index 0000000..4d0c5f4 --- /dev/null +++ b/reacthookspring-frontend/src/app/assets/themes/MainTheme.jsx @@ -0,0 +1,13 @@ +import React from "react"; +import {ThemeProvider} from "@mui/material"; +import ComponentTheme from "./ComponentTheme"; + +function MainTheme(props) { + return ( + + {props.children} + + ) +} + +export default MainTheme; diff --git a/reacthookspring-frontend/src/app/commons/BearerProvider/BearerProvider.jsx b/reacthookspring-frontend/src/app/commons/BearerProvider/BearerProvider.jsx new file mode 100644 index 0000000..6ff6346 --- /dev/null +++ b/reacthookspring-frontend/src/app/commons/BearerProvider/BearerProvider.jsx @@ -0,0 +1,29 @@ +import {useEffect, useState} from "react"; +import {useAuth} from "oidc-react"; +import {CircularProgress} from "@material-ui/core"; +import axios from "axios"; + +function BearerProvider(props) { + const [headerSet, setHeaderSet] = useState(false); + const auth = useAuth(); + const {children} = props; + + useEffect(() => { + if (auth?.userData?.access_token) { + if (!auth?.userData?.access_token) { + return; + } + axios.defaults.headers.common["Authorization"] = `Bearer ${auth.userData.access_token}`; + setHeaderSet(true); + } + }, [auth.userData?.access_token]); + + if (auth.isLoading || !headerSet){ + return ; + } + + return children; + +} + +export default BearerProvider; \ No newline at end of file diff --git a/reacthookspring-frontend/src/app/commons/appHeader/AppHeader.jsx b/reacthookspring-frontend/src/app/commons/appHeader/AppHeader.jsx new file mode 100644 index 0000000..a626914 --- /dev/null +++ b/reacthookspring-frontend/src/app/commons/appHeader/AppHeader.jsx @@ -0,0 +1,38 @@ +import React from "react"; +// Material UI Components +import logo from "../../assets/images/logo-white.png"; +import {AppBar, Button, IconButton, Toolbar, Typography} from "@mui/material"; +import HeaderStyles from "./../../assets/styles/HeaderStyles"; +import {useHistory} from "react-router-dom"; +import {useTranslation} from "react-i18next"; +import {Logout} from "@mui/icons-material"; + +function AppHeader(props) { + const {menuItems, title} = props; + const headerStyles = HeaderStyles(); + const history = useHistory(); + const {t} = useTranslation(); + + return ( + <> + + + Logo of lirejarp + + {title} + +
+ {menuItems.map(item => ( + + ))} + history.push("/logout")}> + + +
+ + ); +} + +export default AppHeader; \ No newline at end of file diff --git a/reacthookspring-frontend/src/app/commons/navigation/Navigation.jsx b/reacthookspring-frontend/src/app/commons/navigation/Navigation.jsx new file mode 100644 index 0000000..7e347a2 --- /dev/null +++ b/reacthookspring-frontend/src/app/commons/navigation/Navigation.jsx @@ -0,0 +1,45 @@ +import React from "react"; +import AppHeader from "./appHeader/AppHeader"; +import SidebarNavigation from "./sidebarNavigation/SidebarNavigation"; +import NavigationStyles from "./NavigationStyles"; + +function Navigation(props) { + const {menuItems, switchLength, title, logo} = props; + const navigationStyles = NavigationStyles(); + + + function renderCorrectNavigation() { + if (menuItems.length > switchLength) { + return ( + <> + + {props.children} + + + ) + } + + return ( + <> + + {props.children} + + ) + } + + return( +
+ {renderCorrectNavigation()} +
+ ) + +} + +Navigation.defaultProps = { + switchLength: 4, + title: "New App", + menuItems: [] + +} + +export default Navigation; diff --git a/reacthookspring-frontend/src/app/commons/navigation/NavigationStyles.js b/reacthookspring-frontend/src/app/commons/navigation/NavigationStyles.js new file mode 100644 index 0000000..bac7daf --- /dev/null +++ b/reacthookspring-frontend/src/app/commons/navigation/NavigationStyles.js @@ -0,0 +1,8 @@ +import {makeStyles} from "@mui/styles"; + +const NavigationStyles = makeStyles(theme => ({ + contentSpacer: { + paddingBottom: theme.spacing(4) + } +})); +export default NavigationStyles; diff --git a/reacthookspring-frontend/src/app/commons/navigation/appHeader/AppHeader.jsx b/reacthookspring-frontend/src/app/commons/navigation/appHeader/AppHeader.jsx new file mode 100644 index 0000000..056383e --- /dev/null +++ b/reacthookspring-frontend/src/app/commons/navigation/appHeader/AppHeader.jsx @@ -0,0 +1,37 @@ +import React from "react"; +// Material UI Components +import {AppBar, Button, IconButton, Toolbar, Typography} from "@mui/material"; +import HeaderStyles from "../../../assets/styles/HeaderStyles"; +import {useHistory} from "react-router-dom"; +import {useTranslation} from "react-i18next"; +import {Logout} from "@mui/icons-material"; + +function AppHeader(props) { + const {menuItems, title, logo} = props; + const headerStyles = HeaderStyles(); + const history = useHistory(); + const {t} = useTranslation(); + + return ( + <> + + + Logo of lirejarp + + {title} + +
+ {menuItems.map(item => ( + + ))} + history.push("/logout")}> + + +
+ + ); +} + +export default AppHeader; diff --git a/reacthookspring-frontend/src/app/commons/navigation/sidebarNavigation/SidebarNavigation.jsx b/reacthookspring-frontend/src/app/commons/navigation/sidebarNavigation/SidebarNavigation.jsx new file mode 100644 index 0000000..1407d07 --- /dev/null +++ b/reacthookspring-frontend/src/app/commons/navigation/sidebarNavigation/SidebarNavigation.jsx @@ -0,0 +1,72 @@ +import React from "react"; +import { + AppBar, + Box, + CssBaseline, + Drawer, + IconButton, + List, + ListItem, + ListItemButton, + ListItemText, + Stack, + Toolbar, + Typography +} from "@mui/material"; +import {Logout} from "@mui/icons-material"; +import HeaderStyles from "../../../assets/styles/HeaderStyles"; +import {useTranslation} from "react-i18next"; +import {useHistory} from "react-router-dom"; + +function SidebarNavigation(props) { + + const headerStyles = HeaderStyles(); + const drawerWidth = 240; + const {t} = useTranslation(); + const history = useHistory(); + + + return ( + + + theme.zIndex.drawer + 1}}> + + Logo of lirejarp + + {props.title} + +
+ history.push("/logout")}> + + + + + + + {props.menuItems.map((menuItem, index) => ( + + history.push(menuItem.link)}> + + + + ))} + + + + + + {props.children} + + + ) +} + +export default SidebarNavigation; diff --git a/reacthookspring-frontend/src/app/features/home/Home.jsx b/reacthookspring-frontend/src/app/features/home/Home.jsx new file mode 100644 index 0000000..de5c110 --- /dev/null +++ b/reacthookspring-frontend/src/app/features/home/Home.jsx @@ -0,0 +1,18 @@ +import {Container, Typography} from "@mui/material"; +import React from "react"; +import {useTranslation} from "react-i18next"; + +function Home() { + const {t} = useTranslation(); + + return ( + + + {t("home.title")} + + {t("home.welcome")} + + ); +} + +export default Home; diff --git a/reacthookspring-frontend/src/app/modifiers/HomeModifier.js b/reacthookspring-frontend/src/app/modifiers/HomeModifier.js new file mode 100644 index 0000000..0b587ed --- /dev/null +++ b/reacthookspring-frontend/src/app/modifiers/HomeModifier.js @@ -0,0 +1,40 @@ +import {produce} from "immer"; + +const RegexConfig = { + prop1: /^[A-Z][a-zA-Z0-9]{1,100}$/ +}; + +const newHome = + {id: null, prop1: null}; + +function toDatabase(home) { + const data = {}; + data.id = home.id; + data.prop1 = home.prop1; + return data; +} + +function updateHome(home, data) { + return produce(home, draft => { + draft.id = data.id; + draft.prop1 = data.prop1; + draft.isValid = isValid(draft); + }); +} + +function updateProp1(home, prop1) { + return produce(home, draft => { + draft.prop1 = prop1; + }); +} + +function isValid(data) { + return RegexConfig.prop1.test(data.prop1); +} + +export { + newHome, + updateHome, + updateProp1, + isValid, + toDatabase}; diff --git a/reacthookspring-frontend/src/app/services/CrudRest.js b/reacthookspring-frontend/src/app/services/CrudRest.js new file mode 100755 index 0000000..fa07f14 --- /dev/null +++ b/reacthookspring-frontend/src/app/services/CrudRest.js @@ -0,0 +1,29 @@ +import axios from "axios"; + +class CrudRest { + constructor(baseUrl) { + this.baseUrl = baseUrl; + } + + create = entity => { + return axios.post(this.baseUrl, entity); + }; + + update = entity => { + return axios.put(this.baseUrl, entity); + }; + + delete = entityId => { + return axios.delete(this.baseUrl + "/" + entityId); + }; + + findAll = () => { + return axios.get(this.baseUrl); + }; + + findById = entityId => { + return axios.get(this.baseUrl + "/" + entityId); + }; +} + +export default CrudRest; diff --git a/reacthookspring-frontend/src/app/services/VersionRest.js b/reacthookspring-frontend/src/app/services/VersionRest.js new file mode 100755 index 0000000..feeaff6 --- /dev/null +++ b/reacthookspring-frontend/src/app/services/VersionRest.js @@ -0,0 +1,13 @@ +import axios from "axios"; + +class VersionRest { + constructor() { + this.baseUrl = window._env_.API_URL + + "/monitoring"; + } + + info = () => { + return axios.get(this.baseUrl + "/info"); + }; +} + +export default VersionRest; diff --git a/reacthookspring-frontend/src/index.css b/reacthookspring-frontend/src/index.css new file mode 100755 index 0000000..bec5506 --- /dev/null +++ b/reacthookspring-frontend/src/index.css @@ -0,0 +1,18 @@ +body { + margin: 0; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", + "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body, html, #root { + height: 100% +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", + monospace; +} diff --git a/reacthookspring-frontend/src/index.js b/reacthookspring-frontend/src/index.js new file mode 100755 index 0000000..9730d79 --- /dev/null +++ b/reacthookspring-frontend/src/index.js @@ -0,0 +1,42 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import "./index.css"; +import App from "./app/App"; +import * as serviceWorker from "./serviceWorker"; +import {HashRouter as Router} from "react-router-dom"; +import "./localization/i18n"; +import {SnackbarProvider} from "notistack"; +import MainTheme from "./app/assets/themes/MainTheme"; +import BearerProvider from "./commons/BearerProvider/BearerProvider"; +import {AuthProvider} from "oidc-react"; + +ReactDOM.render(( + + + { + console.log("on signin"); + }} + > + + + + + + + + + ), + document.getElementById("root") +); + +// If you want your app to work offline and load faster, you can change +// unregister() to register() below. Note this comes with some pitfalls. +// Learn more about service workers: http://bit.ly/CRA-PWA +serviceWorker.unregister(); diff --git a/reacthookspring-frontend/src/localization/i18n.js b/reacthookspring-frontend/src/localization/i18n.js new file mode 100755 index 0000000..75b63e0 --- /dev/null +++ b/reacthookspring-frontend/src/localization/i18n.js @@ -0,0 +1,27 @@ +import i18n from "i18next"; +import {initReactI18next} from "react-i18next"; +import LanguageDetector from "i18next-browser-languagedetector"; +import translationEnEN from "./translation-en-EN"; + +const resources = { + "en-US": {translation: translationEnEN} +}; + +const lngDetectinOptions = { + order: ["navigator", "cookie", "localStorage", "querystring", "htmlTag", "path", "subdomain"] +}; + +i18n + .use(LanguageDetector) + .use(initReactI18next) + .init({ + resources, + detection: lngDetectinOptions, + fallbackLng: ["en-US"], + keySeparator: false, + interpolation: { + escapeValue: false + } + }, null).then(null, null); + +export default i18n; diff --git a/reacthookspring-frontend/src/localization/translation-en-EN.js b/reacthookspring-frontend/src/localization/translation-en-EN.js new file mode 100644 index 0000000..08ace0d --- /dev/null +++ b/reacthookspring-frontend/src/localization/translation-en-EN.js @@ -0,0 +1,6 @@ +const translationEnEN = { + "app.baseName": "ReactHook", + "home.title": "Welcome", + "home.welcome": "Welcome to ProjectBuilder" +}; +export default translationEnEN; diff --git a/reacthookspring-frontend/src/serviceWorker.js b/reacthookspring-frontend/src/serviceWorker.js new file mode 100755 index 0000000..e05fdeb --- /dev/null +++ b/reacthookspring-frontend/src/serviceWorker.js @@ -0,0 +1,135 @@ +// This optional code is used to register a service worker. +// register() is not called by default. + +// This lets the app load faster on subsequent visits in production, and gives +// it offline capabilities. However, it also means that developers (and users) +// will only see deployed updates on subsequent visits to a page, after all the +// existing tabs open on the page have been closed, since previously cached +// resources are updated in the background. + +// To learn more about the benefits of this model and instructions on how to +// opt-in, read http://bit.ly/CRA-PWA + +const isLocalhost = Boolean( + window.location.hostname === "localhost" || + // [::1] is the IPv6 localhost address. + window.location.hostname === "[::1]" || + // 127.0.0.1/8 is considered localhost for IPv4. + window.location.hostname.match( + /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ + ) +); + +export function register(config) { + if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { + // The URL constructor is available in all browsers that support SW. + const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); + if (publicUrl.origin !== window.location.origin) { + // Our service worker won't work if PUBLIC_URL is on a different origin + // from what our page is served on. This might happen if a CDN is used to + // serve assets; see https://github.com/facebook/create-react-app/issues/2374 + return; + } + + window.addEventListener("load", () => { + const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; + + if (isLocalhost) { + // This is running on localhost. Let's check if a service worker still exists or not. + checkValidServiceWorker(swUrl, config); + + // Add some additional logging to localhost, pointing developers to the + // service worker/PWA documentation. + navigator.serviceWorker.ready.then(() => { + console.log( + "This web app is being served cache-first by a service " + + "worker. To learn more, visit http://bit.ly/CRA-PWA" + ); + }); + } else { + // Is not localhost. Just register service worker + registerValidSW(swUrl, config); + } + }); + } +} + +function registerValidSW(swUrl, config) { + navigator.serviceWorker + .register(swUrl) + .then(registration => { + registration.onupdatefound = () => { + const installingWorker = registration.installing; + if (installingWorker == null) { + return; + } + installingWorker.onstatechange = () => { + if (installingWorker.state === "installed") { + if (navigator.serviceWorker.controller) { + // At this point, the updated precached content has been fetched, + // but the previous service worker will still serve the older + // content until all client tabs are closed. + console.log( + "New content is available and will be used when all " + + "tabs for this page are closed. See http://bit.ly/CRA-PWA." + ); + + // Execute callback + if (config && config.onUpdate) { + config.onUpdate(registration); + } + } else { + // At this point, everything has been precached. + // It's the perfect time to display a + // "Content is cached for offline use." message. + console.log("Content is cached for offline use."); + + // Execute callback + if (config && config.onSuccess) { + config.onSuccess(registration); + } + } + } + }; + }; + }) + .catch(error => { + console.error("Error during service worker registration:", error); + }); +} + +function checkValidServiceWorker(swUrl, config) { + // Check if the service worker can be found. If it can't reload the page. + fetch(swUrl) + .then(response => { + // Ensure service worker exists, and that we really are getting a JS file. + const contentType = response.headers.get("content-type"); + if ( + response.status === 404 || + (contentType != null && contentType.indexOf("javascript") === -1) + ) { + // No service worker found. Probably a different app. Reload the page. + navigator.serviceWorker.ready.then(registration => { + registration.unregister().then(() => { + window.location.reload(); + }); + }); + } else { + // Service worker found. Proceed as normal. + registerValidSW(swUrl, config); + } + }) + .catch(() => { + console.log( + "No internet connection found. App is running in offline mode." + ); + }); +} + +export function unregister() { + if ("serviceWorker" in navigator) { + navigator.serviceWorker.ready.then(registration => { + registration.unregister(); + }); + } +} diff --git a/reacthookspring/application/pom.xml b/reacthookspring/application/pom.xml index e664cb6..40b1f7e 100644 --- a/reacthookspring/application/pom.xml +++ b/reacthookspring/application/pom.xml @@ -19,13 +19,17 @@ de.starwit persistence + + org.keycloak + keycloak-spring-boot-starter + org.springframework.boot spring-boot-starter-actuator org.springframework.boot - spring-boot-starter-oauth2-resource-server + spring-boot-starter-oauth2-client org.springframework.boot diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/CorsConfig.java b/reacthookspring/application/src/main/java/de/starwit/application/config/CorsConfig.java index b26384d..50337fc 100644 --- a/reacthookspring/application/src/main/java/de/starwit/application/config/CorsConfig.java +++ b/reacthookspring/application/src/main/java/de/starwit/application/config/CorsConfig.java @@ -1,4 +1,4 @@ -package de.city.application.config; +package de.starwit.application.config; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; @@ -13,10 +13,9 @@ public class CorsConfig implements WebMvcConfigurer { public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedMethods("*") - .allowedOrigins("http://localhost:8080", "http://localhost:8081", "http://localhost:8083", - "http://localhost:3001", "http://localhost:3000", "*") + .allowedOrigins("http://localhost:8080", "http://localhost:8081") .allowedHeaders("*") .allowCredentials(false); } -} +} \ No newline at end of file diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/DisableSecurityConfig.java b/reacthookspring/application/src/main/java/de/starwit/application/config/DisableSecurityConfig.java index b6a82a9..678bc42 100644 --- a/reacthookspring/application/src/main/java/de/starwit/application/config/DisableSecurityConfig.java +++ b/reacthookspring/application/src/main/java/de/starwit/application/config/DisableSecurityConfig.java @@ -1,4 +1,4 @@ -package de.city.application.config; +package de.starwit.application.config; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/KeycloakConfig.java b/reacthookspring/application/src/main/java/de/starwit/application/config/KeycloakConfig.java new file mode 100644 index 0000000..76350a7 --- /dev/null +++ b/reacthookspring/application/src/main/java/de/starwit/application/config/KeycloakConfig.java @@ -0,0 +1,76 @@ +package de.starwit.application.config; + +import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider; +import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Profile; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper; +import org.springframework.security.core.session.SessionRegistryImpl; +import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy; +import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; + +@Profile("!dev") +@EnableWebSecurity +public class KeycloakConfig extends KeycloakWebSecurityConfigurerAdapter { + + private static final String[] AUTH_WHITELIST = { + // -- Swagger UI v2 + "/v2/api-docs", + "/swagger-resources", + "/swagger-resources/**", + "/configuration/ui", + "/configuration/security", + "/swagger-ui.html", + "/webjars/**", + // -- Swagger UI v3 (OpenAPI) + "/v3/api-docs/**", + "/swagger-ui/**" + // other public endpoints of your API may be appended to this array + }; + + @Autowired + public void configureGlobal(AuthenticationManagerBuilder auth) { + SimpleAuthorityMapper grantedAuthorityMapper = new SimpleAuthorityMapper(); + grantedAuthorityMapper.setPrefix("ROLE_"); + + KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider(); + keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(grantedAuthorityMapper); + auth.authenticationProvider(keycloakAuthenticationProvider); + } + + @Bean + @Override + protected SessionAuthenticationStrategy sessionAuthenticationStrategy() { + return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl()); + } + + @Override + public void configure(AuthenticationManagerBuilder auth) { + KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider(); + auth.authenticationProvider(keycloakAuthenticationProvider); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + super.configure(http); + + http + // .csrf().disable() + .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + .and() + .authorizeRequests() + .antMatchers(AUTH_WHITELIST).permitAll() + .antMatchers("/logout.html").permitAll() + .antMatchers("/api/user/logout").permitAll() + .antMatchers("/**").hasAnyRole("admin", "user", "reader") + .anyRequest().authenticated() + .and() + .logout().logoutSuccessUrl("/myLogout.html") + .and().exceptionHandling().accessDeniedPage("/accessDenied.html"); + } +} diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/KeycloakResolverConfig.java b/reacthookspring/application/src/main/java/de/starwit/application/config/KeycloakResolverConfig.java new file mode 100644 index 0000000..4b121a7 --- /dev/null +++ b/reacthookspring/application/src/main/java/de/starwit/application/config/KeycloakResolverConfig.java @@ -0,0 +1,14 @@ +package de.starwit.application.config; + +import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class KeycloakResolverConfig { + + @Bean + public KeycloakSpringBootConfigResolver keycloakConfigResolver() { + return new KeycloakSpringBootConfigResolver(); + } +} diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/MethodSecurityConfig.java b/reacthookspring/application/src/main/java/de/starwit/application/config/MethodSecurityConfig.java new file mode 100644 index 0000000..b9bafab --- /dev/null +++ b/reacthookspring/application/src/main/java/de/starwit/application/config/MethodSecurityConfig.java @@ -0,0 +1,16 @@ +package de.starwit.application.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration; + +@Profile("!dev") +@Configuration +@EnableGlobalMethodSecurity( + prePostEnabled = true, + securedEnabled = true, + jsr250Enabled = true) +public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration { + +} diff --git a/reacthookspring/pom.xml b/reacthookspring/pom.xml index 31cd1a9..21cfce6 100644 --- a/reacthookspring/pom.xml +++ b/reacthookspring/pom.xml @@ -38,6 +38,13 @@ spring-core ${spring-version} + + org.keycloak.bom + keycloak-adapter-bom + 15.0.2 + pom + import + org.springframework.boot spring-boot-starter @@ -51,7 +58,7 @@ org.springframework.boot - spring-boot-starter-oauth2-resource-server + spring-boot-starter-oauth2-client ${spring-boot-version} diff --git a/reacthookspring/rest/pom.xml b/reacthookspring/rest/pom.xml index 69f239f..8edae42 100644 --- a/reacthookspring/rest/pom.xml +++ b/reacthookspring/rest/pom.xml @@ -15,6 +15,10 @@ org.springframework.security spring-security-web + + org.keycloak + keycloak-spring-security-adapter + org.springframework.boot spring-boot-starter-validation diff --git a/reacthookspring/webclient/app/package.json b/reacthookspring/webclient/app/package.json index 2cfbd10..ed0418f 100644 --- a/reacthookspring/webclient/app/package.json +++ b/reacthookspring/webclient/app/package.json @@ -27,7 +27,6 @@ "@date-io/moment": "^2.15.0", "@mui/x-date-pickers": "^5.0.1", "moment": "^2.29.4", - "oidc-react": "^3.0.3" }, "scripts": { "start": "react-scripts start", diff --git a/template-config-frontend.json b/template-config-frontend.json index 7ae92fb..dc4da18 100755 --- a/template-config-frontend.json +++ b/template-config-frontend.json @@ -6,55 +6,55 @@ { "fileName": "AppConfig.js", "templatePath": "webclient/AppConfig.ftl", - "targetPath": "webclient/app/src/app/", + "targetPath": "src/app/", "category": "FRONTEND" }, { "fileName": "translation-en-EN.js", "templatePath": "webclient/translationEnEN.ftl", - "targetPath": "webclient/app/src/localization/", + "targetPath": "src/localization/", "category": "FRONTEND" }, { "fileName": "${entity.name}Rest.js", "templatePath": "webclient/EntityRest.ftl", - "targetPath": "webclient/app/src/app/services/", + "targetPath": "src/app/services/", "category": "FRONTEND" }, { "fileName": "${entity.name}Modifier.js", "templatePath": "webclient/EntityModifier.ftl", - "targetPath": "webclient/app/src/app/modifiers/", + "targetPath": "src/app/modifiers/", "category": "FRONTEND" }, { "fileName": "MainContentRouter.jsx", "templatePath": "webclient/MainContentRouter.ftl", - "targetPath": "webclient/app/src/app/", + "targetPath": "src/app/", "category": "FRONTEND" }, { "fileName": "${entity.name}Main.jsx", "templatePath": "webclient/EntityMain.ftl", - "targetPath": "webclient/app/src/app/features/${entity.name?uncap_first}/", + "targetPath": "src/app/features/${entity.name?uncap_first}/", "category": "FRONTEND" }, { "fileName": "${entity.name}Detail.jsx", "templatePath": "webclient/EntityDetail.ftl", - "targetPath": "webclient/app/src/app/features/${entity.name?uncap_first}/", + "targetPath": "src/app/features/${entity.name?uncap_first}/", "category": "FRONTEND" }, { "fileName": "${entity.name}Overview.jsx", "templatePath": "webclient/EntityOverview.ftl", - "targetPath": "webclient/app/src/app/features/${entity.name?uncap_first}/", + "targetPath": "src/app/features/${entity.name?uncap_first}/", "category": "FRONTEND" }, { "fileName": ".public-env", "templatePath": "webclient/.public-env.ftl", - "targetPath": "webclient/.public-env", + "targetPath": ".public-env", "category": "FRONTEND" } ] From 30809e7212f059fa28421541155cceb4053fe2e4 Mon Sep 17 00:00:00 2001 From: Cedric Rische Date: Fri, 24 Mar 2023 23:18:23 +0100 Subject: [PATCH 05/22] Removed more stuff --- reacthookspring/webclient/app/package.json | 2 +- .../commons/BearerProvider/BearerProvider.jsx | 29 ------------------- reacthookspring/webclient/app/src/index.js | 22 ++------------ 3 files changed, 4 insertions(+), 49 deletions(-) delete mode 100644 reacthookspring/webclient/app/src/app/commons/BearerProvider/BearerProvider.jsx diff --git a/reacthookspring/webclient/app/package.json b/reacthookspring/webclient/app/package.json index ed0418f..e8d4af8 100644 --- a/reacthookspring/webclient/app/package.json +++ b/reacthookspring/webclient/app/package.json @@ -26,7 +26,7 @@ "use-immer": "^0.7.0", "@date-io/moment": "^2.15.0", "@mui/x-date-pickers": "^5.0.1", - "moment": "^2.29.4", + "moment": "^2.29.4" }, "scripts": { "start": "react-scripts start", diff --git a/reacthookspring/webclient/app/src/app/commons/BearerProvider/BearerProvider.jsx b/reacthookspring/webclient/app/src/app/commons/BearerProvider/BearerProvider.jsx deleted file mode 100644 index 6ff6346..0000000 --- a/reacthookspring/webclient/app/src/app/commons/BearerProvider/BearerProvider.jsx +++ /dev/null @@ -1,29 +0,0 @@ -import {useEffect, useState} from "react"; -import {useAuth} from "oidc-react"; -import {CircularProgress} from "@material-ui/core"; -import axios from "axios"; - -function BearerProvider(props) { - const [headerSet, setHeaderSet] = useState(false); - const auth = useAuth(); - const {children} = props; - - useEffect(() => { - if (auth?.userData?.access_token) { - if (!auth?.userData?.access_token) { - return; - } - axios.defaults.headers.common["Authorization"] = `Bearer ${auth.userData.access_token}`; - setHeaderSet(true); - } - }, [auth.userData?.access_token]); - - if (auth.isLoading || !headerSet){ - return ; - } - - return children; - -} - -export default BearerProvider; \ No newline at end of file diff --git a/reacthookspring/webclient/app/src/index.js b/reacthookspring/webclient/app/src/index.js index 9730d79..ea42528 100755 --- a/reacthookspring/webclient/app/src/index.js +++ b/reacthookspring/webclient/app/src/index.js @@ -7,29 +7,13 @@ import {HashRouter as Router} from "react-router-dom"; import "./localization/i18n"; import {SnackbarProvider} from "notistack"; import MainTheme from "./app/assets/themes/MainTheme"; -import BearerProvider from "./commons/BearerProvider/BearerProvider"; -import {AuthProvider} from "oidc-react"; ReactDOM.render(( - { - console.log("on signin"); - }} - > - - - - - - + + + ), From cae38e4a93ac4aa41163366ebe1e93ccd1df0c8f Mon Sep 17 00:00:00 2001 From: Cedric Rische Date: Fri, 24 Mar 2023 23:19:49 +0100 Subject: [PATCH 06/22] Rolled back --- reacthookspring/webclient/app/src/app/services/VersionRest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reacthookspring/webclient/app/src/app/services/VersionRest.js b/reacthookspring/webclient/app/src/app/services/VersionRest.js index feeaff6..548d644 100755 --- a/reacthookspring/webclient/app/src/app/services/VersionRest.js +++ b/reacthookspring/webclient/app/src/app/services/VersionRest.js @@ -2,7 +2,7 @@ import axios from "axios"; class VersionRest { constructor() { - this.baseUrl = window._env_.API_URL + + "/monitoring"; + this.baseUrl = window.location.pathname + + "/monitoring"; } info = () => { From e507313b41cb798f482b094fa3523a150797926a Mon Sep 17 00:00:00 2001 From: Cedric Rische Date: Fri, 24 Mar 2023 23:21:39 +0100 Subject: [PATCH 07/22] Removed double plus --- reacthookspring/webclient/app/src/app/services/VersionRest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reacthookspring/webclient/app/src/app/services/VersionRest.js b/reacthookspring/webclient/app/src/app/services/VersionRest.js index 548d644..2e028d2 100755 --- a/reacthookspring/webclient/app/src/app/services/VersionRest.js +++ b/reacthookspring/webclient/app/src/app/services/VersionRest.js @@ -2,7 +2,7 @@ import axios from "axios"; class VersionRest { constructor() { - this.baseUrl = window.location.pathname + + "/monitoring"; + this.baseUrl = window.location.pathname + "/monitoring"; } info = () => { From 938938da6236f13dd80498bfe94f3bfdbf3c6ea1 Mon Sep 17 00:00:00 2001 From: Cedric Rische Date: Fri, 24 Mar 2023 23:26:32 +0100 Subject: [PATCH 08/22] Updated template config --- template-config-backend.json | 6 ------ template-config-frontend.json | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/template-config-backend.json b/template-config-backend.json index 087b953..189d98c 100755 --- a/template-config-backend.json +++ b/template-config-backend.json @@ -74,12 +74,6 @@ "templatePath": "rest/ControllerIntegrationTest.ftl", "targetPath": "rest/src/test/java/de/${app.packageName?lower_case}/rest/integration/", "category": "REST" - }, - { - "fileName": ".public-env", - "templatePath": "webclient/.public-env.ftl", - "targetPath": "webclient/.public-env", - "category": "FRONTEND" } ] } diff --git a/template-config-frontend.json b/template-config-frontend.json index dc4da18..4976789 100755 --- a/template-config-frontend.json +++ b/template-config-frontend.json @@ -17,7 +17,7 @@ }, { "fileName": "${entity.name}Rest.js", - "templatePath": "webclient/EntityRest.ftl", + "templatePath": "webclient/EntityRestStandalone.ftl", "targetPath": "src/app/services/", "category": "FRONTEND" }, From 5ad599a45974ae114475d1c27e11d6a4ce8ea147 Mon Sep 17 00:00:00 2001 From: Cedric Rische Date: Sat, 15 Jul 2023 21:25:19 +0200 Subject: [PATCH 09/22] Migrated (hopefully seamless) to new Spring boot version --- generator-templates/persistence/Entity.ftl | 2 +- generator-templates/rest/Controller.ftl | 8 +- reacthookspring/application/pom.xml | 10 +- .../config/DisableSecurityConfig.java | 19 - .../application/config/KeycloakConfig.java | 76 - .../config/KeycloakResolverConfig.java | 14 - .../config/MethodSecurityConfig.java | 16 - .../application/config/SecurityConfig.java | 97 + .../src/main/resources/application.properties | 23 +- .../deployment/keycloak/imports/realm.json | 2137 ++++++++++++++++- .../deployment/localenv-docker-compose.yml | 21 +- reacthookspring/persistence/pom.xml | 8 +- .../converter/ListToStringConverter.java | 2 +- .../persistence/entity/AbstractEntity.java | 10 +- reacthookspring/pom.xml | 23 +- reacthookspring/rest/pom.xml | 6 +- .../rest/controller/UserController.java | 6 +- .../exception/ControllerExceptionHandler.java | 5 +- reacthookspring/service/pom.xml | 2 +- .../service/impl/ServiceInterface.java | 2 +- reacthookspring/webclient/app/.gitignore | 2 + 21 files changed, 2238 insertions(+), 251 deletions(-) delete mode 100644 reacthookspring/application/src/main/java/de/starwit/application/config/DisableSecurityConfig.java delete mode 100644 reacthookspring/application/src/main/java/de/starwit/application/config/KeycloakConfig.java delete mode 100644 reacthookspring/application/src/main/java/de/starwit/application/config/KeycloakResolverConfig.java delete mode 100644 reacthookspring/application/src/main/java/de/starwit/application/config/MethodSecurityConfig.java create mode 100644 reacthookspring/application/src/main/java/de/starwit/application/config/SecurityConfig.java diff --git a/generator-templates/persistence/Entity.ftl b/generator-templates/persistence/Entity.ftl index 05a4074..ba8ef0e 100644 --- a/generator-templates/persistence/Entity.ftl +++ b/generator-templates/persistence/Entity.ftl @@ -9,7 +9,7 @@ import de.${app.packageName?lower_case}.persistence.serializer.ZonedDateTimeSeri import de.${app.packageName?lower_case}.persistence.serializer.ZonedDateTimeDeserializer; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import javax.persistence.CascadeType; +import jakarta.persistence.CascadeType; /** * ${entity.name} Entity class diff --git a/generator-templates/rest/Controller.ftl b/generator-templates/rest/Controller.ftl index 514f78e..2615567 100755 --- a/generator-templates/rest/Controller.ftl +++ b/generator-templates/rest/Controller.ftl @@ -2,8 +2,8 @@ package de.${app.packageName?lower_case}.rest.controller; import java.util.List; -import javax.persistence.EntityNotFoundException; -import javax.validation.Valid; +import jakarta.persistence.EntityNotFoundException; +import jakarta.validation.Valid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,13 +49,13 @@ public class ${entity.name}Controller { <#list (entity.relationships) as relation> <#if relation.relationshipType == "OneToOne" || relation.relationshipType == "ManyToOne"> @Operation(summary = "Get all ${entity.name?lower_case} without ${relation.relationshipName}") - @GetMapping(value = "find-without-${relation.relationshipName}") + @GetMapping(value = "/find-without-${relation.relationshipName}") public List<${entity.name}Entity> findAllWithout${relation.relationshipName?cap_first}() { return ${entity.name?lower_case}Service.findAllWithout${relation.relationshipName?cap_first}(); } @Operation(summary = "Get all ${entity.name?lower_case} without other ${relation.relationshipName}") - @GetMapping(value = "find-without-other-${relation.relationshipName}/{id}") + @GetMapping(value = "/find-without-other-${relation.relationshipName}/{id}") public List<${entity.name}Entity> findAllWithoutOther${relation.relationshipName?cap_first}(@PathVariable("id") Long id) { return ${entity.name?lower_case}Service.findAllWithoutOther${relation.relationshipName?cap_first}(id); } diff --git a/reacthookspring/application/pom.xml b/reacthookspring/application/pom.xml index 40b1f7e..d010b84 100644 --- a/reacthookspring/application/pom.xml +++ b/reacthookspring/application/pom.xml @@ -19,10 +19,6 @@ de.starwit persistence - - org.keycloak - keycloak-spring-boot-starter - org.springframework.boot spring-boot-starter-actuator @@ -37,11 +33,11 @@ org.springdoc - springdoc-openapi-ui + springdoc-openapi-starter-webmvc-ui - org.springdoc - springdoc-openapi-security + org.projectlombok + lombok diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/DisableSecurityConfig.java b/reacthookspring/application/src/main/java/de/starwit/application/config/DisableSecurityConfig.java deleted file mode 100644 index 678bc42..0000000 --- a/reacthookspring/application/src/main/java/de/starwit/application/config/DisableSecurityConfig.java +++ /dev/null @@ -1,19 +0,0 @@ -package de.starwit.application.config; - -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; - -@Profile("dev") -@EnableWebSecurity -@Configuration -public class DisableSecurityConfig extends WebSecurityConfigurerAdapter { - - @Override - protected void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests().antMatchers("/**").permitAll().anyRequest().authenticated().and().csrf().disable(); - } -} diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/KeycloakConfig.java b/reacthookspring/application/src/main/java/de/starwit/application/config/KeycloakConfig.java deleted file mode 100644 index 76350a7..0000000 --- a/reacthookspring/application/src/main/java/de/starwit/application/config/KeycloakConfig.java +++ /dev/null @@ -1,76 +0,0 @@ -package de.starwit.application.config; - -import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider; -import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Profile; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper; -import org.springframework.security.core.session.SessionRegistryImpl; -import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy; -import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; -import org.springframework.security.web.csrf.CookieCsrfTokenRepository; - -@Profile("!dev") -@EnableWebSecurity -public class KeycloakConfig extends KeycloakWebSecurityConfigurerAdapter { - - private static final String[] AUTH_WHITELIST = { - // -- Swagger UI v2 - "/v2/api-docs", - "/swagger-resources", - "/swagger-resources/**", - "/configuration/ui", - "/configuration/security", - "/swagger-ui.html", - "/webjars/**", - // -- Swagger UI v3 (OpenAPI) - "/v3/api-docs/**", - "/swagger-ui/**" - // other public endpoints of your API may be appended to this array - }; - - @Autowired - public void configureGlobal(AuthenticationManagerBuilder auth) { - SimpleAuthorityMapper grantedAuthorityMapper = new SimpleAuthorityMapper(); - grantedAuthorityMapper.setPrefix("ROLE_"); - - KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider(); - keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(grantedAuthorityMapper); - auth.authenticationProvider(keycloakAuthenticationProvider); - } - - @Bean - @Override - protected SessionAuthenticationStrategy sessionAuthenticationStrategy() { - return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl()); - } - - @Override - public void configure(AuthenticationManagerBuilder auth) { - KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider(); - auth.authenticationProvider(keycloakAuthenticationProvider); - } - - @Override - protected void configure(HttpSecurity http) throws Exception { - super.configure(http); - - http - // .csrf().disable() - .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) - .and() - .authorizeRequests() - .antMatchers(AUTH_WHITELIST).permitAll() - .antMatchers("/logout.html").permitAll() - .antMatchers("/api/user/logout").permitAll() - .antMatchers("/**").hasAnyRole("admin", "user", "reader") - .anyRequest().authenticated() - .and() - .logout().logoutSuccessUrl("/myLogout.html") - .and().exceptionHandling().accessDeniedPage("/accessDenied.html"); - } -} diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/KeycloakResolverConfig.java b/reacthookspring/application/src/main/java/de/starwit/application/config/KeycloakResolverConfig.java deleted file mode 100644 index 4b121a7..0000000 --- a/reacthookspring/application/src/main/java/de/starwit/application/config/KeycloakResolverConfig.java +++ /dev/null @@ -1,14 +0,0 @@ -package de.starwit.application.config; - -import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class KeycloakResolverConfig { - - @Bean - public KeycloakSpringBootConfigResolver keycloakConfigResolver() { - return new KeycloakSpringBootConfigResolver(); - } -} diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/MethodSecurityConfig.java b/reacthookspring/application/src/main/java/de/starwit/application/config/MethodSecurityConfig.java deleted file mode 100644 index da595d1..0000000 --- a/reacthookspring/application/src/main/java/de/starwit/application/config/MethodSecurityConfig.java +++ /dev/null @@ -1,16 +0,0 @@ -package de.starwit.application.config; - -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration; - -@Profile("!dev") -@Configuration -@EnableGlobalMethodSecurity( - prePostEnabled = true, - securedEnabled = true, - jsr250Enabled = true) -public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration { - -} diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/SecurityConfig.java b/reacthookspring/application/src/main/java/de/starwit/application/config/SecurityConfig.java new file mode 100644 index 0000000..dbe3c2e --- /dev/null +++ b/reacthookspring/application/src/main/java/de/starwit/application/config/SecurityConfig.java @@ -0,0 +1,97 @@ +package de.starwit.application.config; + +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; +import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.stereotype.Component; + +import java.util.*; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + static final Logger LOG = LoggerFactory.getLogger(SecurityConfig.class); + + private static final String[] AUTH_WHITELIST = { + // -- Swagger UI v2 + "/v2/api-docs", + "/swagger-resources", + "/swagger-resources/**", + "/configuration/ui", + "/configuration/security", + "/swagger-ui.html", + "/webjars/**", + // -- Swagger UI v3 (OpenAPI) + "/v3/api-docs/**", + "/swagger-ui/**" + // other public endpoints of your API may be appended to this array + }; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + // TODO Not great, has to be fixed + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(authorize -> authorize + .requestMatchers(AUTH_WHITELIST).permitAll() + .requestMatchers("/**").hasAnyRole("admin", "user", "reader") + .requestMatchers("/", "/login/**", "/oauth2/**").permitAll() + .anyRequest().authenticated() + ) + // Maybe https://stackoverflow.com/questions/74939220/classnotfoundexception-org-springframework-security-oauth2-server-resource-web + .oauth2Login(Customizer.withDefaults()); + return http.build(); + } + + + // Taken from https://stackoverflow.com/questions/74939220/classnotfoundexception-org-springframework-security-oauth2-server-resource-web + @Component + @RequiredArgsConstructor + static class GrantedAuthoritiesMapperImpl implements GrantedAuthoritiesMapper { + + @Override + public Collection mapAuthorities(Collection authorities) { + Set mappedAuthorities = new HashSet<>(); + + authorities.forEach(authority -> { + if (authority instanceof SimpleGrantedAuthority) { + mappedAuthorities.add(authority); + } + if (authority instanceof OidcUserAuthority) { + try { + final var oidcUserAuthority = (OidcUserAuthority) authority; + final List roles = oidcUserAuthority.getUserInfo().getClaimAsStringList("roles"); + + mappedAuthorities.addAll(roles.stream().map(SimpleGrantedAuthority::new).toList()); + } catch (NullPointerException e) { + LOG.error("Could not read the roles from claims.realm_access.roles -- Is the Mapper set up correctly?"); + } + } else if (authority instanceof OAuth2UserAuthority oauth2UserAuthority) { + final var userAttributes = oauth2UserAuthority.getAttributes(); + final Map> realmAccess = (Map>) userAttributes.get("realm_access"); + final List roles = realmAccess.get("roles"); + + mappedAuthorities.addAll(roles.stream().map(SimpleGrantedAuthority::new).toList()); + } + }); + + return mappedAuthorities; + } + + } + + +} \ No newline at end of file diff --git a/reacthookspring/application/src/main/resources/application.properties b/reacthookspring/application/src/main/resources/application.properties index 56a1535..a23d34b 100644 --- a/reacthookspring/application/src/main/resources/application.properties +++ b/reacthookspring/application/src/main/resources/application.properties @@ -29,27 +29,12 @@ spring.flyway.locations=classpath:db/migration spring.flyway.encoding=UTF-8 spring.flyway.placeholder-replacement=false -springdoc.swagger-ui.disable-swagger-default-url=true -#spring.profiles.active=dev -springdoc.api-docs.path=/api-docs -springdoc.swagger-ui.path=/swagger-ui.html -springdoc.swagger-ui.csrf.enabled=true -#logging.level.org.springframework=DEBUG -#spring.jpa.generate-ddl=true -#spring.jpa.show-sql=true -#spring.jpa.hibernate.ddl-auto=create-drop -#spring.jpa.properties.javax.persistence.schema-generation.create-source=metadata -#spring.jpa.properties.javax.persistence.schema-generation.scripts.action=create -#spring.jpa.properties.javax.persistence.schema-generation.scripts.create-target=/home/anett/create.sql - #logging.level.org.springframework.security=DEBUG -keycloak.auth-server-url=http://localhost:8080/auth -keycloak.realm=reacthookspring -keycloak.resource=reacthookspring -keycloak.principal-attribute=preferred_username -keycloak.public-client=true -keycloak.enabled=true +spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8080/auth/realms/reacthookspring +spring.security.oauth2.client.registration.keycloak.client-id=reacthookspring +spring.security.oauth2.client.registration.keycloak.client-secret=reacthookspring +spring.security.oauth2.client.registration.keycloak.scope=openid diff --git a/reacthookspring/deployment/keycloak/imports/realm.json b/reacthookspring/deployment/keycloak/imports/realm.json index b63daf2..65e579a 100644 --- a/reacthookspring/deployment/keycloak/imports/realm.json +++ b/reacthookspring/deployment/keycloak/imports/realm.json @@ -1,123 +1,716 @@ { - "id": "reacthookspring", "realm": "reacthookspring", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, "roles": { "realm": [ { - "name": "ROLE_user", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "attributes": {} + }, + { + "name": "uma_authorization", + "description": "${role_uma_authorization}", "composite": false, "clientRole": false, - "containerId": "reacthookspring", + "attributes": {} + }, + { + "name": "default-roles-reacthookspring", + "description": "${role_default-roles}", + "composite": true, + "composites": { + "realm": [ + "offline_access", + "uma_authorization" + ], + "client": { + "account": [ + "manage-account", + "view-profile" + ] + } + }, + "clientRole": false, "attributes": {} }, { "name": "ROLE_admin", "composite": false, "clientRole": false, - "containerId": "reacthookspring", "attributes": {} }, { "name": "ROLE_reader", "composite": false, "clientRole": false, - "containerId": "reacthookspring", + "attributes": {} + }, + { + "name": "ROLE_user", + "composite": false, + "clientRole": false, "attributes": {} } ], "client": { + "realm-management": [ + { + "name": "realm-admin", + "description": "${role_realm-admin}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-realms", + "view-clients", + "view-realm", + "manage-clients", + "impersonation", + "view-users", + "manage-users", + "query-clients", + "query-groups", + "query-users", + "view-events", + "manage-identity-providers", + "manage-authorization", + "manage-events", + "view-authorization", + "create-client", + "view-identity-providers", + "manage-realm" + ] + } + }, + "clientRole": true, + "attributes": {} + }, + { + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "attributes": {} + }, + { + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-clients" + ] + } + }, + "clientRole": true, + "attributes": {} + }, + { + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "attributes": {} + }, + { + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "attributes": {} + }, + { + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "attributes": {} + }, + { + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-groups", + "query-users" + ] + } + }, + "clientRole": true, + "attributes": {} + }, + { + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "attributes": {} + }, + { + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "attributes": {} + }, + { + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "attributes": {} + }, + { + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "attributes": {} + }, + { + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "attributes": {} + }, + { + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "attributes": {} + }, + { + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "attributes": {} + }, + { + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "attributes": {} + }, + { + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "attributes": {} + }, + { + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "attributes": {} + }, + { + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "attributes": {} + }, + { + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "attributes": {} + } + ], + "security-admin-console": [], + "admin-cli": [], + "account-console": [], + "broker": [ + { + "name": "read-token", + "description": "${role_read-token}", + "composite": false, + "clientRole": true, + "attributes": {} + } + ], + "account": [ + { + "name": "delete-account", + "description": "${role_delete-account}", + "composite": false, + "clientRole": true, + "attributes": {} + }, + { + "name": "view-applications", + "description": "${role_view-applications}", + "composite": false, + "clientRole": true, + "attributes": {} + }, + { + "name": "view-consent", + "description": "${role_view-consent}", + "composite": false, + "clientRole": true, + "attributes": {} + }, + { + "name": "manage-account", + "description": "${role_manage-account}", + "composite": true, + "composites": { + "client": { + "account": [ + "manage-account-links" + ] + } + }, + "clientRole": true, + "attributes": {} + }, + { + "name": "view-profile", + "description": "${role_view-profile}", + "composite": false, + "clientRole": true, + "attributes": {} + }, + { + "name": "manage-account-links", + "description": "${role_manage-account-links}", + "composite": false, + "clientRole": true, + "attributes": {} + }, + { + "name": "view-groups", + "description": "${role_view-groups}", + "composite": false, + "clientRole": true, + "attributes": {} + }, + { + "name": "manage-consent", + "description": "${role_manage-consent}", + "composite": true, + "composites": { + "client": { + "account": [ + "view-consent" + ] + } + }, + "clientRole": true, + "attributes": {} + } + ], "reacthookspring": [] } }, - "groups": [ + "groups": [], + "defaultRole": { + "name": "default-roles-reacthookspring", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false + }, + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpPolicyCodeReusable": false, + "otpSupportedApplications": [ + "totpAppFreeOTPName", + "totpAppGoogleName" + ], + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "scopeMappings": [ { - "name": "public", - "path": "/public", - "attributes": {}, - "realmRoles": [ - "ROLE_reader" - ], - "clientRoles": {}, - "subGroups": [] + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] } ], - "defaultGroups": [ - "/public" - ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account", + "view-groups" + ] + } + ] + }, "clients": [ + { + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/reacthookspring/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/reacthookspring/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "secret": null + }, + { + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/reacthookspring/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/reacthookspring/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "secret": null + }, + { + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "secret": null, + "rootUrl": null + }, + { + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "secret": null, + "rootUrl": null + }, { "clientId": "reacthookspring", "name": "reacthookspring", - "rootUrl": "", + "description": "", + "adminUrl": "", "baseUrl": "", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", + "secret": "reacthookspring", "redirectUris": [ "*" ], "webOrigins": [ - "+" + "*" ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, "standardFlowEnabled": true, "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, + "directAccessGrantsEnabled": true, "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, + "publicClient": false, + "frontchannelLogout": true, "protocol": "openid-connect", "attributes": { - "id.token.as.detached.signature": "false", - "saml.assertion.signature": "false", - "saml.force.post.binding": "false", - "saml.multivalued.roles": "false", - "saml.encrypt": "false", - "login_theme": "keycloak", - "oauth2.device.authorization.grant.enabled": "false", - "backchannel.logout.revoke.offline.tokens": "false", - "saml.server.signature": "false", - "saml.server.signature.keyinfo.ext": "false", - "use.refresh.tokens": "true", - "exclude.session.state.from.auth.response": "false", "oidc.ciba.grant.enabled": "false", - "saml.artifact.binding": "false", + "client.secret.creation.time": "1689243300", "backchannel.logout.session.required": "true", - "client_credentials.use_refresh_token": "false", - "saml_force_name_id_format": "false", - "require.pushed.authorization.requests": "false", - "saml.client.signature": "false", - "tls.client.certificate.bound.access.tokens": "false", - "saml.authnstatement": "false", "display.on.consent.screen": "false", - "saml.onetimeuse.condition": "false" + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false" }, "authenticationFlowBindingOverrides": {}, "fullScopeAllowed": true, "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], "protocolMappers": [ { + "id": "8741eb9f-9d31-4f0d-86c9-a5b9c66b4b5f", "name": "groups", "protocol": "openid-connect", - "protocolMapper": "oidc-group-membership-mapper", + "protocolMapper": "oidc-usermodel-realm-role-mapper", "consentRequired": false, "config": { - "full.path": "false", - "id.token.claim": "false", + "multivalued": "true", + "user.attribute": "foo", + "id.token.claim": "true", "access.token.claim": "true", "claim.name": "groups", - "userinfo.token.claim": "true" + "jsonType.label": "String" + } + }, + { + "id": "6db650d3-b452-4d98-b3eb-11288006100a", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "roles", + "jsonType.label": "String" } } - ], + ] + }, + { + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, "defaultClientScopes": [ "web-origins", - "roles", + "acr", "profile", + "roles", "email" ], "optionalClientScopes": [ @@ -125,7 +718,1453 @@ "phone", "offline_access", "microprofile-jwt" - ] + ], + "secret": null, + "rootUrl": null + }, + { + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/reacthookspring/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/admin/reacthookspring/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "secret": null + } + ], + "clientScopes": [ + { + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "long" + } + }, + { + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + }, + { + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + } + ] + }, + { + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": {} + } + ] + }, + { + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + }, + { + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + } + ] + }, + { + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + }, + { + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + } + ] + }, + { + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + }, + { + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + } + ] + }, + { + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + }, + { + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + } + ] + }, + { + "name": "acr", + "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "name": "acr loa level", + "protocol": "openid-connect", + "protocolMapper": "oidc-acr-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true" + } + } + ] + } + ], + "defaultDefaultClientScopes": [ + "role_list", + "profile", + "email", + "roles", + "web-origins", + "acr" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "address", + "phone", + "microprofile-jwt" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": { + "password": "", + "port": "", + "host": "", + "from": "", + "fromDisplayName": "", + "user": "", + "ssl": "", + "replyToDisplayName": "", + "starttls": "" + }, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + }, + { + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + }, + { + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-address-mapper", + "oidc-full-name-mapper", + "oidc-usermodel-property-mapper", + "saml-user-attribute-mapper", + "saml-role-list-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-usermodel-attribute-mapper", + "saml-user-property-mapper" + ] + } + }, + { + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-usermodel-property-mapper", + "saml-user-attribute-mapper", + "saml-user-property-mapper", + "oidc-full-name-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-address-mapper", + "saml-role-list-mapper" + ] + } + } + ], + "org.keycloak.userprofile.UserProfileProvider": [ + { + "providerId": "declarative-user-profile", + "subComponents": {}, + "config": {} + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false + } + ] + }, + { + "alias": "Authentication Options", + "description": "Authentication options.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "basic-auth", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "basic-auth-otp", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Account verification options", + "userSetupAllowed": false + } + ] + }, + { + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false + } + ] + }, + { + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "forms", + "userSetupAllowed": false + } + ] + }, + { + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "User creation or linking", + "userSetupAllowed": false + } + ] + }, + { + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "alias": "http challenge", + "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "no-cookie-redirect", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Authentication Options", + "userSetupAllowed": false + } + ] + }, + { + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": true, + "flowAlias": "registration form", + "userSetupAllowed": false + } + ] + }, + { + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-profile-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "autheticatorFlow": true, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + } + ], + "authenticatorConfig": [ + { + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "terms_and_conditions", + "name": "Terms and Conditions", + "providerId": "terms_and_conditions", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "webauthn-register", + "name": "Webauthn Register", + "providerId": "webauthn-register", + "enabled": true, + "defaultAction": false, + "priority": 70, + "config": {} + }, + { + "alias": "webauthn-register-passwordless", + "name": "Webauthn Register Passwordless", + "providerId": "webauthn-register-passwordless", + "enabled": true, + "defaultAction": false, + "priority": 80, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaExpiresIn": "120", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DeviceCodeLifespan": "600", + "oauth2DevicePollingInterval": "5", + "clientOfflineSessionMaxLifespan": "0", + "clientSessionIdleTimeout": "0", + "parRequestUriLifespan": "60", + "clientSessionMaxLifespan": "0", + "clientOfflineSessionIdleTimeout": "0", + "cibaInterval": "5", + "realmReusableOtpCode": "false" + }, + "keycloakVersion": "20.0.2", + "userManagedAccessAllowed": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + }, + "users": [ + { + "createdTimestamp": 1689189284738, + "username": "admin", + "enabled": true, + "totp": false, + "emailVerified": false, + "credentials": [ + { + "id": "1ec1abd6-bb09-4a90-b874-bbb8e1e64290", + "type": "password", + "createdDate": 1689189284762, + "secretData": "{\"value\":\"bdTG67Kc8WkmG5O9FidYPVms6la4EVyA7eRGm4aKmbpuwXIO4PGHZxYzLWeLAaRstqz8xatqtostvs4LKWd/vw==\",\"salt\":\"y99TTu8C8VZFt8j64Lf2hw==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": [ + "default-roles-reacthookspring", + "ROLE_admin" + ], + "notBefore": 0 + }, + { + "createdTimestamp": 1689189284782, + "username": "reader", + "enabled": true, + "totp": false, + "emailVerified": false, + "credentials": [ + { + "id": "f7724b15-c2f0-4625-8528-cebbb89268f1", + "type": "password", + "createdDate": 1689189284783, + "secretData": "{\"value\":\"o2sxyw56EK8jKnh0CQSV2FiN9plXTSzwOWNlt7SiIUd/Ox+huiDWYGlCL4gqSM0R/3Q+PONjG0Dl7dQrvtfU2A==\",\"salt\":\"m63ldw2Y1VagMqRIEhQZOg==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":100000,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": [ + "default-roles-reacthookspring", + "ROLE_reader" + ], + "notBefore": 0 + }, + { + "createdTimestamp": 1689189284771, + "username": "user", + "enabled": true, + "totp": false, + "emailVerified": false, + "credentials": [ + { + "id": "6de67af4-01e6-4092-b184-45e64ba67ad4", + "type": "password", + "createdDate": 1689189284774, + "secretData": "{\"value\":\"o2sxyw56EK8jKnh0CQSV2FiN9plXTSzwOWNlt7SiIUd/Ox+huiDWYGlCL4gqSM0R/3Q+PONjG0Dl7dQrvtfU2A==\",\"salt\":\"m63ldw2Y1VagMqRIEhQZOg==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":100000,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": [ + "default-roles-reacthookspring", + "ROLE_user" + ], + "notBefore": 0 } ] } \ No newline at end of file diff --git a/reacthookspring/deployment/localenv-docker-compose.yml b/reacthookspring/deployment/localenv-docker-compose.yml index 44777b2..8406fd3 100644 --- a/reacthookspring/deployment/localenv-docker-compose.yml +++ b/reacthookspring/deployment/localenv-docker-compose.yml @@ -44,23 +44,22 @@ services: - backend reacthookspring-keycloak: - image: jboss/keycloak + image: quay.io/keycloak/keycloak volumes: - - ./keycloak/imports:/opt/jboss/keycloak/imports - - ./keycloak/local-test-users.json:/opt/jboss/keycloak/standalone/configuration/keycloak-add-user.json + - ./keycloak/imports:/opt/keycloak/data/import depends_on: reacthookspring-db-keycloak: condition: service_healthy restart: on-failure environment: - KEYCLOAK_IMPORT: /opt/jboss/keycloak/imports/realm.json - DB_VENDOR: mariadb - DB_ADDR: reacthookspring-db-keycloak - DB_PORT: 3307 - DB_USER: 'keycloak' - DB_PASSWORD: 'keycloak' - PROXY_ADDRESS_FORWARDING: 'true' - KEYCLOAK_FRONTEND_URL: 'http://localhost:8080/auth' + KC_DB_URL: jdbc:postgresql://reacthookspring-db-keycloak:5432/keycloak + KC_DB: postgres + KC_DB_USERNAME: keycloak + KC_DB_PASSWORD: keycloak + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: admin + KC_HTTP_RELATIVE_PATH: /auth/ + command: start-dev --import-realm ports: # : < MySQL Port running inside container> - '8080:8080' diff --git a/reacthookspring/persistence/pom.xml b/reacthookspring/persistence/pom.xml index bd1f785..74eac3c 100644 --- a/reacthookspring/persistence/pom.xml +++ b/reacthookspring/persistence/pom.xml @@ -51,8 +51,12 @@ jackson-annotations - org.springdoc - springdoc-openapi-security + com.fasterxml.jackson.core + jackson-databind + + + org.springframework.data + spring-data-jpa diff --git a/reacthookspring/persistence/src/main/java/de/starwit/persistence/converter/ListToStringConverter.java b/reacthookspring/persistence/src/main/java/de/starwit/persistence/converter/ListToStringConverter.java index 41aab98..4501c7c 100644 --- a/reacthookspring/persistence/src/main/java/de/starwit/persistence/converter/ListToStringConverter.java +++ b/reacthookspring/persistence/src/main/java/de/starwit/persistence/converter/ListToStringConverter.java @@ -4,7 +4,7 @@ import java.util.Collections; import java.util.List; -import javax.persistence.AttributeConverter; +import jakarta.persistence.AttributeConverter; public class ListToStringConverter implements AttributeConverter, String> { @Override diff --git a/reacthookspring/persistence/src/main/java/de/starwit/persistence/entity/AbstractEntity.java b/reacthookspring/persistence/src/main/java/de/starwit/persistence/entity/AbstractEntity.java index 4af94a8..7c545a7 100755 --- a/reacthookspring/persistence/src/main/java/de/starwit/persistence/entity/AbstractEntity.java +++ b/reacthookspring/persistence/src/main/java/de/starwit/persistence/entity/AbstractEntity.java @@ -2,11 +2,11 @@ import java.io.Serializable; -import javax.persistence.Column; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.Column; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; @MappedSuperclass public abstract class AbstractEntity { diff --git a/reacthookspring/pom.xml b/reacthookspring/pom.xml index 21cfce6..cd7e1f8 100644 --- a/reacthookspring/pom.xml +++ b/reacthookspring/pom.xml @@ -4,7 +4,7 @@ org.springframework.boot spring-boot-starter-parent - 2.6.6 + 3.1.1 de.starwit @@ -16,10 +16,10 @@ UTF-8 UTF-8 - 2.6.6 - 5.3.20 + 3.1.1 + 6.0.1 2.17.2 - 1.6.7 + 2.1.0 2.7.4 @@ -38,13 +38,6 @@ spring-core ${spring-version} - - org.keycloak.bom - keycloak-adapter-bom - 15.0.2 - pom - import - org.springframework.boot spring-boot-starter @@ -140,13 +133,13 @@ ${spring-boot-version} - org.springdoc - springdoc-openapi-ui - ${openapi-version} + org.springframework.data + spring-data-jpa + ${spring-boot-version} org.springdoc - springdoc-openapi-security + springdoc-openapi-starter-webmvc-ui ${openapi-version} diff --git a/reacthookspring/rest/pom.xml b/reacthookspring/rest/pom.xml index 8edae42..cf90d9b 100644 --- a/reacthookspring/rest/pom.xml +++ b/reacthookspring/rest/pom.xml @@ -15,10 +15,6 @@ org.springframework.security spring-security-web - - org.keycloak - keycloak-spring-security-adapter - org.springframework.boot spring-boot-starter-validation @@ -37,7 +33,7 @@ org.springdoc - springdoc-openapi-ui + springdoc-openapi-starter-webmvc-ui org.springframework.boot diff --git a/reacthookspring/rest/src/main/java/de/starwit/rest/controller/UserController.java b/reacthookspring/rest/src/main/java/de/starwit/rest/controller/UserController.java index ffd8d66..6824664 100644 --- a/reacthookspring/rest/src/main/java/de/starwit/rest/controller/UserController.java +++ b/reacthookspring/rest/src/main/java/de/starwit/rest/controller/UserController.java @@ -2,9 +2,9 @@ import java.io.IOException; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/reacthookspring/rest/src/main/java/de/starwit/rest/exception/ControllerExceptionHandler.java b/reacthookspring/rest/src/main/java/de/starwit/rest/exception/ControllerExceptionHandler.java index 2540bc0..be56c6b 100644 --- a/reacthookspring/rest/src/main/java/de/starwit/rest/exception/ControllerExceptionHandler.java +++ b/reacthookspring/rest/src/main/java/de/starwit/rest/exception/ControllerExceptionHandler.java @@ -4,7 +4,7 @@ import java.util.HashMap; import java.util.Map; -import javax.persistence.EntityNotFoundException; +import jakarta.persistence.EntityNotFoundException; import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; @@ -16,6 +16,7 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; import org.springframework.orm.jpa.JpaSystemException; import org.springframework.security.access.AccessDeniedException; @@ -132,7 +133,7 @@ public ResponseEntity handleException(SQLIntegrityConstraintViolationExc @Override protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, - HttpHeaders headers, HttpStatus status, WebRequest request) { + HttpHeaders headers, HttpStatusCode status, WebRequest request) { Map errors = new HashMap<>(); ex.getBindingResult().getAllErrors().forEach((error) -> { diff --git a/reacthookspring/service/pom.xml b/reacthookspring/service/pom.xml index 7b75a12..7a70252 100644 --- a/reacthookspring/service/pom.xml +++ b/reacthookspring/service/pom.xml @@ -32,7 +32,7 @@ org.springdoc - springdoc-openapi-ui + springdoc-openapi-starter-webmvc-ui com.h2database diff --git a/reacthookspring/service/src/main/java/de/starwit/service/impl/ServiceInterface.java b/reacthookspring/service/src/main/java/de/starwit/service/impl/ServiceInterface.java index f8a0807..bcbd4f5 100644 --- a/reacthookspring/service/src/main/java/de/starwit/service/impl/ServiceInterface.java +++ b/reacthookspring/service/src/main/java/de/starwit/service/impl/ServiceInterface.java @@ -2,7 +2,7 @@ import java.util.List; -import javax.persistence.EntityNotFoundException; +import jakarta.persistence.EntityNotFoundException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/reacthookspring/webclient/app/.gitignore b/reacthookspring/webclient/app/.gitignore index 4d29575..d3ff5fc 100755 --- a/reacthookspring/webclient/app/.gitignore +++ b/reacthookspring/webclient/app/.gitignore @@ -21,3 +21,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +package-lock.json \ No newline at end of file From 81fda51ee44337a467921ece8925e5c573353073 Mon Sep 17 00:00:00 2001 From: Cedric Rische Date: Sat, 15 Jul 2023 23:56:20 +0200 Subject: [PATCH 10/22] Template modifications --- .../application/properties.ftl | 15 ++++-------- .../Entity-lowercaseIdentifier.ftl | 3 +-- generator-templates/persistence/Entity.ftl | 1 - .../postgres/application/properties.ftl | 15 ++++-------- .../deployment/env-docker-compose.ftl | 23 ++++++++----------- .../deployment/localenv-docker-compose.ftl | 21 ++++++++--------- .../postgres/pom-persistence.ftl | 4 ---- generator-templates/postgres/pom.ftl | 9 ++++---- .../deployment/localenv-docker-compose.yml | 4 ++-- reacthookspring/pom.xml | 1 - 10 files changed, 35 insertions(+), 61 deletions(-) diff --git a/generator-templates/application/properties.ftl b/generator-templates/application/properties.ftl index 73d8f32..5ed023b 100755 --- a/generator-templates/application/properties.ftl +++ b/generator-templates/application/properties.ftl @@ -30,16 +30,9 @@ spring.flyway.locations=classpath:db/migration spring.flyway.encoding=UTF-8 spring.flyway.placeholder-replacement=false -springdoc.swagger-ui.disable-swagger-default-url=true -springdoc.api-docs.path=/api-docs -springdoc.swagger-ui.path=/swagger-ui.html -springdoc.swagger-ui.csrf.enabled=true - #logging.level.org.springframework.security=DEBUG -keycloak.auth-server-url=http://localhost:8080/auth -keycloak.realm=${app.baseName?lower_case} -keycloak.resource=${app.baseName?lower_case} -keycloak.principal-attribute=preferred_username -keycloak.public-client=true -keycloak.enabled=true +spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8080/auth/realms/${app.baseName?lower_case} +spring.security.oauth2.client.registration.keycloak.client-id=${app.baseName?lower_case} +spring.security.oauth2.client.registration.keycloak.client-secret=${app.baseName?lower_case} +spring.security.oauth2.client.registration.keycloak.scope=openid diff --git a/generator-templates/persistence/Entity-lowercaseIdentifier.ftl b/generator-templates/persistence/Entity-lowercaseIdentifier.ftl index 2f79345..a65cfef 100644 --- a/generator-templates/persistence/Entity-lowercaseIdentifier.ftl +++ b/generator-templates/persistence/Entity-lowercaseIdentifier.ftl @@ -9,12 +9,11 @@ import de.${app.packageName?lower_case}.persistence.serializer.ZonedDateTimeSeri import de.${app.packageName?lower_case}.persistence.serializer.ZonedDateTimeDeserializer; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import javax.persistence.CascadeType; +import jakarta.persistence.CascadeType; /** * ${entity.name} Entity class */ -@XmlRootElement @Entity @Table(name = "${entity.name?lower_case}") public class ${entity.name}Entity extends AbstractEntity { diff --git a/generator-templates/persistence/Entity.ftl b/generator-templates/persistence/Entity.ftl index 0763be9..dab2a3b 100644 --- a/generator-templates/persistence/Entity.ftl +++ b/generator-templates/persistence/Entity.ftl @@ -14,7 +14,6 @@ import jakarta.persistence.CascadeType; /** * ${entity.name} Entity class */ -@XmlRootElement @Entity @Table(name = "${entity.name?upper_case}") public class ${entity.name}Entity extends AbstractEntity { diff --git a/generator-templates/postgres/application/properties.ftl b/generator-templates/postgres/application/properties.ftl index 26ef25d..1792b26 100755 --- a/generator-templates/postgres/application/properties.ftl +++ b/generator-templates/postgres/application/properties.ftl @@ -31,16 +31,9 @@ spring.flyway.locations=classpath:db/migration spring.flyway.encoding=UTF-8 spring.flyway.placeholder-replacement=false -springdoc.swagger-ui.disable-swagger-default-url=true -springdoc.api-docs.path=/api-docs -springdoc.swagger-ui.path=/swagger-ui.html -springdoc.swagger-ui.csrf.enabled=true - #logging.level.org.springframework.security=DEBUG -keycloak.auth-server-url=http://localhost:8080/auth -keycloak.realm=${app.baseName?lower_case} -keycloak.resource=${app.baseName?lower_case} -keycloak.principal-attribute=preferred_username -keycloak.public-client=true -keycloak.enabled=true +spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8080/auth/realms/${app.baseName?lower_case} +spring.security.oauth2.client.registration.keycloak.client-id=${app.baseName?lower_case} +spring.security.oauth2.client.registration.keycloak.client-secret=${app.baseName?lower_case} +spring.security.oauth2.client.registration.keycloak.scope=openid diff --git a/generator-templates/postgres/deployment/env-docker-compose.ftl b/generator-templates/postgres/deployment/env-docker-compose.ftl index 89b42ff..399299d 100644 --- a/generator-templates/postgres/deployment/env-docker-compose.ftl +++ b/generator-templates/postgres/deployment/env-docker-compose.ftl @@ -58,25 +58,22 @@ services: - backend ${app.baseName?lower_case}-keycloak: - image: jboss/keycloak + image: quay.io/keycloak/keycloak volumes: - - ./imports:/opt/jboss/keycloak/imports + - ./keycloak/imports:/opt/keycloak/data/import depends_on: ${app.baseName?lower_case}-db-keycloak: condition: service_healthy restart: on-failure environment: - KEYCLOAK_IMPORT: /opt/jboss/keycloak/imports/realm.json - KEYCLOAK_USER: ${r"${KEYCLOAK_USER}"} - KEYCLOAK_PASSWORD: ${r"${KEYCLOAK_PW}"} - DB_VENDOR: postgres - DB_ADDR: ${app.baseName?lower_case}-db-keycloak - DB_PORT: 5432 - DB_USER: 'keycloak' - DB_PASSWORD: ${r"${DB_PW_KEYCLOAK}"} - PROXY_ADDRESS_FORWARDING: 'true' - KEYCLOAK_FRONTEND_URL: 'https://${r"${DOMAIN}"}/auth' - networks: + KC_DB_URL: jdbc:postgresql://${app.baseName?lower_case}-db-keycloak:5432/keycloak + KC_DB: postgres + KC_DB_USERNAME: keycloak + KC_DB_PASSWORD: ${r"${DB_PW_KEYCLOAK}"} + KEYCLOAK_ADMIN: ${r"${KEYCLOAK_USER}"} + KEYCLOAK_ADMIN_PASSWORD: ${r"${KEYCLOAK_PW}"} + KC_HTTP_RELATIVE_PATH: /auth/ + networks: - backend volumes: diff --git a/generator-templates/postgres/deployment/localenv-docker-compose.ftl b/generator-templates/postgres/deployment/localenv-docker-compose.ftl index a5b4fc3..8bb5503 100644 --- a/generator-templates/postgres/deployment/localenv-docker-compose.ftl +++ b/generator-templates/postgres/deployment/localenv-docker-compose.ftl @@ -55,28 +55,27 @@ services: - backend ${app.baseName?lower_case}-keycloak: - image: jboss/keycloak + image: quay.io/keycloak/keycloak volumes: - - ./keycloak/imports:/opt/jboss/keycloak/imports - - ./keycloak/local-test-users.json:/opt/jboss/keycloak/standalone/configuration/keycloak-add-user.json + - ./keycloak/imports:/opt/keycloak/data/import depends_on: ${app.baseName?lower_case}-db-keycloak: condition: service_healthy restart: on-failure environment: - KEYCLOAK_IMPORT: /opt/jboss/keycloak/imports/realm.json - DB_VENDOR: postgres - DB_ADDR: ${app.baseName?lower_case}-db-keycloak - DB_PORT: 5432 - DB_USER: 'keycloak' - DB_PASSWORD: 'keycloak' - PROXY_ADDRESS_FORWARDING: 'true' - KEYCLOAK_FRONTEND_URL: 'http://localhost:8080/auth' + KC_DB_URL: jdbc:postgresql://${app.baseName?lower_case}-db-keycloak:5432/keycloak + KC_DB: postgres + KC_DB_USERNAME: keycloak + KC_DB_PASSWORD: keycloak + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: admin + KC_HTTP_RELATIVE_PATH: /auth/ ports: - '8080:8080' networks: - backend + networks: backend: diff --git a/generator-templates/postgres/pom-persistence.ftl b/generator-templates/postgres/pom-persistence.ftl index 171f4be..82945c0 100644 --- a/generator-templates/postgres/pom-persistence.ftl +++ b/generator-templates/postgres/pom-persistence.ftl @@ -48,9 +48,5 @@ com.fasterxml.jackson.core jackson-annotations - - org.springdoc - springdoc-openapi-security - diff --git a/generator-templates/postgres/pom.ftl b/generator-templates/postgres/pom.ftl index 4866f82..418c134 100644 --- a/generator-templates/postgres/pom.ftl +++ b/generator-templates/postgres/pom.ftl @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 2.6.6 + 3.1.1 de.${app.packageName?lower_case} @@ -18,10 +18,9 @@ UTF-8 UTF-8 - 2.6.6 - 5.3.27 - 2.17.2 - 1.6.7 + 3.1.1 + 6.0.1 + 2.1.0 2.7.4 diff --git a/reacthookspring/deployment/localenv-docker-compose.yml b/reacthookspring/deployment/localenv-docker-compose.yml index 8406fd3..4a94cb1 100644 --- a/reacthookspring/deployment/localenv-docker-compose.yml +++ b/reacthookspring/deployment/localenv-docker-compose.yml @@ -52,8 +52,8 @@ services: condition: service_healthy restart: on-failure environment: - KC_DB_URL: jdbc:postgresql://reacthookspring-db-keycloak:5432/keycloak - KC_DB: postgres + KC_DB_URL: jdbc:mariadb://reacthookspring-db-keycloak:3307/keycloak + KC_DB: mariadb KC_DB_USERNAME: keycloak KC_DB_PASSWORD: keycloak KEYCLOAK_ADMIN: admin diff --git a/reacthookspring/pom.xml b/reacthookspring/pom.xml index f30ad61..ac3ab51 100644 --- a/reacthookspring/pom.xml +++ b/reacthookspring/pom.xml @@ -18,7 +18,6 @@ UTF-8 3.1.1 6.0.1 - 2.17.2 2.1.0 2.7.4 From de1c36475f04f3f9538dad1e2987a58618d06326 Mon Sep 17 00:00:00 2001 From: Cedric Rische Date: Wed, 19 Jul 2023 20:54:17 +0200 Subject: [PATCH 11/22] Added fixes from cameramanagement --- .../application/properties.ftl | 1 + .../postgres/application/properties.ftl | 1 + generator-templates/rest/Controller.ftl | 2 +- generator-templates/webclient/EntityRest.ftl | 2 +- reacthookspring/application/pom.xml | 4 + .../application/config/CorsConfig.java | 21 ----- .../application/config/SecurityConfig.java | 76 ++++++++++++++++++- reacthookspring/persistence/pom.xml | 4 - reacthookspring/pom.xml | 5 ++ 9 files changed, 85 insertions(+), 31 deletions(-) delete mode 100644 reacthookspring/application/src/main/java/de/starwit/application/config/CorsConfig.java diff --git a/generator-templates/application/properties.ftl b/generator-templates/application/properties.ftl index 5ed023b..f8392e7 100755 --- a/generator-templates/application/properties.ftl +++ b/generator-templates/application/properties.ftl @@ -20,6 +20,7 @@ spring.jpa.hibernate.naming.physical-strategy=de.${app.packageName?lower_case}.p #spring.jpa.hibernate.ddl-auto=create spring.datasource.username=${app.baseName?lower_case} spring.datasource.password=${app.baseName?lower_case} +spring.data.rest.detection-strategy=annotated # Flyway spring.flyway.user=${r"${spring.datasource.username}"} diff --git a/generator-templates/postgres/application/properties.ftl b/generator-templates/postgres/application/properties.ftl index 1792b26..e5e4a8e 100755 --- a/generator-templates/postgres/application/properties.ftl +++ b/generator-templates/postgres/application/properties.ftl @@ -20,6 +20,7 @@ spring.jpa.hibernate.naming.physical-strategy=de.${app.packageName?lower_case}.p spring.datasource.username=${app.baseName?lower_case} spring.datasource.password=${app.baseName?lower_case} spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +spring.data.rest.detection-strategy=annotated #spring.jpa.hibernate.ddl-auto=create # Flyway diff --git a/generator-templates/rest/Controller.ftl b/generator-templates/rest/Controller.ftl index 2615567..7ee8ad4 100755 --- a/generator-templates/rest/Controller.ftl +++ b/generator-templates/rest/Controller.ftl @@ -31,7 +31,7 @@ import io.swagger.v3.oas.annotations.Operation; * Have a look at the RequestMapping!!!!!! */ @RestController -@RequestMapping("${r"${rest.base-path}"}/${entity.name?lower_case}") +@RequestMapping(path = "${r"${rest.base-path}"}/${entity.name?lower_case}") public class ${entity.name}Controller { static final Logger LOG = LoggerFactory.getLogger(${entity.name}Controller.class); diff --git a/generator-templates/webclient/EntityRest.ftl b/generator-templates/webclient/EntityRest.ftl index afe12e7..cd710cc 100755 --- a/generator-templates/webclient/EntityRest.ftl +++ b/generator-templates/webclient/EntityRest.ftl @@ -21,7 +21,7 @@ class ${entity.name}Rest extends CrudRest { findAllWithout${relation.relationshipName?cap_first}(selected) { if (isNaN(selected)) { - return axios.get(this.baseUrl + "/find-without-${relation.relationshipName}/"); + return axios.get(this.baseUrl + "/find-without-${relation.relationshipName}"); } else { return axios.get(this.baseUrl + "/find-without-other-${relation.relationshipName}/" + selected); } diff --git a/reacthookspring/application/pom.xml b/reacthookspring/application/pom.xml index d010b84..ff6f3df 100644 --- a/reacthookspring/application/pom.xml +++ b/reacthookspring/application/pom.xml @@ -35,6 +35,10 @@ org.springdoc springdoc-openapi-starter-webmvc-ui + + org.springdoc + springdoc-openapi-starter-common + org.projectlombok lombok diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/CorsConfig.java b/reacthookspring/application/src/main/java/de/starwit/application/config/CorsConfig.java deleted file mode 100644 index 50337fc..0000000 --- a/reacthookspring/application/src/main/java/de/starwit/application/config/CorsConfig.java +++ /dev/null @@ -1,21 +0,0 @@ -package de.starwit.application.config; - -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; -import org.springframework.web.servlet.config.annotation.CorsRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Profile("!dev") -@Configuration -public class CorsConfig implements WebMvcConfigurer { - - @Override - public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/**") - .allowedMethods("*") - .allowedOrigins("http://localhost:8080", "http://localhost:8081") - .allowedHeaders("*") - .allowCredentials(false); - } - -} \ No newline at end of file diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/SecurityConfig.java b/reacthookspring/application/src/main/java/de/starwit/application/config/SecurityConfig.java index dbe3c2e..d4e7dfc 100644 --- a/reacthookspring/application/src/main/java/de/starwit/application/config/SecurityConfig.java +++ b/reacthookspring/application/src/main/java/de/starwit/application/config/SecurityConfig.java @@ -1,5 +1,9 @@ package de.starwit.application.config; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -8,16 +12,22 @@ import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.web.csrf.*; import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.filter.OncePerRequestFilter; +import java.io.IOException; import java.util.*; +import java.util.function.Supplier; @Configuration @EnableWebSecurity @@ -43,8 +53,18 @@ public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - // TODO Not great, has to be fixed - .csrf(AbstractHttpConfigurer::disable) + .cors(cors -> cors.configurationSource(request -> { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(List.of("http://localhost:8081")); + configuration.setAllowedHeaders(List.of("*")); + configuration.setAllowedMethods(List.of("*")); + return configuration; + })) + .csrf(httpSecurityCsrfConfigurer -> httpSecurityCsrfConfigurer + .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()) + ) + .addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class) .authorizeHttpRequests(authorize -> authorize .requestMatchers(AUTH_WHITELIST).permitAll() .requestMatchers("/**").hasAnyRole("admin", "user", "reader") @@ -94,4 +114,52 @@ public Collection mapAuthorities(Collection csrfToken) { + /* + * Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of + * the CsrfToken when it is rendered in the response body. + */ + this.delegate.handle(request, response, csrfToken); + } + + @Override + public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) { + /* + * If the request contains a request header, use CsrfTokenRequestAttributeHandler + * to resolve the CsrfToken. This applies when a single-page application includes + * the header value automatically, which was obtained via a cookie containing the + * raw CsrfToken. + */ + if (StringUtils.hasText(request.getHeader(csrfToken.getHeaderName()))) { + return super.resolveCsrfTokenValue(request, csrfToken); + } + /* + * In all other cases (e.g. if the request contains a request parameter), use + * XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies + * when a server-side rendered form includes the _csrf request parameter as a + * hidden input. + */ + return this.delegate.resolveCsrfTokenValue(request, csrfToken); + } +} + +final class CsrfCookieFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + CsrfToken csrfToken = (CsrfToken) request.getAttribute("_csrf"); + // Render the token value to a cookie by causing the deferred token to be loaded + csrfToken.getToken(); + + filterChain.doFilter(request, response); + } +} diff --git a/reacthookspring/persistence/pom.xml b/reacthookspring/persistence/pom.xml index 37356bd..dd3ff78 100644 --- a/reacthookspring/persistence/pom.xml +++ b/reacthookspring/persistence/pom.xml @@ -56,10 +56,6 @@ com.fasterxml.jackson.core jackson-databind - - org.springframework.data - spring-data-jpa - \ No newline at end of file diff --git a/reacthookspring/pom.xml b/reacthookspring/pom.xml index ac3ab51..7dc64cb 100644 --- a/reacthookspring/pom.xml +++ b/reacthookspring/pom.xml @@ -141,6 +141,11 @@ springdoc-openapi-starter-webmvc-ui ${openapi-version} + + org.springdoc + springdoc-openapi-starter-common + ${openapi-version} + com.h2database h2 From 72edbbc794cbcec72a981c361b45debfaa0ec5ac Mon Sep 17 00:00:00 2001 From: Cedric Rische Date: Wed, 2 Aug 2023 19:54:09 +0200 Subject: [PATCH 12/22] Added new logout feature Disable security (pt 1) --- reacthookspring/README.MD | 2 +- .../config/DevelopmentSecurityConfig.java | 27 +++++++++++++++++++ .../application/config/SecurityConfig.java | 15 +++++++++++ .../src/main/resources/application.properties | 1 + .../navigation/appHeader/AppHeader.jsx | 2 +- 5 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 reacthookspring/application/src/main/java/de/starwit/application/config/DevelopmentSecurityConfig.java diff --git a/reacthookspring/README.MD b/reacthookspring/README.MD index ad11541..cdcabf7 100755 --- a/reacthookspring/README.MD +++ b/reacthookspring/README.MD @@ -1,6 +1,6 @@ ### Prerequisites -* Java JDK 14 or later +* Java JDK 17 or later * Maven 3 * NodeJs (16.9.1) and NPM (8.3.2) - [NodeJS Install](https://nodejs.org/en/download/package-manager/) * mariaDB 10.6 (available for development via docker-compose scripts) diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/DevelopmentSecurityConfig.java b/reacthookspring/application/src/main/java/de/starwit/application/config/DevelopmentSecurityConfig.java new file mode 100644 index 0000000..5c75ce1 --- /dev/null +++ b/reacthookspring/application/src/main/java/de/starwit/application/config/DevelopmentSecurityConfig.java @@ -0,0 +1,27 @@ +package de.starwit.application.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.SecurityFilterChain; + + +@Profile("dev") +@Configuration +@EnableWebSecurity +public class DevelopmentSecurityConfig { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(authorize -> authorize + .requestMatchers("/**").permitAll() + .anyRequest().authenticated() + ) + .csrf(AbstractHttpConfigurer::disable); + + return http.build(); + } +} \ No newline at end of file diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/SecurityConfig.java b/reacthookspring/application/src/main/java/de/starwit/application/config/SecurityConfig.java index d4e7dfc..031aee1 100644 --- a/reacthookspring/application/src/main/java/de/starwit/application/config/SecurityConfig.java +++ b/reacthookspring/application/src/main/java/de/starwit/application/config/SecurityConfig.java @@ -29,12 +29,27 @@ import java.util.*; import java.util.function.Supplier; +@Profile("!dev") @Configuration @EnableWebSecurity public class SecurityConfig { static final Logger LOG = LoggerFactory.getLogger(SecurityConfig.class); + @Autowired + private ClientRegistrationRepository clientRegistrationRepository; + + LogoutSuccessHandler oidcLogoutSuccessHandler() { + OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler = + new OidcClientInitiatedLogoutSuccessHandler(this.clientRegistrationRepository); + + // Sets the location that the End-User's User Agent will be redirected to + // after the logout has been performed at the Provider + // oidcLogoutSuccessHandler.setPostLogoutRedirectUri(contextPath+"/"); + + return oidcLogoutSuccessHandler; + } + private static final String[] AUTH_WHITELIST = { // -- Swagger UI v2 "/v2/api-docs", diff --git a/reacthookspring/application/src/main/resources/application.properties b/reacthookspring/application/src/main/resources/application.properties index a23d34b..ab206b7 100644 --- a/reacthookspring/application/src/main/resources/application.properties +++ b/reacthookspring/application/src/main/resources/application.properties @@ -19,6 +19,7 @@ spring.datasource.url=jdbc:mariadb://localhost:3306/reacthookspring?useLegacyDat spring.jpa.hibernate.naming.physical-strategy=de.starwit.persistence.config.DatabasePhysicalNamingStrategy spring.datasource.username=reacthookspring spring.datasource.password=reacthookspring +spring.data.rest.detection-strategy=annotated # Flyway spring.flyway.user=${spring.datasource.username} diff --git a/reacthookspring/webclient/app/src/app/commons/navigation/appHeader/AppHeader.jsx b/reacthookspring/webclient/app/src/app/commons/navigation/appHeader/AppHeader.jsx index 056383e..85b16ee 100644 --- a/reacthookspring/webclient/app/src/app/commons/navigation/appHeader/AppHeader.jsx +++ b/reacthookspring/webclient/app/src/app/commons/navigation/appHeader/AppHeader.jsx @@ -26,7 +26,7 @@ function AppHeader(props) { onClick={() => history.push(item.link)}>{t(item.title)} ))} history.push("/logout")}> + onClick={() => window.location.href = window.location.origin + window.location.pathname + "logout"}>
From f6cae1fe5325cfdd601084d2ca7bf160b0b387c4 Mon Sep 17 00:00:00 2001 From: Cedric Rische Date: Wed, 2 Aug 2023 20:22:39 +0200 Subject: [PATCH 13/22] Added Disable security --- .../application/config/DevelopmentSecurityConfig.java | 5 +---- .../de/starwit/application/config/SecurityConfig.java | 8 +++++++- .../src/main/resources/application.properties | 9 +++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/DevelopmentSecurityConfig.java b/reacthookspring/application/src/main/java/de/starwit/application/config/DevelopmentSecurityConfig.java index 5c75ce1..5ce22fb 100644 --- a/reacthookspring/application/src/main/java/de/starwit/application/config/DevelopmentSecurityConfig.java +++ b/reacthookspring/application/src/main/java/de/starwit/application/config/DevelopmentSecurityConfig.java @@ -16,10 +16,7 @@ public class DevelopmentSecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/**").permitAll() - .anyRequest().authenticated() - ) + .httpBasic(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable); return http.build(); diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/SecurityConfig.java b/reacthookspring/application/src/main/java/de/starwit/application/config/SecurityConfig.java index 031aee1..dcb492d 100644 --- a/reacthookspring/application/src/main/java/de/starwit/application/config/SecurityConfig.java +++ b/reacthookspring/application/src/main/java/de/starwit/application/config/SecurityConfig.java @@ -7,17 +7,22 @@ import lombok.RequiredArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.csrf.*; import org.springframework.stereotype.Component; @@ -29,6 +34,7 @@ import java.util.*; import java.util.function.Supplier; + @Profile("!dev") @Configuration @EnableWebSecurity @@ -131,9 +137,9 @@ public Collection mapAuthorities(Collection Date: Wed, 2 Aug 2023 20:45:12 +0200 Subject: [PATCH 14/22] Fixed logout --- .../java/de/starwit/application/config/SecurityConfig.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/SecurityConfig.java b/reacthookspring/application/src/main/java/de/starwit/application/config/SecurityConfig.java index dcb492d..8037238 100644 --- a/reacthookspring/application/src/main/java/de/starwit/application/config/SecurityConfig.java +++ b/reacthookspring/application/src/main/java/de/starwit/application/config/SecurityConfig.java @@ -25,6 +25,7 @@ import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.csrf.*; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.cors.CorsConfiguration; @@ -92,6 +93,10 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers("/", "/login/**", "/oauth2/**").permitAll() .anyRequest().authenticated() ) + .logout((logout) -> logout + .logoutSuccessHandler(oidcLogoutSuccessHandler()) + .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) + ) // Maybe https://stackoverflow.com/questions/74939220/classnotfoundexception-org-springframework-security-oauth2-server-resource-web .oauth2Login(Customizer.withDefaults()); return http.build(); From 7cb4bd2841d66a969cc63a420ec3f98cd28155c9 Mon Sep 17 00:00:00 2001 From: Cedric Rische Date: Wed, 2 Aug 2023 20:57:39 +0200 Subject: [PATCH 15/22] Added csrf for springdoc --- generator-templates/application/properties.ftl | 9 +++++++-- .../src/main/resources/application.properties | 3 +++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/generator-templates/application/properties.ftl b/generator-templates/application/properties.ftl index f8392e7..16a0346 100755 --- a/generator-templates/application/properties.ftl +++ b/generator-templates/application/properties.ftl @@ -31,9 +31,14 @@ spring.flyway.locations=classpath:db/migration spring.flyway.encoding=UTF-8 spring.flyway.placeholder-replacement=false -#logging.level.org.springframework.security=DEBUG - +# Authentication spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8080/auth/realms/${app.baseName?lower_case} spring.security.oauth2.client.registration.keycloak.client-id=${app.baseName?lower_case} spring.security.oauth2.client.registration.keycloak.client-secret=${app.baseName?lower_case} spring.security.oauth2.client.registration.keycloak.scope=openid + +# OpenApi +springdoc.swagger-ui.csrf.enabled=true + +# logging.level.org.springframework.security=DEBUG +# logging.level.org.springframework.web=DEBUG \ No newline at end of file diff --git a/reacthookspring/application/src/main/resources/application.properties b/reacthookspring/application/src/main/resources/application.properties index 93891f3..0ee17c0 100644 --- a/reacthookspring/application/src/main/resources/application.properties +++ b/reacthookspring/application/src/main/resources/application.properties @@ -36,5 +36,8 @@ spring.security.oauth2.client.registration.keycloak.client-id=reacthookspring spring.security.oauth2.client.registration.keycloak.client-secret=reacthookspring spring.security.oauth2.client.registration.keycloak.scope=openid +# OpenApi +springdoc.swagger-ui.csrf.enabled=true + # logging.level.org.springframework.security=DEBUG # logging.level.org.springframework.web=DEBUG From 20c2e484d3908d117c5d4fe394f81ee85af7e609 Mon Sep 17 00:00:00 2001 From: Cedric Rische Date: Wed, 2 Aug 2023 21:08:27 +0200 Subject: [PATCH 16/22] Removed Swagger ui public access Restricted HTTP methods --- .../application/config/SecurityConfig.java | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/reacthookspring/application/src/main/java/de/starwit/application/config/SecurityConfig.java b/reacthookspring/application/src/main/java/de/starwit/application/config/SecurityConfig.java index 8037238..843ae73 100644 --- a/reacthookspring/application/src/main/java/de/starwit/application/config/SecurityConfig.java +++ b/reacthookspring/application/src/main/java/de/starwit/application/config/SecurityConfig.java @@ -57,28 +57,13 @@ LogoutSuccessHandler oidcLogoutSuccessHandler() { return oidcLogoutSuccessHandler; } - private static final String[] AUTH_WHITELIST = { - // -- Swagger UI v2 - "/v2/api-docs", - "/swagger-resources", - "/swagger-resources/**", - "/configuration/ui", - "/configuration/security", - "/swagger-ui.html", - "/webjars/**", - // -- Swagger UI v3 (OpenAPI) - "/v3/api-docs/**", - "/swagger-ui/**" - // other public endpoints of your API may be appended to this array - }; - @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .cors(cors -> cors.configurationSource(request -> { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(List.of("http://localhost:8081")); - configuration.setAllowedHeaders(List.of("*")); + configuration.setAllowedHeaders(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")); configuration.setAllowedMethods(List.of("*")); return configuration; })) @@ -88,9 +73,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { ) .addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class) .authorizeHttpRequests(authorize -> authorize - .requestMatchers(AUTH_WHITELIST).permitAll() .requestMatchers("/**").hasAnyRole("admin", "user", "reader") - .requestMatchers("/", "/login/**", "/oauth2/**").permitAll() .anyRequest().authenticated() ) .logout((logout) -> logout From 09cc50a2804c523f5510a1ea29a6743a2cf5d4f2 Mon Sep 17 00:00:00 2001 From: Cedric Rische Date: Wed, 2 Aug 2023 21:32:03 +0200 Subject: [PATCH 17/22] Added api postgres config removed old configs --- template-config-api-postgres.json | 175 ++++++++++++++++++++++++++++++ template-config-backend.json | 79 -------------- template-config-frontend.json | 61 ----------- 3 files changed, 175 insertions(+), 140 deletions(-) create mode 100755 template-config-api-postgres.json delete mode 100755 template-config-backend.json delete mode 100755 template-config-frontend.json diff --git a/template-config-api-postgres.json b/template-config-api-postgres.json new file mode 100755 index 0000000..3fb5dd9 --- /dev/null +++ b/template-config-api-postgres.json @@ -0,0 +1,175 @@ +{ + "templateName": "reacthookspring", + "packagePlaceholder": "starwit", + "imageUrl": "https://raw.githubusercontent.com/starwit/reacthook-spring-template/main/screenshot.png", + "templateFiles": [ + { + "fileName": "pom.xml", + "templatePath": "postgres/pom.ftl", + "targetPath": "/", + "category": "BUILD" + }, + { + "fileName": "pom.xml", + "templatePath": "postgres/pom-persistence.ftl", + "targetPath": "persistence/", + "category": "BUILD" + }, + { + "fileName": "README.MD", + "templatePath": "postgres/readme.ftl", + "targetPath": "/", + "category": "DOC" + }, + { + "fileName": "localenv-docker-compose.yml", + "templatePath": "postgres/deployment/localenv-docker-compose.ftl", + "targetPath": "deployment/", + "category": "DEPLOYMENT" + }, + { + "fileName": "postgreslocal-docker-compose.yml", + "templatePath": "postgres/deployment/postgreslocal-docker-compose.ftl", + "targetPath": "deployment/", + "category": "DEPLOYMENT" + }, + { + "fileName": "app-docker-compose.yml", + "templatePath": "postgres/deployment/app-docker-compose.ftl", + "targetPath": "deployment/https/", + "category": "DEPLOYMENT" + }, + { + "fileName": "env-docker-compose.yml", + "templatePath": "postgres/deployment/env-docker-compose.ftl", + "targetPath": "deployment/https/", + "category": "DEPLOYMENT" + }, + { + "fileName": "application.properties", + "templatePath": "postgres/application/properties.ftl", + "targetPath": "application/src/main/resources/", + "category": "APP" + }, + { + "fileName": "application.properties", + "templatePath": "postgres/application/propertiestest.ftl", + "targetPath": "rest/src/test/resources/", + "category": "APP" + }, + { + "fileName": "application.properties", + "templatePath": "postgres/application/propertiestestpersistence.ftl", + "targetPath": "persistence/src/test/resources/", + "category": "APP" + }, + { + "fileName": "${entity.name}Entity.java", + "templatePath": "persistence/Entity-lowercaseIdentifier.ftl", + "targetPath": "persistence/src/main/java/de/${app.packageName?lower_case}/persistence/entity/", + "category": "ENTITY" + }, + { + "fileName": "${enumDef.name}.java", + "templatePath": "persistence/Enum.ftl", + "targetPath": "persistence/src/main/java/de/${app.packageName?lower_case}/persistence/entity/", + "category": "ENTITY" + }, + { + "fileName": "${entity.name}Repository.java", + "templatePath": "persistence/EntityRepository.ftl", + "targetPath": "persistence/src/main/java/de/${app.packageName?lower_case}/persistence/repository/", + "category": "ENTITY" + }, + { + "fileName": "${entity.name}RepositoryTest.java", + "templatePath": "persistence/EntityRepositoryTest.ftl", + "targetPath": "persistence/src/test/java/de/${app.packageName?lower_case}/persistence/repository/", + "category": "ENTITY" + }, + { + "fileName": "V1_0__init.sql", + "templatePath": "postgres/flyway/V1_0__init-postgres.ftl", + "targetPath": "persistence/src/main/resources/db/migration/", + "category": "ENTITY" + }, + { + "fileName": "V1_0__init.sql", + "templatePath": "postgres/flyway/V1_0__init-postgres.ftl", + "targetPath": "persistence/src/test/resources/db/test/", + "category": "ENTITY" + }, + { + "fileName": "${entity.name}Service.java", + "templatePath": "service/Service.ftl", + "targetPath": "service/src/main/java/de/${app.packageName?lower_case}/service/impl/", + "category": "SERVICE" + }, + { + "fileName": "${entity.name}Controller.java", + "templatePath": "rest/Controller.ftl", + "targetPath": "rest/src/main/java/de/${app.packageName?lower_case}/rest/controller/", + "category": "REST" + }, + { + "fileName": "${entity.name}ControllerAcceptanceTest.java", + "templatePath": "rest/ControllerAcceptanceTest.ftl", + "targetPath": "rest/src/test/java/de/${app.packageName?lower_case}/rest/acceptance/", + "category": "REST" + }, + { + "fileName": "${entity.name}ControllerIntegrationTest.java", + "templatePath": "rest/ControllerIntegrationTest.ftl", + "targetPath": "rest/src/test/java/de/${app.packageName?lower_case}/rest/integration/", + "category": "REST" + }, + { + "fileName": "AppConfig.js", + "templatePath": "webclient/AppConfig.ftl", + "targetPath": "webclient/app/src/app/", + "category": "FRONTEND" + }, + { + "fileName": "translation-en-EN.js", + "templatePath": "webclient/translationEnEN.ftl", + "targetPath": "webclient/app/src/localization/", + "category": "FRONTEND" + }, + { + "fileName": "${entity.name}Rest.js", + "templatePath": "webclient/EntityRest.ftl", + "targetPath": "webclient/app/src/app/services/", + "category": "FRONTEND" + }, + { + "fileName": "${entity.name}Modifier.js", + "templatePath": "webclient/EntityModifier.ftl", + "targetPath": "webclient/app/src/app/modifiers/", + "category": "FRONTEND" + }, + { + "fileName": "MainContentRouter.jsx", + "templatePath": "webclient/MainContentRouter.ftl", + "targetPath": "webclient/app/src/app/", + "category": "FRONTEND" + }, + { + "fileName": "${entity.name}Main.jsx", + "templatePath": "webclient/EntityMain.ftl", + "targetPath": "webclient/app/src/app/features/${entity.name?uncap_first}/", + "category": "FRONTEND" + }, + { + "fileName": "${entity.name}Detail.jsx", + "templatePath": "webclient/EntityDetail.ftl", + "targetPath": "webclient/app/src/app/features/${entity.name?uncap_first}/", + "category": "FRONTEND" + }, + { + "fileName": "${entity.name}Overview.jsx", + "templatePath": "webclient/EntityOverview.ftl", + "targetPath": "webclient/app/src/app/features/${entity.name?uncap_first}/", + "category": "FRONTEND" + } + ] +} \ No newline at end of file diff --git a/template-config-backend.json b/template-config-backend.json deleted file mode 100755 index 189d98c..0000000 --- a/template-config-backend.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "templateName": "reacthookspring-backend", - "packagePlaceholder": "starwit", - "imageUrl": "https://raw.githubusercontent.com/starwit/reacthook-spring-template/main/screenshot.png", - "templateFiles": [ - { - "fileName": "${entity.name}Entity.java", - "templatePath": "persistence/Entity.ftl", - "targetPath": "persistence/src/main/java/de/${app.packageName?lower_case}/persistence/entity/", - "category": "ENTITY" - }, - { - "fileName": "${enumDef.name}.java", - "templatePath": "persistence/Enum.ftl", - "targetPath": "persistence/src/main/java/de/${app.packageName?lower_case}/persistence/entity/", - "category": "ENTITY" - }, - { - "fileName": "application.properties", - "templatePath": "application/properties.ftl", - "targetPath": "application/src/main/resources/", - "category": "ENTITY" - }, - { - "fileName": "application.properties", - "templatePath": "application/propertiestest.ftl", - "targetPath": "rest/src/test/resources/", - "category": "REST" - }, - { - "fileName": "${entity.name}Repository.java", - "templatePath": "persistence/EntityRepository.ftl", - "targetPath": "persistence/src/main/java/de/${app.packageName?lower_case}/persistence/repository/", - "category": "ENTITY" - }, - { - "fileName": "${entity.name}RepositoryTest.java", - "templatePath": "persistence/EntityRepositoryTest.ftl", - "targetPath": "persistence/src/test/java/de/${app.packageName?lower_case}/persistence/repository/", - "category": "ENTITY" - }, - { - "fileName": "V1_0__init.sql", - "templatePath": "flyway/V1_0__init.ftl", - "targetPath": "persistence/src/main/resources/db/migration/", - "category": "ENTITY" - }, - { - "fileName": "V1_0__init.sql", - "templatePath": "flyway/V1_0__init.ftl", - "targetPath": "persistence/src/test/resources/db/test/", - "category": "ENTITY" - }, - { - "fileName": "${entity.name}Service.java", - "templatePath": "service/Service.ftl", - "targetPath": "service/src/main/java/de/${app.packageName?lower_case}/service/impl/", - "category": "SERVICE" - }, - { - "fileName": "${entity.name}Controller.java", - "templatePath": "rest/Controller.ftl", - "targetPath": "rest/src/main/java/de/${app.packageName?lower_case}/rest/controller/", - "category": "REST" - }, - { - "fileName": "${entity.name}ControllerAcceptanceTest.java", - "templatePath": "rest/ControllerAcceptanceTest.ftl", - "targetPath": "rest/src/test/java/de/${app.packageName?lower_case}/rest/acceptance/", - "category": "REST" - }, - { - "fileName": "${entity.name}ControllerIntegrationTest.java", - "templatePath": "rest/ControllerIntegrationTest.ftl", - "targetPath": "rest/src/test/java/de/${app.packageName?lower_case}/rest/integration/", - "category": "REST" - } - ] -} diff --git a/template-config-frontend.json b/template-config-frontend.json deleted file mode 100755 index 4976789..0000000 --- a/template-config-frontend.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "templateName": "reacthookspring-frontend", - "packagePlaceholder": "starwit", - "imageUrl": "https://raw.githubusercontent.com/starwit/reacthook-spring-template/main/screenshot.png", - "templateFiles": [ - { - "fileName": "AppConfig.js", - "templatePath": "webclient/AppConfig.ftl", - "targetPath": "src/app/", - "category": "FRONTEND" - }, - { - "fileName": "translation-en-EN.js", - "templatePath": "webclient/translationEnEN.ftl", - "targetPath": "src/localization/", - "category": "FRONTEND" - }, - { - "fileName": "${entity.name}Rest.js", - "templatePath": "webclient/EntityRestStandalone.ftl", - "targetPath": "src/app/services/", - "category": "FRONTEND" - }, - { - "fileName": "${entity.name}Modifier.js", - "templatePath": "webclient/EntityModifier.ftl", - "targetPath": "src/app/modifiers/", - "category": "FRONTEND" - }, - { - "fileName": "MainContentRouter.jsx", - "templatePath": "webclient/MainContentRouter.ftl", - "targetPath": "src/app/", - "category": "FRONTEND" - }, - { - "fileName": "${entity.name}Main.jsx", - "templatePath": "webclient/EntityMain.ftl", - "targetPath": "src/app/features/${entity.name?uncap_first}/", - "category": "FRONTEND" - }, - { - "fileName": "${entity.name}Detail.jsx", - "templatePath": "webclient/EntityDetail.ftl", - "targetPath": "src/app/features/${entity.name?uncap_first}/", - "category": "FRONTEND" - }, - { - "fileName": "${entity.name}Overview.jsx", - "templatePath": "webclient/EntityOverview.ftl", - "targetPath": "src/app/features/${entity.name?uncap_first}/", - "category": "FRONTEND" - }, - { - "fileName": ".public-env", - "templatePath": "webclient/.public-env.ftl", - "targetPath": ".public-env", - "category": "FRONTEND" - } - ] -} From 2b3c80de3dc56d7208c4475cb1fab663f506b36b Mon Sep 17 00:00:00 2001 From: Cedric Rische Date: Wed, 2 Aug 2023 21:55:14 +0200 Subject: [PATCH 18/22] Added api postgres added api template config --- .../api-postgres/application/properties.ftl | 40 ++++ .../application/propertiestest.ftl | 27 +++ .../application/propertiestestpersistence.ftl | 6 + .../deployment/app-docker-compose.ftl | 47 ++++ .../deployment/env-docker-compose.ftl | 86 +++++++ .../deployment/localenv-docker-compose.ftl | 86 +++++++ .../postgreslocal-docker-compose.ftl | 44 ++++ .../flyway/V1_0__init-postgres.ftl | 94 ++++++++ .../api-postgres/pom-persistence.ftl | 52 +++++ generator-templates/api-postgres/pom.ftl | 215 ++++++++++++++++++ generator-templates/api-postgres/readme.ftl | 94 ++++++++ template-config-api-postgres.json | 24 +- 12 files changed, 803 insertions(+), 12 deletions(-) create mode 100755 generator-templates/api-postgres/application/properties.ftl create mode 100644 generator-templates/api-postgres/application/propertiestest.ftl create mode 100644 generator-templates/api-postgres/application/propertiestestpersistence.ftl create mode 100644 generator-templates/api-postgres/deployment/app-docker-compose.ftl create mode 100644 generator-templates/api-postgres/deployment/env-docker-compose.ftl create mode 100644 generator-templates/api-postgres/deployment/localenv-docker-compose.ftl create mode 100644 generator-templates/api-postgres/deployment/postgreslocal-docker-compose.ftl create mode 100755 generator-templates/api-postgres/flyway/V1_0__init-postgres.ftl create mode 100644 generator-templates/api-postgres/pom-persistence.ftl create mode 100644 generator-templates/api-postgres/pom.ftl create mode 100755 generator-templates/api-postgres/readme.ftl diff --git a/generator-templates/api-postgres/application/properties.ftl b/generator-templates/api-postgres/application/properties.ftl new file mode 100755 index 0000000..e5e4a8e --- /dev/null +++ b/generator-templates/api-postgres/application/properties.ftl @@ -0,0 +1,40 @@ +spring.profiles.active=@spring.profiles.active@ +spring.banner.location=classpath:banner.txt +server.servlet.context-path=/${app.baseName?lower_case} +rest.base-path=/api +server.port=8081 + +# actuator +management.endpoints.web.base-path=/monitoring +management.endpoint.health.show-details=always +management.endpoints.web.exposure.include=* + +# show full git properties +management.info.git.mode=full + +# Postgres +spring.datasource.hikari.connection-timeout=10000 +#spring.datasource.driver-class-name=org.postgresql.Driver +spring.datasource.url=jdbc:postgresql://localhost:5433/${app.baseName?lower_case}?useLegacyDatetimeCode=false&serverTimezone=CET +spring.jpa.hibernate.naming.physical-strategy=de.${app.packageName?lower_case}.persistence.config.DatabasePhysicalNamingStrategy +spring.datasource.username=${app.baseName?lower_case} +spring.datasource.password=${app.baseName?lower_case} +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +spring.data.rest.detection-strategy=annotated +#spring.jpa.hibernate.ddl-auto=create + +# Flyway +spring.flyway.user=${r"${spring.datasource.username}"} +spring.flyway.password=${r"${spring.datasource.password}"} +spring.flyway.url=${r"${spring.datasource.url}"} +spring.flyway.baseline-on-migrate=true +spring.flyway.locations=classpath:db/migration +spring.flyway.encoding=UTF-8 +spring.flyway.placeholder-replacement=false + +#logging.level.org.springframework.security=DEBUG + +spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8080/auth/realms/${app.baseName?lower_case} +spring.security.oauth2.client.registration.keycloak.client-id=${app.baseName?lower_case} +spring.security.oauth2.client.registration.keycloak.client-secret=${app.baseName?lower_case} +spring.security.oauth2.client.registration.keycloak.scope=openid diff --git a/generator-templates/api-postgres/application/propertiestest.ftl b/generator-templates/api-postgres/application/propertiestest.ftl new file mode 100644 index 0000000..f6405d4 --- /dev/null +++ b/generator-templates/api-postgres/application/propertiestest.ftl @@ -0,0 +1,27 @@ +# is only required for direct test executing. +rest.base-path=/api + +# actuator +management.endpoints.web.base-path=/monitoring +management.endpoint.health.show-details=always +management.endpoints.web.exposure.include=* + +# show full git properties +management.info.git.mode=full + +# h2 +spring.datasource.url=jdbc:h2:mem:${app.baseName?lower_case};DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=PostgreSQL +spring.datasource.driverClassName=org.h2.Driver +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.show-sql=true +spring.datasource.username=${app.baseName?lower_case} +spring.datasource.password=${app.baseName?lower_case} + +# Flyway +spring.flyway.user=${r"${spring.datasource.username}"} +spring.flyway.password=${r"${spring.datasource.password}"} +spring.flyway.url=${r"${spring.datasource.url}"} +spring.flyway.baseline-on-migrate=true +spring.flyway.locations=classpath:db/migration +spring.flyway.encoding=UTF-8 +spring.flyway.placeholder-replacement=false diff --git a/generator-templates/api-postgres/application/propertiestestpersistence.ftl b/generator-templates/api-postgres/application/propertiestestpersistence.ftl new file mode 100644 index 0000000..63ff24d --- /dev/null +++ b/generator-templates/api-postgres/application/propertiestestpersistence.ftl @@ -0,0 +1,6 @@ +spring.flyway.baselineOnMigrate=true +spring.flyway.locations=classpath:db/test +spring.flyway.encoding=UTF-8 +spring.flyway.placeholder-replacement=false +spring.datasource.url=jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=PostgreSQL +spring.flyway.url=${r"${spring.datasource.url}"} \ No newline at end of file diff --git a/generator-templates/api-postgres/deployment/app-docker-compose.ftl b/generator-templates/api-postgres/deployment/app-docker-compose.ftl new file mode 100644 index 0000000..12136fd --- /dev/null +++ b/generator-templates/api-postgres/deployment/app-docker-compose.ftl @@ -0,0 +1,47 @@ +version: "3.9" +services: + ${app.baseName?lower_case}-db: + container_name: ${app.baseName?lower_case}-db + image: postgres:latest + environment: + POSTGRES_DB: ${app.baseName?lower_case} + POSTGRES_USER: ${app.baseName?lower_case} + POSTGRES_PASSWORD: ${r"${DB_PW_"}${app.baseName}${r"}"} + PGDATA: /var/lib/postgresql/data + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U ${app.baseName?lower_case}'] # <<<--- + interval: 5s + timeout: 60s + retries: 30 + volumes: + - ${app.baseName?lower_case}-db:/var/lib/postgresql/data + expose: + # Opens port 3306 on the container + - '3306' + networks: + - backend + restart: unless-stopped + + ${app.baseName?lower_case}: + image: ${app.baseName?lower_case}:latest + depends_on: + ${app.baseName?lower_case}-db: + condition: service_healthy + restart: on-failure + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://${app.baseName?lower_case}:5432/${app.baseName?lower_case}?useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false + SPRING_DATASOURCE_USERNAME: ${app.baseName?lower_case} + SPRING_DATASOURCE_PASSWORD: ${r"${DB_PW_"}${app.baseName}${r"}"} + KEYCLOAK_AUTH-SERVER-URL: https://${r"${DOMAIN}"}/auth + SERVER_USE_FORWARD_HEADERS: "true" + SERVER_FORWARD_HEADERS_STRATEGY: FRAMEWORK + networks: # Networks to join (Services on the same network can communicate with each other using their name) + - backend + +volumes: + ${app.baseName?lower_case}-db: + + # Networks to be created to facilitate communication between containers +networks: + backend: + diff --git a/generator-templates/api-postgres/deployment/env-docker-compose.ftl b/generator-templates/api-postgres/deployment/env-docker-compose.ftl new file mode 100644 index 0000000..399299d --- /dev/null +++ b/generator-templates/api-postgres/deployment/env-docker-compose.ftl @@ -0,0 +1,86 @@ +version: "3.9" +services: + nginx: + image: nginx:latest + restart: always + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + - ./data/certbot/conf:/etc/letsencrypt + - ./data/certbot/www:/var/www/certbot + - ./conf.d:/etc/nginx/conf.d + - ./content:/var/www/html + ports: + - 80:80 + - 443:443 + networks: # Networks to join (Services on the same network can communicate with each other using their name) + - backend + + certbot: + image: certbot/certbot:latest + entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait ${r"$${!}"}; done;'" + command: "/bin/sh -c 'while :; do sleep 6h & wait ${r"$${!}"}; nginx -s reload; done & nginx -g \"daemon off;\"'" + volumes: + - ./data/certbot/conf:/etc/letsencrypt + - ./certbot/logs:/var/log/letsencrypt + - ./data/certbot/www:/var/www/certbot + + pgadmin: + container_name: pgadmin_container + image: dpage/pgadmin4 + environment: + PGADMIN_DEFAULT_EMAIL: pgadmin4@pgadmin.org + PGADMIN_DEFAULT_PASSWORD: ${r"${DB_PW_ROOT}"} + PGADMIN_CONFIG_SERVER_MODE: 'False' + volumes: + - ${app.baseName?lower_case}-pgadmin:/var/lib/pgadmin + ports: + - "5050:80" + networks: + - backend + restart: unless-stopped + + ${app.baseName?lower_case}-db-keycloak: + image: postgres:latest + restart: on-failure + environment: + POSTGRES_DB: 'keycloak' + POSTGRES_USER: 'keycloak' + POSTGRES_PASSWORD: ${r"${DB_PW_KEYCLOAK}"} + PGDATA: /var/lib/postgresql/data + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U keycloak'] + interval: 5s + timeout: 60s + retries: 30 + volumes: + - ${app.baseName?lower_case}-keycloak-db:/var/lib/postgresql/data + networks: + - backend + + ${app.baseName?lower_case}-keycloak: + image: quay.io/keycloak/keycloak + volumes: + - ./keycloak/imports:/opt/keycloak/data/import + depends_on: + ${app.baseName?lower_case}-db-keycloak: + condition: service_healthy + restart: on-failure + environment: + KC_DB_URL: jdbc:postgresql://${app.baseName?lower_case}-db-keycloak:5432/keycloak + KC_DB: postgres + KC_DB_USERNAME: keycloak + KC_DB_PASSWORD: ${r"${DB_PW_KEYCLOAK}"} + KEYCLOAK_ADMIN: ${r"${KEYCLOAK_USER}"} + KEYCLOAK_ADMIN_PASSWORD: ${r"${KEYCLOAK_PW}"} + KC_HTTP_RELATIVE_PATH: /auth/ + networks: + - backend + +volumes: + templatetest-pgadmin: + templatetest-keycloak-db: + + # Networks to be created to facilitate communication between containers +networks: + backend: + diff --git a/generator-templates/api-postgres/deployment/localenv-docker-compose.ftl b/generator-templates/api-postgres/deployment/localenv-docker-compose.ftl new file mode 100644 index 0000000..8bb5503 --- /dev/null +++ b/generator-templates/api-postgres/deployment/localenv-docker-compose.ftl @@ -0,0 +1,86 @@ +version: "3.9" +services: + postgres: + container_name: ${app.baseName?lower_case}-db + image: postgres:latest + environment: + POSTGRES_DB: ${app.baseName?lower_case} + POSTGRES_USER: ${app.baseName?lower_case} + POSTGRES_PASSWORD: ${app.baseName?lower_case} + PGDATA: /var/lib/postgresql/data + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U ${app.baseName?lower_case}'] + interval: 5s + timeout: 60s + retries: 30 + volumes: + - ${app.baseName?lower_case}-db:/var/lib/postgresql/data + ports: + - "5433:5432" + networks: + - backend + restart: unless-stopped + + pgadmin: + container_name: pgadmin_container + image: dpage/pgadmin4 + environment: + PGADMIN_DEFAULT_EMAIL: pgadmin4@pgadmin.org + PGADMIN_DEFAULT_PASSWORD: admin + PGADMIN_CONFIG_SERVER_MODE: 'False' + volumes: + - ${app.baseName?lower_case}-pgadmin:/var/lib/pgadmin + ports: + - "5050:80" + networks: + - backend + restart: unless-stopped + + ${app.baseName?lower_case}-db-keycloak: + image: postgres:latest + restart: on-failure + environment: + POSTGRES_DB: 'keycloak' + POSTGRES_USER: 'keycloak' + POSTGRES_PASSWORD: 'keycloak' + PGDATA: /var/lib/postgresql/data + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U keycloak'] + interval: 5s + timeout: 60s + retries: 30 + volumes: + - ${app.baseName?lower_case}-keycloak-db:/var/lib/postgresql/data + networks: + - backend + + ${app.baseName?lower_case}-keycloak: + image: quay.io/keycloak/keycloak + volumes: + - ./keycloak/imports:/opt/keycloak/data/import + depends_on: + ${app.baseName?lower_case}-db-keycloak: + condition: service_healthy + restart: on-failure + environment: + KC_DB_URL: jdbc:postgresql://${app.baseName?lower_case}-db-keycloak:5432/keycloak + KC_DB: postgres + KC_DB_USERNAME: keycloak + KC_DB_PASSWORD: keycloak + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: admin + KC_HTTP_RELATIVE_PATH: /auth/ + ports: + - '8080:8080' + networks: + - backend + + +networks: + backend: + +volumes: + ${app.baseName?lower_case}-db: + ${app.baseName?lower_case}-pgadmin: + ${app.baseName?lower_case}-keycloak-db: + diff --git a/generator-templates/api-postgres/deployment/postgreslocal-docker-compose.ftl b/generator-templates/api-postgres/deployment/postgreslocal-docker-compose.ftl new file mode 100644 index 0000000..8c4246f --- /dev/null +++ b/generator-templates/api-postgres/deployment/postgreslocal-docker-compose.ftl @@ -0,0 +1,44 @@ +version: "3.9" +services: + postgres: + container_name: ${app.baseName?lower_case}-db + image: postgres:latest + environment: + POSTGRES_DB: ${app.baseName?lower_case} + POSTGRES_USER: ${app.baseName?lower_case} + POSTGRES_PASSWORD: ${app.baseName?lower_case} + PGDATA: /var/lib/postgresql/data + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U ${app.baseName?lower_case}'] + interval: 5s + timeout: 60s + retries: 30 + volumes: + - ${app.baseName?lower_case}-db:/var/lib/postgresql/data + ports: + - "5433:5432" + networks: + - backend + restart: unless-stopped + + pgadmin: + container_name: pgadmin_container + image: dpage/pgadmin4 + environment: + PGADMIN_DEFAULT_EMAIL: pgadmin4@pgadmin.org + PGADMIN_DEFAULT_PASSWORD: admin + PGADMIN_CONFIG_SERVER_MODE: 'False' + volumes: + - ${app.baseName?lower_case}-pgadmin:/var/lib/pgadmin + ports: + - "5050:80" + networks: + - backend + restart: unless-stopped + +networks: + backend: + +volumes: + ${app.baseName?lower_case}-db: + ${app.baseName?lower_case}-pgadmin: \ No newline at end of file diff --git a/generator-templates/api-postgres/flyway/V1_0__init-postgres.ftl b/generator-templates/api-postgres/flyway/V1_0__init-postgres.ftl new file mode 100755 index 0000000..dffa0d4 --- /dev/null +++ b/generator-templates/api-postgres/flyway/V1_0__init-postgres.ftl @@ -0,0 +1,94 @@ +<#list app.entities as entity> +CREATE SEQUENCE IF NOT EXISTS "${entity.name?lower_case}_id_seq"; + +CREATE TABLE "${entity.name?lower_case}" +( +<#if entity.fields??> + <#list entity.fields as field> + <#if field.fieldType == "String" || field.fieldType == "Enum"> + "${field.fieldName?lower_case}" VARCHAR(255)<#if field.required> NOT NULL , + + <#if field.fieldType == "Integer"> + "${field.fieldName?lower_case}" INTEGER<#if field.required> NOT NULL , + + <#if field.fieldType == "BigDecimal"> + "${field.fieldName?lower_case}" DECIMAL(19,2)<#if field.required> NOT NULL , + + <#if field.fieldType == "Double"> + "${field.fieldName?lower_case}" DOUBLE PRECISION<#if field.required> NOT NULL , + + <#if field.fieldType == "Date"> + "${field.fieldName?lower_case}" DATE<#if field.required> NOT NULL , + + <#if field.fieldType == "Time"> + "${field.fieldName?lower_case}" TIME WITH TIME ZONE<#if field.required> NOT NULL , + + <#if field.fieldType == "Timestamp"> + "${field.fieldName?lower_case}" TIMESTAMP WITH TIME ZONE<#if field.required> NOT NULL , + + <#if field.fieldType == "Boolean"> + "${field.fieldName?lower_case}" BOOLEAN<#if field.required> NOT NULL , + + <#if field.fieldType == "Long"> + "${field.fieldName?lower_case}" BIGINT<#if field.required> NOT NULL , + + + +<#if entity.relationships??> + <#list entity.relationships as relation> + <#if relation.relationshipType == "ManyToOne"> + "${relation.otherEntityName?lower_case}_id" BIGINT, + <#elseif relation.relationshipType == "OneToOne"> + <#if relation.ownerSide> + "${relation.otherEntityName?lower_case}_id" BIGINT UNIQUE, + + + + + "id" BIGINT NOT NULL DEFAULT nextval('${entity.name?lower_case}_id_seq'), + CONSTRAINT "${entity.name?lower_case}_pkey" PRIMARY KEY ("id") +); + + +<#list app.entities as entity> +<#if entity.relationships??> + <#list entity.relationships as relation> + <#if relation.relationshipType == "OneToMany"> + <#elseif relation.relationshipType == "ManyToOne"> +ALTER TABLE "${entity.name?lower_case}" + ADD CONSTRAINT "fk_${entity.name?lower_case}_${relation.relationshipName?lower_case}" + FOREIGN KEY ("${relation.otherEntityName?lower_case}_id") + REFERENCES "${relation.otherEntityName?lower_case}" ("id"); + + <#elseif relation.relationshipType == "OneToOne"> + <#if relation.ownerSide> +ALTER TABLE "${entity.name?lower_case}" + ADD CONSTRAINT "fk_${entity.name?lower_case}_${relation.relationshipName?lower_case}" + FOREIGN KEY ("${relation.otherEntityName?lower_case}_id") + REFERENCES "${relation.otherEntityName?lower_case}" ("id"); + + <#else> + + <#elseif relation.relationshipType == "ManyToMany"> + <#if relation.ownerSide> +CREATE TABLE "${entity.name?lower_case}_${relation.relationshipName?lower_case}" ( + "${entity.name?lower_case}_id" BIGINT NOT NULL, + "${relation.otherEntityName?lower_case}_id" BIGINT NOT NULL, + PRIMARY KEY ("${entity.name?lower_case}_id", "${relation.otherEntityName?lower_case}_id") +); + +ALTER TABLE "${entity.name?lower_case}_${relation.relationshipName?lower_case}" + ADD CONSTRAINT "fk_${entity.name?lower_case}_${relation.relationshipName?lower_case}" + FOREIGN KEY ("${entity.name?lower_case}_id") + REFERENCES "${entity.name?lower_case}" ("id"); + +ALTER TABLE "${entity.name?lower_case}_${relation.relationshipName?lower_case}" + ADD CONSTRAINT "fk_${relation.otherEntityName?lower_case}_${relation.relationshipName?lower_case}" + FOREIGN KEY ("${relation.otherEntityName?lower_case}_id") + REFERENCES "${relation.otherEntityName?lower_case}" ("id"); + + + + + + diff --git a/generator-templates/api-postgres/pom-persistence.ftl b/generator-templates/api-postgres/pom-persistence.ftl new file mode 100644 index 0000000..82945c0 --- /dev/null +++ b/generator-templates/api-postgres/pom-persistence.ftl @@ -0,0 +1,52 @@ + + + 4.0.0 + + de.${app.packageName?lower_case} + ${app.baseName} + 0.0.1-SNAPSHOT + + + persistence + jar + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-validation + + + com.h2database + h2 + test + + + org.postgresql + postgresql + runtime + + + org.flywaydb + flyway-core + + + org.springframework.boot + spring-boot-starter-log4j2 + + + org.springframework.boot + spring-boot-starter-test + test + + + com.fasterxml.jackson.core + jackson-annotations + + + diff --git a/generator-templates/api-postgres/pom.ftl b/generator-templates/api-postgres/pom.ftl new file mode 100644 index 0000000..418c134 --- /dev/null +++ b/generator-templates/api-postgres/pom.ftl @@ -0,0 +1,215 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.1.1 + + + de.${app.packageName?lower_case} + ${app.baseName} + 0.0.1-SNAPSHOT + App template from test @ spring + pom + + + UTF-8 + UTF-8 + 3.1.1 + 6.0.1 + 2.1.0 + 2.7.4 + + + + webclient + application + rest + service + persistence + + + + + + org.springframework + spring-core + ${r"${spring-version}"} + + + org.keycloak.bom + keycloak-adapter-bom + 15.0.2 + pom + import + + + org.springframework.boot + spring-boot-starter + ${r"${spring-boot-version}"} + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-oauth2-client + ${r"${spring-boot-version}"} + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-security + ${r"${spring-boot-version}"} + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-test + test + ${r"${spring-boot-version}"} + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-actuator + ${r"${spring-boot-version}"} + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-validation + ${r"${spring-boot-version}"} + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-data-jpa + ${r"${spring-boot-version}"} + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-data-rest + ${r"${spring-boot-version}"} + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-log4j2 + ${r"${spring-boot-version}"} + + + org.springdoc + springdoc-openapi-ui + ${r"${openapi-version}"} + + + org.springdoc + springdoc-openapi-security + ${r"${openapi-version}"} + + + com.h2database + h2 + 2.1.214 + test + + + org.postgresql + postgresql + 42.5.1 + runtime + + + org.flywaydb + flyway-core + 8.5.8 + + + org.freemarker + freemarker + 2.3.31 + + + + commons-beanutils + commons-beanutils + 1.9.4 + + + + commons-io + commons-io + 2.11.0 + + + + ${r"${project.groupId}"} + application + ${r"${project.version}"} + + + ${r"${project.groupId}"} + rest + ${r"${project.version}"} + + + ${r"${project.groupId}"} + service + ${r"${project.version}"} + + + ${r"${project.groupId}"} + generator + ${r"${project.version}"} + + + ${r"${project.groupId}"} + persistence + ${r"${project.version}"} + + + + diff --git a/generator-templates/api-postgres/readme.ftl b/generator-templates/api-postgres/readme.ftl new file mode 100755 index 0000000..fed44a2 --- /dev/null +++ b/generator-templates/api-postgres/readme.ftl @@ -0,0 +1,94 @@ +### Prerequisites + +* Java JDK 14 or later +* Maven 3 +* NodeJs (16.9.1) and NPM (8.3.2) - [NodeJS Install](https://nodejs.org/en/download/package-manager/) +* mariaDB 10.6 (available for development via docker-compose scripts) +* using Keycloak is optional + +### Installation Steps + +:exclamation: Each step is executed from the project home directory. + +1) go to the deployment folder and start the environment (database and keycloak) via docker-compose: + + ```bash + cd deployment + docker-compose -f localenv-docker-compose.yml up + ``` + +2) go to `webclient/app` and install the frontend applications dependencies + + ```bash + cd webclient/app + npm install --legacy-peer-deps + ``` + +3) build the project + + ```bash + mvn clean install -P frontend + ``` + +4) start project + + ```bash + java -jar application/target/application-0.0.1-SNAPSHOT.jar + ``` + You can also run the main-class via Visual Studio Code. + + +* **Project Builder can be reached under http://localhost:8081/starwit/** +* **If you are using keycloak:** + * **default user/password is admin/admin** + * **keycloak can be reached under http://localost:8081/auth** + +### Debugging + +#### Frontend Debugging + +For debugging, you can start the frontend separately. + +```shell +cd webclient/app +npm start +``` +NPM server starts under localhost:3000/starwit/ by default + +! If you are using the installation with keycloak, make sure you are logged in before first usage - just go to localhost:8081/starwit in your browser. + +#### Backend Debugging + +You can start the spring boot application in debug mode. See Spring Boot documentation for further details. The easiest way is, to use debug functionality integrated with your IDE like VS Code. + +### Postgres Client + +The database is available under localhost:3006 + +``` +Username:starwit +Database:starwit +Password:starwit +``` +PGAdmin is recommended to access database for development purpose. It can be deployed via docker-compose file. + +### Starting without keycloak + +If you want to start your application without keycloak, you need to change spring boot profile to dev in application\src\main\resources\application.properties. + +```properties +spring.profiles.active=dev +``` + +or define env-variable + +```bash +SPRING_PROFILES_ACTIVE=dev +``` + +Start the database without keycloak: + +```bash +cd deployment +docker-compose -f mysqllocal-docker-compose.yml up +``` diff --git a/template-config-api-postgres.json b/template-config-api-postgres.json index 3fb5dd9..3aba1e4 100755 --- a/template-config-api-postgres.json +++ b/template-config-api-postgres.json @@ -5,61 +5,61 @@ "templateFiles": [ { "fileName": "pom.xml", - "templatePath": "postgres/pom.ftl", + "templatePath": "api-postgres/pom.ftl", "targetPath": "/", "category": "BUILD" }, { "fileName": "pom.xml", - "templatePath": "postgres/pom-persistence.ftl", + "templatePath": "api-postgres/pom-persistence.ftl", "targetPath": "persistence/", "category": "BUILD" }, { "fileName": "README.MD", - "templatePath": "postgres/readme.ftl", + "templatePath": "api-postgres/readme.ftl", "targetPath": "/", "category": "DOC" }, { "fileName": "localenv-docker-compose.yml", - "templatePath": "postgres/deployment/localenv-docker-compose.ftl", + "templatePath": "api-postgres/deployment/localenv-docker-compose.ftl", "targetPath": "deployment/", "category": "DEPLOYMENT" }, { "fileName": "postgreslocal-docker-compose.yml", - "templatePath": "postgres/deployment/postgreslocal-docker-compose.ftl", + "templatePath": "api-postgres/deployment/postgreslocal-docker-compose.ftl", "targetPath": "deployment/", "category": "DEPLOYMENT" }, { "fileName": "app-docker-compose.yml", - "templatePath": "postgres/deployment/app-docker-compose.ftl", + "templatePath": "api-postgres/deployment/app-docker-compose.ftl", "targetPath": "deployment/https/", "category": "DEPLOYMENT" }, { "fileName": "env-docker-compose.yml", - "templatePath": "postgres/deployment/env-docker-compose.ftl", + "templatePath": "api-postgres/deployment/env-docker-compose.ftl", "targetPath": "deployment/https/", "category": "DEPLOYMENT" }, { "fileName": "application.properties", - "templatePath": "postgres/application/properties.ftl", + "templatePath": "api-postgres/application/properties.ftl", "targetPath": "application/src/main/resources/", "category": "APP" }, { "fileName": "application.properties", - "templatePath": "postgres/application/propertiestest.ftl", + "templatePath": "api-postgres/application/propertiestest.ftl", "targetPath": "rest/src/test/resources/", "category": "APP" }, { "fileName": "application.properties", - "templatePath": "postgres/application/propertiestestpersistence.ftl", + "templatePath": "api-postgres/application/propertiestestpersistence.ftl", "targetPath": "persistence/src/test/resources/", "category": "APP" }, @@ -89,13 +89,13 @@ }, { "fileName": "V1_0__init.sql", - "templatePath": "postgres/flyway/V1_0__init-postgres.ftl", + "templatePath": "api-postgres/flyway/V1_0__init-postgres.ftl", "targetPath": "persistence/src/main/resources/db/migration/", "category": "ENTITY" }, { "fileName": "V1_0__init.sql", - "templatePath": "postgres/flyway/V1_0__init-postgres.ftl", + "templatePath": "api-postgres/flyway/V1_0__init-postgres.ftl", "targetPath": "persistence/src/test/resources/db/test/", "category": "ENTITY" }, From 932d04ec4dc3224a8a47376c22acc75b477d454e Mon Sep 17 00:00:00 2001 From: Cedric Rische Date: Wed, 2 Aug 2023 22:02:39 +0200 Subject: [PATCH 19/22] Updated Postgres Template --- .../postgres/application/properties.ftl | 9 +++++++-- generator-templates/postgres/pom.ftl | 20 +++++++++---------- generator-templates/postgres/readme.ftl | 6 +++--- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/generator-templates/postgres/application/properties.ftl b/generator-templates/postgres/application/properties.ftl index e5e4a8e..57222d9 100755 --- a/generator-templates/postgres/application/properties.ftl +++ b/generator-templates/postgres/application/properties.ftl @@ -32,9 +32,14 @@ spring.flyway.locations=classpath:db/migration spring.flyway.encoding=UTF-8 spring.flyway.placeholder-replacement=false -#logging.level.org.springframework.security=DEBUG - +# Authentication spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8080/auth/realms/${app.baseName?lower_case} spring.security.oauth2.client.registration.keycloak.client-id=${app.baseName?lower_case} spring.security.oauth2.client.registration.keycloak.client-secret=${app.baseName?lower_case} spring.security.oauth2.client.registration.keycloak.scope=openid + +# OpenApi +springdoc.swagger-ui.csrf.enabled=true + +# logging.level.org.springframework.security=DEBUG +# logging.level.org.springframework.web=DEBUG \ No newline at end of file diff --git a/generator-templates/postgres/pom.ftl b/generator-templates/postgres/pom.ftl index 418c134..4f8e484 100644 --- a/generator-templates/postgres/pom.ftl +++ b/generator-templates/postgres/pom.ftl @@ -39,13 +39,6 @@ spring-core ${r"${spring-version}"} - - org.keycloak.bom - keycloak-adapter-bom - 15.0.2 - pom - import - org.springframework.boot spring-boot-starter @@ -140,15 +133,20 @@ spring-boot-starter-log4j2 ${r"${spring-boot-version}"} + + org.springframework.data + spring-data-jpa + ${spring-boot-version} + org.springdoc - springdoc-openapi-ui - ${r"${openapi-version}"} + springdoc-openapi-starter-webmvc-ui + ${openapi-version} org.springdoc - springdoc-openapi-security - ${r"${openapi-version}"} + springdoc-openapi-starter-common + ${openapi-version} com.h2database diff --git a/generator-templates/postgres/readme.ftl b/generator-templates/postgres/readme.ftl index fed44a2..6d58e7f 100755 --- a/generator-templates/postgres/readme.ftl +++ b/generator-templates/postgres/readme.ftl @@ -1,9 +1,9 @@ ### Prerequisites -* Java JDK 14 or later +* Java JDK 17 or later * Maven 3 * NodeJs (16.9.1) and NPM (8.3.2) - [NodeJS Install](https://nodejs.org/en/download/package-manager/) -* mariaDB 10.6 (available for development via docker-compose scripts) +* Postgres (available for development via docker-compose scripts) * using Keycloak is optional ### Installation Steps @@ -90,5 +90,5 @@ Start the database without keycloak: ```bash cd deployment -docker-compose -f mysqllocal-docker-compose.yml up +docker-compose -f postgreslocal-docker-compose.yml up ``` From 5bc6d84786ff09507337abffa6dec026bea267ff Mon Sep 17 00:00:00 2001 From: Cedric Rische Date: Wed, 2 Aug 2023 22:06:39 +0200 Subject: [PATCH 20/22] Fixed versioning --- generator-templates/postgres/pom.ftl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generator-templates/postgres/pom.ftl b/generator-templates/postgres/pom.ftl index 4f8e484..82d7f1c 100644 --- a/generator-templates/postgres/pom.ftl +++ b/generator-templates/postgres/pom.ftl @@ -136,17 +136,17 @@ org.springframework.data spring-data-jpa - ${spring-boot-version} + ${r"${spring-boot-version}"} org.springdoc springdoc-openapi-starter-webmvc-ui - ${openapi-version} + ${r"${openapi-version}"} org.springdoc springdoc-openapi-starter-common - ${openapi-version} + ${r"${openapi-version}"} com.h2database From 2f674d517b3bfad009d78addc5d5dd47f9f99bfa Mon Sep 17 00:00:00 2001 From: Cedric Rische Date: Wed, 2 Aug 2023 22:31:09 +0200 Subject: [PATCH 21/22] Fixed keycloak Added missing jackson dep --- .../postgres/deployment/env-docker-compose.ftl | 1 + .../postgres/deployment/localenv-docker-compose.ftl | 1 + generator-templates/postgres/pom-persistence.ftl | 4 ++++ 3 files changed, 6 insertions(+) diff --git a/generator-templates/postgres/deployment/env-docker-compose.ftl b/generator-templates/postgres/deployment/env-docker-compose.ftl index 399299d..9fdb7f8 100644 --- a/generator-templates/postgres/deployment/env-docker-compose.ftl +++ b/generator-templates/postgres/deployment/env-docker-compose.ftl @@ -73,6 +73,7 @@ services: KEYCLOAK_ADMIN: ${r"${KEYCLOAK_USER}"} KEYCLOAK_ADMIN_PASSWORD: ${r"${KEYCLOAK_PW}"} KC_HTTP_RELATIVE_PATH: /auth/ + command: start-dev --import-realm networks: - backend diff --git a/generator-templates/postgres/deployment/localenv-docker-compose.ftl b/generator-templates/postgres/deployment/localenv-docker-compose.ftl index 8bb5503..3838e6c 100644 --- a/generator-templates/postgres/deployment/localenv-docker-compose.ftl +++ b/generator-templates/postgres/deployment/localenv-docker-compose.ftl @@ -70,6 +70,7 @@ services: KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin KC_HTTP_RELATIVE_PATH: /auth/ + command: start-dev --import-realm ports: - '8080:8080' networks: diff --git a/generator-templates/postgres/pom-persistence.ftl b/generator-templates/postgres/pom-persistence.ftl index 82945c0..c6d8fca 100644 --- a/generator-templates/postgres/pom-persistence.ftl +++ b/generator-templates/postgres/pom-persistence.ftl @@ -48,5 +48,9 @@ com.fasterxml.jackson.core jackson-annotations + + com.fasterxml.jackson.core + jackson-databind + From 79ecb3ffc08e6ee43fc0b65cbb3e4204c96a0302 Mon Sep 17 00:00:00 2001 From: Cedric Rische Date: Wed, 2 Aug 2023 22:57:48 +0200 Subject: [PATCH 22/22] moved to resource-server --- .../api-postgres/application/properties.ftl | 14 +++++++++----- .../deployment/env-docker-compose.ftl | 1 + .../deployment/localenv-docker-compose.ftl | 1 + .../api-postgres/pom-persistence.ftl | 4 ++++ generator-templates/api-postgres/pom.ftl | 18 ++++++++---------- generator-templates/api-postgres/readme.ftl | 6 +++--- 6 files changed, 26 insertions(+), 18 deletions(-) diff --git a/generator-templates/api-postgres/application/properties.ftl b/generator-templates/api-postgres/application/properties.ftl index e5e4a8e..f103eb1 100755 --- a/generator-templates/api-postgres/application/properties.ftl +++ b/generator-templates/api-postgres/application/properties.ftl @@ -32,9 +32,13 @@ spring.flyway.locations=classpath:db/migration spring.flyway.encoding=UTF-8 spring.flyway.placeholder-replacement=false -#logging.level.org.springframework.security=DEBUG +# Authentication +starwit.authentication.uri=http://localhost:8080/auth/realms/${app.baseName?lower_case} +spring.security.oauth2.resourceserver.jwt.issuer-uri=${starwit.authentication.uri} +spring.security.oauth2.resourceserver.jwt.jwk-set-uri=${starwit.authentication.uri}/protocol/openid-connect/certs -spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8080/auth/realms/${app.baseName?lower_case} -spring.security.oauth2.client.registration.keycloak.client-id=${app.baseName?lower_case} -spring.security.oauth2.client.registration.keycloak.client-secret=${app.baseName?lower_case} -spring.security.oauth2.client.registration.keycloak.scope=openid +# OpenApi +springdoc.swagger-ui.csrf.enabled=true + +# logging.level.org.springframework.security=DEBUG +# logging.level.org.springframework.web=DEBUG \ No newline at end of file diff --git a/generator-templates/api-postgres/deployment/env-docker-compose.ftl b/generator-templates/api-postgres/deployment/env-docker-compose.ftl index 399299d..9fdb7f8 100644 --- a/generator-templates/api-postgres/deployment/env-docker-compose.ftl +++ b/generator-templates/api-postgres/deployment/env-docker-compose.ftl @@ -73,6 +73,7 @@ services: KEYCLOAK_ADMIN: ${r"${KEYCLOAK_USER}"} KEYCLOAK_ADMIN_PASSWORD: ${r"${KEYCLOAK_PW}"} KC_HTTP_RELATIVE_PATH: /auth/ + command: start-dev --import-realm networks: - backend diff --git a/generator-templates/api-postgres/deployment/localenv-docker-compose.ftl b/generator-templates/api-postgres/deployment/localenv-docker-compose.ftl index 8bb5503..3838e6c 100644 --- a/generator-templates/api-postgres/deployment/localenv-docker-compose.ftl +++ b/generator-templates/api-postgres/deployment/localenv-docker-compose.ftl @@ -70,6 +70,7 @@ services: KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin KC_HTTP_RELATIVE_PATH: /auth/ + command: start-dev --import-realm ports: - '8080:8080' networks: diff --git a/generator-templates/api-postgres/pom-persistence.ftl b/generator-templates/api-postgres/pom-persistence.ftl index 82945c0..c6d8fca 100644 --- a/generator-templates/api-postgres/pom-persistence.ftl +++ b/generator-templates/api-postgres/pom-persistence.ftl @@ -48,5 +48,9 @@ com.fasterxml.jackson.core jackson-annotations + + com.fasterxml.jackson.core + jackson-databind + diff --git a/generator-templates/api-postgres/pom.ftl b/generator-templates/api-postgres/pom.ftl index 418c134..aebbfa2 100644 --- a/generator-templates/api-postgres/pom.ftl +++ b/generator-templates/api-postgres/pom.ftl @@ -39,13 +39,6 @@ spring-core ${r"${spring-version}"} - - org.keycloak.bom - keycloak-adapter-bom - 15.0.2 - pom - import - org.springframework.boot spring-boot-starter @@ -59,7 +52,7 @@ org.springframework.boot - spring-boot-starter-oauth2-client + spring-boot-starter-oauth2-resource-server ${r"${spring-boot-version}"} @@ -140,14 +133,19 @@ spring-boot-starter-log4j2 ${r"${spring-boot-version}"} + + org.springframework.data + spring-data-jpa + ${r"${spring-boot-version}"} + org.springdoc - springdoc-openapi-ui + springdoc-openapi-starter-webmvc-ui ${r"${openapi-version}"} org.springdoc - springdoc-openapi-security + springdoc-openapi-starter-common ${r"${openapi-version}"} diff --git a/generator-templates/api-postgres/readme.ftl b/generator-templates/api-postgres/readme.ftl index fed44a2..6d58e7f 100755 --- a/generator-templates/api-postgres/readme.ftl +++ b/generator-templates/api-postgres/readme.ftl @@ -1,9 +1,9 @@ ### Prerequisites -* Java JDK 14 or later +* Java JDK 17 or later * Maven 3 * NodeJs (16.9.1) and NPM (8.3.2) - [NodeJS Install](https://nodejs.org/en/download/package-manager/) -* mariaDB 10.6 (available for development via docker-compose scripts) +* Postgres (available for development via docker-compose scripts) * using Keycloak is optional ### Installation Steps @@ -90,5 +90,5 @@ Start the database without keycloak: ```bash cd deployment -docker-compose -f mysqllocal-docker-compose.yml up +docker-compose -f postgreslocal-docker-compose.yml up ```