From 7dbcf0dc0aad40e79039ec4aadc9c60cf284df40 Mon Sep 17 00:00:00 2001 From: Lam Tran Date: Fri, 3 May 2024 12:28:40 +0200 Subject: [PATCH] Update datadog example app to include traces and add to cart (#617) --- examples/spring-datadog/build.gradle | 23 ++- .../springmvc/config/CtpSecurityConfig.java | 167 ++++++++++++++++-- .../springmvc/config/CtpUserDetails.java | 70 ++++++++ .../springmvc/config/MeClientFilter.java | 73 ++++++++ .../springmvc/config/MonitoringConfig.java | 44 ----- .../springmvc/config/SessionConfig.java | 32 ++++ .../springmvc/config/SessionTokenStorage.java | 43 +++++ .../config/TokenGrantedAuthority.java | 24 +++ .../examples/springmvc/config/WebConfig.java | 73 +++++++- .../springmvc/service/CartRepository.java | 86 --------- .../service/CtpClientBeanService.java | 18 +- .../CtpReactiveAuthenticationManager.java | 84 +++++++++ .../springmvc/service/MeRepository.java | 102 +++++++++++ .../springmvc/service/ProductsRepository.java | 18 +- .../examples/springmvc/web/AppController.java | 87 +++++---- 15 files changed, 743 insertions(+), 201 deletions(-) create mode 100644 examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/CtpUserDetails.java create mode 100644 examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/MeClientFilter.java delete mode 100644 examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/MonitoringConfig.java create mode 100644 examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/SessionConfig.java create mode 100644 examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/SessionTokenStorage.java create mode 100644 examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/TokenGrantedAuthority.java delete mode 100644 examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/service/CartRepository.java create mode 100644 examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/service/CtpReactiveAuthenticationManager.java create mode 100644 examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/service/MeRepository.java diff --git a/examples/spring-datadog/build.gradle b/examples/spring-datadog/build.gradle index 3bc1dbccbe5..3bfad82efb1 100644 --- a/examples/spring-datadog/build.gradle +++ b/examples/spring-datadog/build.gradle @@ -29,11 +29,13 @@ dependencies { implementation "com.commercetools.sdk:commercetools-sdk-java-api:${versions.commercetools}" implementation "com.commercetools.sdk:commercetools-apachehttp-client:${versions.commercetools}" implementation "com.commercetools.sdk:commercetools-monitoring-datadog:${versions.commercetools}" - implementation 'org.springframework.boot:spring-boot-starter-actuator' - implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'javax.inject:javax.inject:1' + implementation 'com.dynatrace.metric.util:dynatrace-metric-utils-java:2.2.0' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'org.springframework.session:spring-session-core' + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.0.4.RELEASE' implementation "com.datadoghq:datadog-api-client:2.20.0" testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' @@ -44,3 +46,16 @@ tasks.named('test') { useJUnitPlatform() } + +tasks.register('downloadDatadog', Download) { + mkdir 'datadog' + src 'https://repo1.maven.org/maven2/com/datadoghq/dd-java-agent/1.32.0/dd-java-agent-1.32.0.jar' + dest file('datadog') +} + +if (project.file("datadog/dd-java-agent-1.32.0.jar").exists()) { + tasks.withType(JavaExec) + { + jvmArgs "-javaagent:datadog/dd-java-agent-1.32.0.jar", "-Ddd.logs.injection=true", "-Ddd.service=example-app", "-Ddd.env=test" + } +} diff --git a/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/CtpSecurityConfig.java b/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/CtpSecurityConfig.java index b6387d7a8e7..63e46f37d0d 100644 --- a/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/CtpSecurityConfig.java +++ b/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/CtpSecurityConfig.java @@ -1,32 +1,169 @@ - package com.commercetools.sdk.examples.springmvc.config; +import java.time.Duration; + +import com.commercetools.api.client.ProjectApiRoot; +import com.commercetools.api.defaultconfig.ApiRootBuilder; +import com.commercetools.api.defaultconfig.ServiceRegion; + +import com.commercetools.sdk.examples.springmvc.service.CtpReactiveAuthenticationManager; +import com.commercetools.sdk.examples.springmvc.service.MeRepository; +import io.vrap.rmf.base.client.ApiHttpClient; +import io.vrap.rmf.base.client.oauth2.ClientCredentials; +import io.vrap.rmf.base.client.oauth2.TokenStorage; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver; +import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.SecurityWebFiltersOrder; import org.springframework.security.config.web.server.ServerHttpSecurity; -import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.core.Authentication; import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.WebFilterExchange; +import org.springframework.security.web.server.authentication.*; +import org.springframework.security.web.server.authentication.logout.*; import org.springframework.security.web.server.context.ServerSecurityContextRepository; import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebSession; +import reactor.core.publisher.Mono; @Configuration -@EnableWebSecurity -@EnableMethodSecurity +@EnableWebFluxSecurity +@EnableReactiveMethodSecurity public class CtpSecurityConfig { + private final ReactiveAuthenticationManagerResolver authenticationManagerResolver; + + @Autowired + public CtpSecurityConfig( + final ReactiveAuthenticationManagerResolver authenticationManagerResolver) { + this.authenticationManagerResolver = authenticationManagerResolver; + } + @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http + SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception { + ServerSecurityContextRepository securityContextRepository = new WebSessionServerSecurityContextRepository(); + return http.securityContextRepository(securityContextRepository) .anonymous() .and() - .authorizeHttpRequests((requests) -> requests - .requestMatchers("**").permitAll() - .requestMatchers("/resources/**").permitAll() - .anyRequest().permitAll() - ); + .addFilterBefore(new LoginWebFilter(authenticationManagerResolver, securityContextRepository), + SecurityWebFiltersOrder.FORM_LOGIN) + .logout() + .logoutUrl("/logout") + .requiresLogout(ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/logout")) + .logoutHandler(new DelegatingServerLogoutHandler(new WebSessionServerLogoutHandler(), + new SecurityContextServerLogoutHandler())) + .logoutSuccessHandler(new RedirectServerLogoutSuccessHandler()) + .and() + .formLogin() + .loginPage("/login") + .requiresAuthenticationMatcher(ServerWebExchangeMatchers.pathMatchers("none")) + .authenticationManager(Mono::just) + .and() + .authorizeExchange() + .pathMatchers("/login") + .permitAll() + .pathMatchers("/") + .permitAll() + .pathMatchers("/resources/**") + .permitAll() + .pathMatchers("/home") + .permitAll() + .pathMatchers("/p/**") + .permitAll() + .pathMatchers("/cart/**") + .permitAll() + .pathMatchers("/me/**") + .authenticated() + .anyExchange() + .authenticated() + .and() + .build(); + } + + @Component + public static class CtpReactiveAuthenticationManagerResolver + implements ReactiveAuthenticationManagerResolver { + private final ApiHttpClient apiHttpClient; + + @Value(value = "${ctp.client.id}") + private String clientId; + + @Value(value = "${ctp.client.secret}") + private String clientSecret; + + @Value(value = "${ctp.project.key}") + private String projectKey; + + private ClientCredentials credentials() { + return ClientCredentials.of().withClientId(clientId).withClientSecret(clientSecret).build(); + } + + @Autowired + public CtpReactiveAuthenticationManagerResolver(final ApiHttpClient apiHttpClient) { + this.apiHttpClient = apiHttpClient; + } + + @Override + public Mono resolve(final ServerWebExchange context) { + return Mono.just(new CtpReactiveAuthenticationManager(meClient(apiHttpClient, context.getSession()), + credentials(), projectKey)); + } + + private ProjectApiRoot meClient(final ApiHttpClient client, final Mono session) { + TokenStorage storage = new SessionTokenStorage(session); + + ApiRootBuilder builder = ApiRootBuilder.of(client) + .withApiBaseUrl(ServiceRegion.GCP_EUROPE_WEST1.getApiUrl()) + .withProjectKey(projectKey) + .withAnonymousRefreshFlow(credentials(), ServiceRegion.GCP_EUROPE_WEST1, storage); + + return builder.build(projectKey); + } + } + + public static class LoginWebFilter extends AuthenticationWebFilter { + public LoginWebFilter(ReactiveAuthenticationManagerResolver authenticationManager, + ServerSecurityContextRepository securityContextRepository) { + super(authenticationManager); + setServerAuthenticationConverter(new ServerFormLoginAuthenticationConverter()); + setRequiresAuthenticationMatcher(ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/login")); + setAuthenticationFailureHandler(new RedirectServerAuthenticationFailureHandler("/login?error")); + setAuthenticationSuccessHandler(new WebFilterChainServerAuthenticationSuccessHandler()); + setSecurityContextRepository(securityContextRepository); + } + } + + private static class WebFilterChainServerAuthenticationSuccessHandler + implements ServerAuthenticationSuccessHandler { + + @Override + public Mono onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) { + ServerWebExchange exchange = webFilterExchange.getExchange(); + TokenStorage storage = new SessionTokenStorage(exchange.getSession()); + TokenGrantedAuthority authority = (TokenGrantedAuthority) authentication.getAuthorities() + .stream() + .filter(grantedAuthority -> grantedAuthority instanceof TokenGrantedAuthority) + .findFirst() + .get(); + storage.setToken(authority.getToken()); - return http.build(); + if (authentication.getPrincipal() instanceof CtpUserDetails) { + exchange.getSession().blockOptional(Duration.ofMillis(500)).ifPresent(session -> { + session.getAttributes() + .put(MeRepository.SESSION_CART, ((CtpUserDetails) authentication.getPrincipal()).getCart()); + session.save(); + }); + } + return webFilterExchange.getChain().filter(exchange); + } } } diff --git a/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/CtpUserDetails.java b/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/CtpUserDetails.java new file mode 100644 index 00000000000..b4cc3d4e2b4 --- /dev/null +++ b/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/CtpUserDetails.java @@ -0,0 +1,70 @@ +package com.commercetools.sdk.examples.springmvc.config; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.commercetools.api.models.cart.CartReference; +import com.commercetools.api.models.customer.Customer; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import reactor.util.annotation.Nullable; + +public class CtpUserDetails implements UserDetails { + private final String customerName; + private final CartReference cartReference; + private final Collection authorities; + + public CtpUserDetails(Customer customer, CartReference cart, Collection authorities) { + this.customerName = userName(customer); + this.cartReference = cart; + this.authorities = authorities; + } + + private String userName(Customer customer) { + return Stream.of(customer.getFirstName(), customer.getMiddleName(), customer.getLastName()) + .filter(s -> s != null && !s.isEmpty()) + .collect(Collectors.joining(" ")); + } + + @Nullable + public CartReference getCart() { + return cartReference; + } + + @Override + public Collection getAuthorities() { + return authorities; + } + + @Override + public String getPassword() { + return null; + } + + @Override + public String getUsername() { + return customerName; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/MeClientFilter.java b/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/MeClientFilter.java new file mode 100644 index 00000000000..21b2f3fef9e --- /dev/null +++ b/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/MeClientFilter.java @@ -0,0 +1,73 @@ +package com.commercetools.sdk.examples.springmvc.config; + +import com.commercetools.api.client.ProjectApiRoot; +import com.commercetools.api.defaultconfig.ApiRootBuilder; +import com.commercetools.api.defaultconfig.ServiceRegion; + +import io.vrap.rmf.base.client.*; +import io.vrap.rmf.base.client.oauth2.ClientCredentials; +import io.vrap.rmf.base.client.oauth2.TokenStorage; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import org.springframework.web.server.WebSession; +import reactor.core.publisher.Mono; + +import java.util.Map; + +@Component +public class MeClientFilter implements WebFilter { + private final ApiHttpClient client; + + @Value(value = "${ctp.client.id}") + private String clientId; + + @Value(value = "${ctp.client.secret}") + private String clientSecret; + + @Value(value = "${ctp.project.key}") + private String projectKey; + + private ClientCredentials credentials() { + return ClientCredentials.of().withClientId(clientId).withClientSecret(clientSecret).build(); + } + + @Autowired + public MeClientFilter(ApiHttpClient client) { + this.client = client; + } + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + final ContextApiHttpClient contextClient = contextClient(client, new MDCContext(Map.of("requestId", exchange.getRequest().getId()))); + final ProjectApiRoot meClient = exchange.getAttributeOrDefault("meClient", + meClient(contextClient, exchange.getSession())); + exchange.getAttributes().put("meClient", meClient); + exchange.getAttributes().put("contextRoot", contextRoot(contextClient)); + + return chain.filter(exchange); + } + + private ProjectApiRoot meClient(ApiHttpClient client, Mono session) { + TokenStorage storage = new SessionTokenStorage(session); + + ApiRootBuilder builder = ApiRootBuilder.of(client) + .withApiBaseUrl(ServiceRegion.GCP_EUROPE_WEST1.getApiUrl()) + .withProjectKey(projectKey) + .withAnonymousRefreshFlow(credentials(), ServiceRegion.GCP_EUROPE_WEST1, storage); + + return builder.build(projectKey); + } + + private ProjectApiRoot contextRoot(ContextApiHttpClient apiHttpClient) { + return ProjectApiRoot.fromClient(projectKey, apiHttpClient); + } + + private ContextApiHttpClient contextClient(ApiHttpClient client, Context context) { + return ContextApiHttpClient.of(client, context); + } +} diff --git a/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/MonitoringConfig.java b/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/MonitoringConfig.java deleted file mode 100644 index 72cfb8662a5..00000000000 --- a/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/MonitoringConfig.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.commercetools.sdk.examples.springmvc.config; - -import ch.qos.logback.classic.helpers.MDCInsertingServletFilter; -import jakarta.servlet.*; -import org.slf4j.MDC; -import org.springframework.boot.web.servlet.FilterRegistrationBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import java.io.IOException; -import java.util.UUID; - -@Configuration -public class MonitoringConfig { - - @Bean - FilterRegistrationBean mdcFilterRegistrationBean() { - FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); - registrationBean.setFilter(new RequestIdMDCInsertingServletFilter()); - registrationBean.addUrlPatterns("/*"); - registrationBean.setOrder(Integer.MIN_VALUE); - return registrationBean; - } - - static class RequestIdMDCInsertingServletFilter extends MDCInsertingServletFilter - { - public static String REQUEST_ID = "req.id"; - - @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - throws IOException, ServletException { - insertRequestId(); - super.doFilter(request, response, chain); - clearRequestId(); - } - - void insertRequestId() { - MDC.put(REQUEST_ID, UUID.randomUUID().toString()); - } - - void clearRequestId() { - MDC.remove(REQUEST_ID); - } - } -} diff --git a/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/SessionConfig.java b/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/SessionConfig.java new file mode 100644 index 00000000000..1807bfb586e --- /dev/null +++ b/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/SessionConfig.java @@ -0,0 +1,32 @@ +package com.commercetools.sdk.examples.springmvc.config; + +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.session.MapSession; +import org.springframework.session.ReactiveMapSessionRepository; +import org.springframework.session.ReactiveSessionRepository; +import org.springframework.session.config.annotation.web.server.EnableSpringWebSession; +import org.springframework.web.server.session.CookieWebSessionIdResolver; +import org.springframework.web.server.session.WebSessionIdResolver; + +@EnableSpringWebSession +@Configuration +public class SessionConfig { + public static final String SESSION_ACCESS_TOKEN = "ctp.token.access"; + public static final String SESSION_REFRESH_TOKEN = "ctp.token.refresh"; + + @Bean + public ReactiveSessionRepository reactiveSessionRepository() { + return new ReactiveMapSessionRepository(new ConcurrentHashMap<>()); + } + + @Bean + public WebSessionIdResolver webSessionIdResolver() { + CookieWebSessionIdResolver resolver = new CookieWebSessionIdResolver(); + resolver.setCookieName("JSESSIONID"); + resolver.addCookieInitializer((builder) -> builder.httpOnly(true).path("/").sameSite("Strict")); + return resolver; + } +} diff --git a/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/SessionTokenStorage.java b/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/SessionTokenStorage.java new file mode 100644 index 00000000000..53c226b90a0 --- /dev/null +++ b/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/SessionTokenStorage.java @@ -0,0 +1,43 @@ +package com.commercetools.sdk.examples.springmvc.config; + +import java.time.Duration; + +import io.vrap.rmf.base.client.AuthenticationToken; +import io.vrap.rmf.base.client.oauth2.TokenStorage; + +import org.springframework.web.server.WebSession; +import reactor.core.publisher.Mono; + +public class SessionTokenStorage implements TokenStorage { + private final Mono session; + + public SessionTokenStorage(Mono session) { + this.session = session; + } + + @Override + public AuthenticationToken getToken() { + WebSession s = session.block(Duration.ofMillis(500)); + assert s != null; + final String accessToken = s.getAttribute(SessionConfig.SESSION_ACCESS_TOKEN); + final String refreshToken = s.getAttribute(SessionConfig.SESSION_REFRESH_TOKEN); + if (accessToken == null) { + return null; + } + System.out.println(accessToken); + + AuthenticationToken token = new AuthenticationToken(); + token.setAccessToken(accessToken); + token.setRefreshToken(refreshToken); + return token; + } + + @Override + public void setToken(AuthenticationToken token) { + session.blockOptional(Duration.ofMillis(500)).ifPresent(s -> { + s.getAttributes().put(SessionConfig.SESSION_ACCESS_TOKEN, token.getAccessToken()); + s.getAttributes().put(SessionConfig.SESSION_REFRESH_TOKEN, token.getRefreshToken()); + s.save(); + }); + } +} diff --git a/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/TokenGrantedAuthority.java b/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/TokenGrantedAuthority.java new file mode 100644 index 00000000000..683286a7f30 --- /dev/null +++ b/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/TokenGrantedAuthority.java @@ -0,0 +1,24 @@ +package com.commercetools.sdk.examples.springmvc.config; + +import io.vrap.rmf.base.client.AuthenticationToken; + +import org.springframework.security.core.GrantedAuthority; + +public class TokenGrantedAuthority implements GrantedAuthority { + private final AuthenticationToken token; + private final String role; + + public TokenGrantedAuthority(final String role, final AuthenticationToken token) { + this.role = role; + this.token = token; + } + + @Override + public String getAuthority() { + return role; + } + + public AuthenticationToken getToken() { + return token; + } +} diff --git a/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/WebConfig.java b/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/WebConfig.java index 60decddf36f..3e8b805ca99 100644 --- a/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/WebConfig.java +++ b/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/config/WebConfig.java @@ -1,17 +1,30 @@ - package com.commercetools.sdk.examples.springmvc.config; +import io.vrap.rmf.base.client.error.NotFoundException; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.servlet.config.annotation.*; -import io.vrap.rmf.base.client.error.NotFoundException; +import org.springframework.web.reactive.config.EnableWebFlux; +import org.springframework.web.reactive.config.ResourceHandlerRegistry; +import org.springframework.web.reactive.config.ViewResolverRegistry; +import org.springframework.web.reactive.config.WebFluxConfigurer; +import org.thymeleaf.extras.springsecurity5.dialect.SpringSecurityDialect; +import org.thymeleaf.spring6.ISpringWebFluxTemplateEngine; +import org.thymeleaf.spring6.SpringWebFluxTemplateEngine; +import org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver; +import org.thymeleaf.spring6.view.reactive.ThymeleafReactiveViewResolver; +import org.thymeleaf.templatemode.TemplateMode; @Configuration @ComponentScan -@EnableWebMvc -public class WebConfig implements WebMvcConfigurer { +@EnableWebFlux +public class WebConfig implements ApplicationContextAware, WebFluxConfigurer { + private ApplicationContext ctx; @ExceptionHandler(NotFoundException.class) public String handleNoSuchElementFoundException(NotFoundException exception) { @@ -20,8 +33,52 @@ public String handleNoSuchElementFoundException(NotFoundException exception) { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { - registry - .addResourceHandler("/resources/**") - .addResourceLocations("classpath:/static/"); + registry.addResourceHandler("/resources/**").addResourceLocations("/static/"); + } + + @Override + public void setApplicationContext(ApplicationContext context) { + this.ctx = context; } + + @Bean + public SpringResourceTemplateResolver thymeleafTemplateResolver() { + + final SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver(); + resolver.setApplicationContext(this.ctx); + resolver.setPrefix("classpath:/templates/"); + resolver.setSuffix(".html"); + resolver.setTemplateMode(TemplateMode.HTML); + resolver.setCacheable(false); + resolver.setCheckExistence(false); + return resolver; + + } + + @Bean + public ISpringWebFluxTemplateEngine thymeleafTemplateEngine() { + // We override here the SpringTemplateEngine instance that would otherwise be + // instantiated by + // Spring Boot because we want to apply the SpringWebFlux-specific context + // factory, link builder... + final SpringWebFluxTemplateEngine templateEngine = new SpringWebFluxTemplateEngine(); + templateEngine.setTemplateResolver(thymeleafTemplateResolver()); + templateEngine.setEnableSpringELCompiler(true); + templateEngine.addDialect(new SpringSecurityDialect()); + return templateEngine; + } + + @Bean + public ThymeleafReactiveViewResolver thymeleafChunkedAndDataDrivenViewResolver() { + final ThymeleafReactiveViewResolver viewResolver = new ThymeleafReactiveViewResolver(); + viewResolver.setTemplateEngine(thymeleafTemplateEngine()); + viewResolver.setResponseMaxChunkSizeBytes(8192); // OUTPUT BUFFER size limit + return viewResolver; + } + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.viewResolver(thymeleafChunkedAndDataDrivenViewResolver()); + } + } diff --git a/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/service/CartRepository.java b/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/service/CartRepository.java deleted file mode 100644 index 312eff18e3f..00000000000 --- a/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/service/CartRepository.java +++ /dev/null @@ -1,86 +0,0 @@ - -package com.commercetools.sdk.examples.springmvc.service; - -import java.util.Collections; -import java.util.concurrent.CompletableFuture; - -import com.commercetools.api.client.ProjectScopedApiRoot; -import com.commercetools.api.models.cart.Cart; -import com.commercetools.api.models.cart.CartBuilder; - -import io.vrap.rmf.base.client.ApiHttpResponse; - -import jakarta.servlet.http.HttpSession; - -public class CartRepository { - private final ProjectScopedApiRoot apiRoot; - - private final HttpSession session; - - public static final String SESSION_CART = "ctp.session.cart"; - public static final String SESSION_CART_ITEMS = "ctp.session.cart_items"; - - public CartRepository(ProjectScopedApiRoot apiRoot, HttpSession session) { - this.apiRoot = apiRoot; - this.session = session; - } - - private Cart emptyCart() { - return CartBuilder.of().lineItems(Collections.emptyList()).buildUnchecked(); - } - - public CompletableFuture meCart() { - if (session.getAttribute(SESSION_CART) == null) { - return CompletableFuture.completedFuture(emptyCart()); - } - return apiRoot.carts().withId(session.getAttribute(SESSION_CART).toString()).get().execute() - .thenApply(ApiHttpResponse::getBody) - .thenApply(cart -> { - session.setAttribute(SESSION_CART, cart.getId()); - session.setAttribute(SESSION_CART_ITEMS, cart.getTotalLineItemQuantity()); - return cart; - }) - .exceptionally(throwable -> emptyCart()); - } - - - public CompletableFuture addToCart(final String sku) { - return addToCart(sku, 1); - } - - public CompletableFuture addToCart(final String sku, final long quantity) { - return meCart().thenCompose(cart -> { - if (cart.getId() == null) { - return apiRoot.carts() - .post(c -> c.currency("EUR").plusLineItems(b -> b.sku(sku).quantity(quantity))) - .execute() - .thenApply(ApiHttpResponse::getBody) - .thenApply(c -> { - session.setAttribute(SESSION_CART, c.getId()); - session.setAttribute(SESSION_CART_ITEMS, c.getTotalLineItemQuantity()); - return c; - }); - } - return apiRoot.carts() - .withId(cart.getId()) - .post(c -> c.version(cart.getVersion()) - .plusActions(a -> a.addLineItemBuilder().sku(sku).quantity(quantity))) - .execute() - .thenApply(ApiHttpResponse::getBody); - }); - } - - public CompletableFuture removeFromCart(final String lineItemId) { - return meCart().thenCompose(cart -> { - if (cart.getId() == null) { - return CompletableFuture.completedFuture(emptyCart()); - } - return apiRoot.carts() - .withId(cart.getId()) - .post(c -> c.version(cart.getVersion()) - .withActions(a -> a.removeLineItemBuilder().lineItemId(lineItemId))) - .execute() - .thenApply(ApiHttpResponse::getBody); - }); - } -} diff --git a/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/service/CtpClientBeanService.java b/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/service/CtpClientBeanService.java index 13417ab0bb4..3c4f04aeed4 100644 --- a/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/service/CtpClientBeanService.java +++ b/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/service/CtpClientBeanService.java @@ -1,14 +1,15 @@ - package com.commercetools.sdk.examples.springmvc.service; -import com.commercetools.api.client.ProjectScopedApiRoot; +import com.commercetools.api.client.ProjectApiRoot; import com.commercetools.api.defaultconfig.ApiRootBuilder; import com.commercetools.monitoring.datadog.DatadogMiddleware; import com.commercetools.monitoring.datadog.DatadogResponseSerializer; import com.datadog.api.client.ApiClient; +import io.vrap.rmf.base.client.ApiHttpClient; import io.vrap.rmf.base.client.ResponseSerializer; import io.vrap.rmf.base.client.oauth2.ClientCredentials; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.Bean; @@ -32,11 +33,20 @@ private ClientCredentials credentials() { } @Bean - public ProjectScopedApiRoot apiRoot() { + public ApiHttpClient client() { ApiRootBuilder builder = ApiRootBuilder.of() .defaultClient(credentials()) .withTelemetryMiddleware(new DatadogMiddleware(ApiClient.getDefaultApiClient())) .withSerializer(new DatadogResponseSerializer(ResponseSerializer.of(), ApiClient.getDefaultApiClient())); - return builder.build(projectKey); + return builder.buildClient(); + } + + @Bean + @Autowired + public ProjectApiRoot apiRoot(ApiHttpClient client) { + + final ProjectApiRoot build = ProjectApiRoot.fromClient(projectKey, client); + + return build; } } diff --git a/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/service/CtpReactiveAuthenticationManager.java b/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/service/CtpReactiveAuthenticationManager.java new file mode 100644 index 00000000000..e9965cb35f4 --- /dev/null +++ b/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/service/CtpReactiveAuthenticationManager.java @@ -0,0 +1,84 @@ +package com.commercetools.sdk.examples.springmvc.service; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Optional; + +import com.commercetools.api.client.ProjectApiRoot; +import com.commercetools.api.defaultconfig.ServiceRegion; +import com.commercetools.api.models.cart.CartReferenceBuilder; +import com.commercetools.api.models.customer.CustomerSignInResult; +import com.commercetools.api.models.customer.MyCustomerSigninBuilder; + +import com.commercetools.sdk.examples.springmvc.config.CtpUserDetails; +import com.commercetools.sdk.examples.springmvc.config.TokenGrantedAuthority; +import io.vrap.rmf.base.client.*; +import io.vrap.rmf.base.client.oauth2.ClientCredentials; +import io.vrap.rmf.base.client.oauth2.GlobalCustomerPasswordTokenSupplier; + +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import reactor.core.publisher.Mono; + +public class CtpReactiveAuthenticationManager implements ReactiveAuthenticationManager { + VrapHttpClient client; + ProjectApiRoot apiRoot; + + private final ClientCredentials credentials; + + private final String projectKey; + + public CtpReactiveAuthenticationManager(final ProjectApiRoot apiRoot, final ClientCredentials credentials, + final String projectKey) { + this.apiRoot = apiRoot; + this.client = HttpClientSupplier.of().get(); + this.credentials = credentials; + this.projectKey = projectKey; + } + + @Override + public Mono authenticate(Authentication authentication) { + if (authentication instanceof UsernamePasswordAuthenticationToken) { + if (authentication.getCredentials() == null || authentication.getPrincipal() == null) { + return Mono.defer(() -> Mono.error(new BadCredentialsException("Invalid Credentials"))); + } + MyCustomerSigninBuilder customerSignin = MyCustomerSigninBuilder.of() + .email(authentication.getName()) + .password(authentication.getCredentials().toString()); + + return Mono + .fromFuture( + apiRoot.me().login().post(customerSignin.build()).execute().exceptionally(throwable -> null)) + .flatMap(customerSignInResultApiHttpResponse -> { + GlobalCustomerPasswordTokenSupplier supplier = new GlobalCustomerPasswordTokenSupplier( + credentials.getClientId(), credentials.getClientSecret(), authentication.getName(), + authentication.getCredentials().toString(), null, + ServiceRegion.GCP_EUROPE_WEST1.getPasswordFlowTokenURL(projectKey), client); + + return Mono.zip(Mono.fromFuture(supplier.getToken().exceptionally(throwable -> null)), + Mono.just(customerSignInResultApiHttpResponse.getBody())); + }) + .switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("Invalid Credentials")))) + .map(tokenSignin -> { + final AuthenticationToken token = tokenSignin.getT1(); + final CustomerSignInResult signinResult = tokenSignin.getT2(); + + final Collection authorities = authentication.getAuthorities(); + GrantedAuthority authority = new TokenGrantedAuthority("ROLE_USER", token); + Collection updatedAuthorities = new ArrayList<>(); + updatedAuthorities.add(authority); + updatedAuthorities.addAll(authorities); + + return new UsernamePasswordAuthenticationToken(new CtpUserDetails(signinResult.getCustomer(), + Optional.ofNullable(signinResult.getCart()) + .map(c -> CartReferenceBuilder.of().id(c.getId()).build()) + .orElse(null), + updatedAuthorities), "", updatedAuthorities); + }); + } + return Mono.defer(() -> Mono.error(new BadCredentialsException("Invalid authentication"))); + } +} diff --git a/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/service/MeRepository.java b/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/service/MeRepository.java new file mode 100644 index 00000000000..47b020ae1fd --- /dev/null +++ b/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/service/MeRepository.java @@ -0,0 +1,102 @@ +package com.commercetools.sdk.examples.springmvc.service; + +import java.util.Collections; + +import com.commercetools.api.client.ProjectApiRoot; +import com.commercetools.api.models.cart.Cart; +import com.commercetools.api.models.cart.CartBuilder; +import com.commercetools.api.models.customer.Customer; +import com.commercetools.api.models.me.MyCartAddLineItemActionBuilder; +import com.commercetools.api.models.me.MyCartDraftBuilder; +import com.commercetools.api.models.me.MyCartUpdateBuilder; +import com.commercetools.api.models.me.MyLineItemDraftBuilder; + +import io.vrap.rmf.base.client.ApiHttpResponse; + +import org.springframework.web.server.WebSession; +import reactor.core.publisher.Mono; + +public class MeRepository { + private final ProjectApiRoot apiRoot; + + private final WebSession session; + + public static final String SESSION_CART = "ctp.session.cart"; + public static final String SESSION_CART_ITEMS = "ctp.session.cart_items"; + + public MeRepository(ProjectApiRoot apiRoot, WebSession session) { + this.apiRoot = apiRoot; + this.session = session; + } + + private Cart emptyCart() { + return CartBuilder.of().lineItems(Collections.emptyList()).buildUnchecked(); + } + + public Mono meCart() { + if (session.getAttribute(SESSION_CART) == null) { + return Mono.just(emptyCart()); + } + return Mono.fromFuture(apiRoot.me().activeCart().get().execute().thenApply(ApiHttpResponse::getBody)) + .doOnSuccess(cart -> { + session.getAttributes().put(SESSION_CART, cart.getId()); + session.getAttributes().put(SESSION_CART_ITEMS, cart.getTotalLineItemQuantity()); + }) + .onErrorReturn(emptyCart()); + } + + public Mono me() { + return Mono.fromFuture( + apiRoot.me().get().execute().thenApply(ApiHttpResponse::getBody).exceptionally(throwable -> null)); + } + + public Mono addToCart(final String sku) { + return addToCart(sku, 1); + } + + public Mono addToCart(final String sku, final long quantity) { + return meCart().flatMap(cart -> { + if (cart.getId() == null) { + return Mono + .fromFuture(apiRoot.me() + .carts() + .post(MyCartDraftBuilder.of() + .currency("EUR") + .lineItems(MyLineItemDraftBuilder.of().sku(sku).quantity(quantity).build()) + .build()) + .execute() + .thenApply(ApiHttpResponse::getBody)) + .doOnSuccess(c -> { + session.getAttributes().put(SESSION_CART, c.getId()); + session.getAttributes().put(SESSION_CART_ITEMS, c.getTotalLineItemQuantity()); + }); + } + return Mono.fromFuture(apiRoot.me() + .carts() + .withId(cart.getId()) + .post(MyCartUpdateBuilder.of() + .version(cart.getVersion()) + .actions(MyCartAddLineItemActionBuilder.of().sku(sku).quantity(quantity).build()) + .build()) + .execute() + .thenApply(ApiHttpResponse::getBody)); + }); + } + + public Mono removeFromCart(final String lineItemId) { + return meCart().flatMap(cart -> { + if (cart.getId() == null) { + return Mono.just(emptyCart()); + } + return Mono.fromFuture(apiRoot.me() + .carts() + .withId(cart.getId()) + .post(MyCartUpdateBuilder.of() + .version(cart.getVersion()) + .withActions(actionBuilder -> actionBuilder.removeLineItemBuilder().lineItemId(lineItemId)) + .build()) + .execute() + .thenApply(ApiHttpResponse::getBody)); + }); + } +} diff --git a/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/service/ProductsRepository.java b/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/service/ProductsRepository.java index 78bf5ef5f7a..30a143cd29d 100644 --- a/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/service/ProductsRepository.java +++ b/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/service/ProductsRepository.java @@ -4,25 +4,33 @@ import java.util.List; import java.util.concurrent.CompletableFuture; -import com.commercetools.api.client.ProjectScopedApiRoot; +import com.commercetools.api.client.ProjectApiRoot; import com.commercetools.api.models.product.ProductProjection; import com.commercetools.api.models.product.ProductProjectionPagedSearchResponse; import io.vrap.rmf.base.client.ApiHttpResponse; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Mono; + +@Repository +@Component public class ProductsRepository { - private final ProjectScopedApiRoot apiRoot; + private final ProjectApiRoot apiRoot; - public ProductsRepository(ProjectScopedApiRoot apiRoot) { + public ProductsRepository(ProjectApiRoot apiRoot) { this.apiRoot = apiRoot; } - public CompletableFuture> products() { - return apiRoot.productProjections() + public Mono> products() { + final CompletableFuture> products = apiRoot.productProjections() .search() .get() .execute() .thenApply(ApiHttpResponse::getBody) .thenApply(ProductProjectionPagedSearchResponse::getResults); + return Mono.fromFuture(products); } + } diff --git a/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/web/AppController.java b/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/web/AppController.java index 477c9527a93..a3788f1ab28 100644 --- a/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/web/AppController.java +++ b/examples/spring-datadog/src/main/java/com/commercetools/sdk/examples/springmvc/web/AppController.java @@ -1,72 +1,89 @@ package com.commercetools.sdk.examples.springmvc.web; -import com.commercetools.api.client.ProjectScopedApiRoot; +import java.util.List; + +import com.commercetools.api.client.ProjectApiRoot; import com.commercetools.api.models.cart.Cart; +import com.commercetools.api.models.customer.Customer; import com.commercetools.api.models.product.ProductProjection; -import com.commercetools.sdk.examples.springmvc.service.CartRepository; + +import com.commercetools.sdk.examples.springmvc.service.MeRepository; import com.commercetools.sdk.examples.springmvc.service.ProductsRepository; -import jakarta.annotation.Resource; -import jakarta.servlet.http.HttpSession; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.servlet.view.RedirectView; - -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.reactive.result.view.RedirectView; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebSession; +import reactor.core.publisher.Mono; @Controller public class AppController { + + @GetMapping("/login") + public String viewLoginPage(ServerWebExchange exchange, Model model) { + return "login"; + } + + @PostMapping("/login") + public Mono login(ServerWebExchange exchange, Authentication authentication) { + return Mono.just("redirect:/me"); + } + @GetMapping("/") - public String home() { + public String home(Model model) { + model.addAttribute("project", "World"); return "home/index"; } - @Value(value = "${ctp.project.key}") - private String projectKey; + @GetMapping("/p") + public String pop(@RequestAttribute("contextRoot") ProjectApiRoot contextRoot, @RequestAttribute("meClient") ProjectApiRoot meClient, Model model, WebSession session) { - @Autowired - @Resource(name = "apiRoot") - ProjectScopedApiRoot apiRoot; + Mono> products = new ProductsRepository(contextRoot).products(); + final Mono cart = new MeRepository(meClient, session).meCart(); - @GetMapping("/p") - public String pop(Model model, HttpSession session) throws ExecutionException, InterruptedException { - CompletableFuture> products = new ProductsRepository(apiRoot).products(); - CompletableFuture cart = new CartRepository(apiRoot, session).meCart(); - model.addAttribute("products", products.get()); - model.addAttribute("cart", cart.get()); + model.addAttribute("products", products); + model.addAttribute("cart", cart); return "home/pop"; } @GetMapping("/cart") - public String cart(Model model, HttpSession session) { - final CompletableFuture cart = new CartRepository(apiRoot, session).meCart(); + public String myCart(@RequestAttribute("meClient") ProjectApiRoot client, Model model, WebSession session) { + final Mono cart = new MeRepository(client, session).meCart(); + model.addAttribute("cart", cart); return "mycart/index"; } @GetMapping("/cart/add") - public RedirectView addToCart(String sku, Model model, HttpSession session) - throws ExecutionException, InterruptedException { - CartRepository repository = new CartRepository(apiRoot, session); + public RedirectView addToCart(@RequestAttribute("meClient") ProjectApiRoot client, String sku, Model model, + WebSession session) { + MeRepository repository = new MeRepository(client, session); - final CompletableFuture cart = repository.addToCart(sku); - model.addAttribute("cart", cart.get()); + final Mono cart = repository.addToCart(sku); + model.addAttribute("cart", cart); return new RedirectView("/cart"); } @GetMapping("/cart/del") - public RedirectView removeFromCart(String lineItemId, Model model, HttpSession session) - throws ExecutionException, InterruptedException { - CartRepository repository = new CartRepository(apiRoot, session); + public RedirectView removeFromCart(@RequestAttribute("meClient") ProjectApiRoot client, String lineItemId, + Model model, WebSession session) { + MeRepository repository = new MeRepository(client, session); - final CompletableFuture cart = repository.removeFromCart(lineItemId); - model.addAttribute("cart", cart.get()); + final Mono cart = repository.removeFromCart(lineItemId); + model.addAttribute("cart", cart); return new RedirectView("/cart"); } + + @GetMapping("/me") + public String me(@RequestAttribute("meClient") ProjectApiRoot client, Model model, WebSession session) { + final Mono customer = new MeRepository(client, session).me(); + model.addAttribute("customer", customer); + return "me/index"; + } }