Add SPA sample using BFF and Spring Cloud Gateway
This commit is contained in:
@@ -5,6 +5,26 @@
|
|||||||
|
|
||||||
The default sample provides the minimal configuration to get started with Spring Authorization Server.
|
The default sample provides the minimal configuration to get started with Spring Authorization Server.
|
||||||
|
|
||||||
|
[[spa-sample]]
|
||||||
|
== SPA (Single Page Application) Sample
|
||||||
|
|
||||||
|
The SPA sample provides a reference implementation of the https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps-19#name-backend-for-frontend-bff[Backend For Frontend (BFF)] application architecture pattern.
|
||||||
|
The *spa-client* is the _frontend_ SPA implemented with Angular and the *backend-for-spa-client* is the _backend_ application.
|
||||||
|
The *backend-for-spa-client* uses https://spring.io/projects/spring-cloud-gateway[Spring Cloud Gateway] to route `/userinfo` (UserInfo Endpoint) requests to *demo-authorizationserver* and `/messages` requests to *messages-resource*.
|
||||||
|
The *backend-for-spa-client* performs the authorization flows and stores the access tokens.
|
||||||
|
The *spa-client* is never exposed the access tokens and directly communicates with the *backend-for-spa-client* via an authenticated session cookie.
|
||||||
|
|
||||||
|
[[run-spa-sample]]
|
||||||
|
=== Run the Sample
|
||||||
|
|
||||||
|
* Run Authorization Server -> `./gradlew -b samples/demo-authorizationserver/samples-demo-authorizationserver.gradle bootRun`
|
||||||
|
* Run Resource Server -> `./gradlew -b samples/messages-resource/samples-messages-resource.gradle bootRun`
|
||||||
|
* Run Backend -> `./gradlew -b samples/backend-for-spa-client/samples-backend-for-spa-client.gradle bootRun`
|
||||||
|
* Run Frontend -> `ng serve` (from `samples/spa-client` directory)
|
||||||
|
** *NOTE:* Angular must be installed locally before running `ng serve`. See https://angular.dev/installation[installation instructions].
|
||||||
|
* Go to `http://127.0.0.1:4200`
|
||||||
|
** Login with credentials -> user1 \ password
|
||||||
|
|
||||||
[[demo-sample]]
|
[[demo-sample]]
|
||||||
== Demo Sample
|
== Demo Sample
|
||||||
|
|
||||||
|
|||||||
1
samples/backend-for-spa-client/gradle.properties
Normal file
1
samples/backend-for-spa-client/gradle.properties
Normal file
@@ -0,0 +1 @@
|
|||||||
|
spring-security.version=6.3.0
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
plugins {
|
||||||
|
id "org.springframework.boot" version "3.2.2"
|
||||||
|
id "io.spring.dependency-management" version "1.1.0"
|
||||||
|
id "java"
|
||||||
|
}
|
||||||
|
|
||||||
|
group = project.rootProject.group
|
||||||
|
version = project.rootProject.version
|
||||||
|
|
||||||
|
java {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven { url "https://repo.spring.io/milestone" }
|
||||||
|
maven { url "https://repo.spring.io/snapshot" }
|
||||||
|
}
|
||||||
|
|
||||||
|
ext {
|
||||||
|
set("springCloudVersion", "2023.0.2")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencyManagement {
|
||||||
|
imports {
|
||||||
|
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation "org.springframework.boot:spring-boot-starter-web"
|
||||||
|
implementation "org.springframework.boot:spring-boot-starter-security"
|
||||||
|
implementation "org.springframework.boot:spring-boot-starter-oauth2-client"
|
||||||
|
implementation "org.springframework.cloud:spring-cloud-starter-gateway-mvc"
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package sample;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 1.4
|
||||||
|
*/
|
||||||
|
@SpringBootApplication
|
||||||
|
public class BackendForSpaClientApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(BackendForSpaClientApplication.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package sample.config;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
|
import org.springframework.web.cors.CorsConfigurationSource;
|
||||||
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 1.4
|
||||||
|
*/
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
public class CorsConfig {
|
||||||
|
|
||||||
|
@Value("${app.base-uri}")
|
||||||
|
private String appBaseUri;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CorsConfigurationSource corsConfigurationSource() {
|
||||||
|
CorsConfiguration config = new CorsConfiguration();
|
||||||
|
config.addAllowedHeader("X-XSRF-TOKEN");
|
||||||
|
config.addAllowedHeader(HttpHeaders.CONTENT_TYPE);
|
||||||
|
config.setAllowedMethods(Arrays.asList("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||||
|
config.setAllowedOrigins(Collections.singletonList(this.appBaseUri));
|
||||||
|
config.setAllowCredentials(true);
|
||||||
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||||
|
source.registerCorsConfiguration("/**", config);
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package sample.config;
|
||||||
|
|
||||||
|
import org.springframework.cloud.gateway.server.mvc.common.Shortcut;
|
||||||
|
import org.springframework.cloud.gateway.server.mvc.filter.SimpleFilterSupplier;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||||
|
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||||
|
import org.springframework.web.servlet.function.HandlerFilterFunction;
|
||||||
|
import org.springframework.web.servlet.function.ServerRequest;
|
||||||
|
import org.springframework.web.servlet.function.ServerResponse;
|
||||||
|
|
||||||
|
import static org.springframework.cloud.gateway.server.mvc.common.MvcUtils.getApplicationContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom {@code HandlerFilterFunction}'s registered in META-INF/spring.factories and used in application.yml.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 1.4
|
||||||
|
*/
|
||||||
|
public interface GatewayFilterFunctions {
|
||||||
|
|
||||||
|
@Shortcut
|
||||||
|
static HandlerFilterFunction<ServerResponse, ServerResponse> relayTokenIfExists(String clientRegistrationId) {
|
||||||
|
return (request, next) -> {
|
||||||
|
Authentication principal = (Authentication) request.servletRequest().getUserPrincipal();
|
||||||
|
OAuth2AuthorizedClientRepository authorizedClientRepository = getApplicationContext(request)
|
||||||
|
.getBean(OAuth2AuthorizedClientRepository.class);
|
||||||
|
OAuth2AuthorizedClient authorizedClient = authorizedClientRepository.loadAuthorizedClient(
|
||||||
|
clientRegistrationId, principal, request.servletRequest());
|
||||||
|
if (authorizedClient != null) {
|
||||||
|
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
|
||||||
|
ServerRequest bearerRequest = ServerRequest.from(request)
|
||||||
|
.headers(httpHeaders -> httpHeaders.setBearerAuth(accessToken.getTokenValue())).build();
|
||||||
|
return next.handle(bearerRequest);
|
||||||
|
}
|
||||||
|
return next.handle(request);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class FilterSupplier extends SimpleFilterSupplier {
|
||||||
|
|
||||||
|
FilterSupplier() {
|
||||||
|
super(GatewayFilterFunctions.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package sample.config;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
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.web.AuthenticationEntryPoint;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
|
||||||
|
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
|
||||||
|
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
|
||||||
|
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
|
||||||
|
import org.springframework.security.web.authentication.logout.CompositeLogoutHandler;
|
||||||
|
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
|
||||||
|
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
||||||
|
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
||||||
|
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
|
||||||
|
import org.springframework.security.web.csrf.CsrfLogoutHandler;
|
||||||
|
import org.springframework.security.web.csrf.CsrfTokenRepository;
|
||||||
|
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
|
||||||
|
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
|
||||||
|
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 1.4
|
||||||
|
*/
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@EnableWebSecurity
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
@Value("${app.base-uri}")
|
||||||
|
private String appBaseUri;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
|
CookieCsrfTokenRepository cookieCsrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
|
||||||
|
CsrfTokenRequestAttributeHandler csrfTokenRequestAttributeHandler = new CsrfTokenRequestAttributeHandler();
|
||||||
|
/*
|
||||||
|
IMPORTANT:
|
||||||
|
Set the csrfRequestAttributeName to null, to opt-out of deferred tokens, resulting in the CsrfToken to be loaded on every request.
|
||||||
|
If it does not exist, the CookieCsrfTokenRepository will automatically generate a new one and add the Cookie to the response.
|
||||||
|
See the reference: https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#deferred-csrf-token
|
||||||
|
*/
|
||||||
|
csrfTokenRequestAttributeHandler.setCsrfRequestAttributeName(null);
|
||||||
|
|
||||||
|
// @formatter:off
|
||||||
|
http
|
||||||
|
.authorizeHttpRequests(authorize ->
|
||||||
|
authorize
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
)
|
||||||
|
.csrf(csrf ->
|
||||||
|
csrf
|
||||||
|
.csrfTokenRepository(cookieCsrfTokenRepository)
|
||||||
|
.csrfTokenRequestHandler(csrfTokenRequestAttributeHandler)
|
||||||
|
)
|
||||||
|
.cors(Customizer.withDefaults())
|
||||||
|
.exceptionHandling(exceptionHandling ->
|
||||||
|
exceptionHandling
|
||||||
|
.authenticationEntryPoint(authenticationEntryPoint())
|
||||||
|
)
|
||||||
|
.oauth2Login(oauth2Login ->
|
||||||
|
oauth2Login
|
||||||
|
.successHandler(new SimpleUrlAuthenticationSuccessHandler(this.appBaseUri)))
|
||||||
|
.logout(logout ->
|
||||||
|
logout
|
||||||
|
.addLogoutHandler(logoutHandler(cookieCsrfTokenRepository))
|
||||||
|
.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK))
|
||||||
|
)
|
||||||
|
.oauth2Client(Customizer.withDefaults());
|
||||||
|
// @formatter:on
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private AuthenticationEntryPoint authenticationEntryPoint() {
|
||||||
|
AuthenticationEntryPoint authenticationEntryPoint =
|
||||||
|
new LoginUrlAuthenticationEntryPoint("/oauth2/authorization/messaging-client-oidc");
|
||||||
|
MediaTypeRequestMatcher textHtmlMatcher =
|
||||||
|
new MediaTypeRequestMatcher(MediaType.TEXT_HTML);
|
||||||
|
textHtmlMatcher.setUseEquals(true);
|
||||||
|
|
||||||
|
LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> entryPoints = new LinkedHashMap<>();
|
||||||
|
entryPoints.put(textHtmlMatcher, authenticationEntryPoint);
|
||||||
|
|
||||||
|
DelegatingAuthenticationEntryPoint delegatingAuthenticationEntryPoint = new DelegatingAuthenticationEntryPoint(entryPoints);
|
||||||
|
delegatingAuthenticationEntryPoint.setDefaultEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));
|
||||||
|
return delegatingAuthenticationEntryPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LogoutHandler logoutHandler(CsrfTokenRepository csrfTokenRepository) {
|
||||||
|
return new CompositeLogoutHandler(
|
||||||
|
new SecurityContextLogoutHandler(),
|
||||||
|
new CsrfLogoutHandler(csrfTokenRepository));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package sample.web;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 1.4
|
||||||
|
*/
|
||||||
|
@Controller
|
||||||
|
public class DefaultController {
|
||||||
|
|
||||||
|
@Value("${app.base-uri}")
|
||||||
|
private String appBaseUri;
|
||||||
|
|
||||||
|
@GetMapping("/")
|
||||||
|
public String root() {
|
||||||
|
return "redirect:" + this.appBaseUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
// '/authorized' is the registered 'redirect_uri' for authorization_code
|
||||||
|
@GetMapping("/authorized")
|
||||||
|
public String authorized() {
|
||||||
|
return "redirect:" + this.appBaseUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
org.springframework.cloud.gateway.server.mvc.filter.FilterSupplier=\
|
||||||
|
sample.config.GatewayFilterFunctions.FilterSupplier
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
server:
|
||||||
|
port: 8080
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
root: INFO
|
||||||
|
org.springframework.web: INFO
|
||||||
|
org.springframework.security: INFO
|
||||||
|
org.springframework.security.oauth2: INFO
|
||||||
|
|
||||||
|
spring:
|
||||||
|
security:
|
||||||
|
oauth2:
|
||||||
|
client:
|
||||||
|
registration:
|
||||||
|
messaging-client-oidc:
|
||||||
|
provider: spring
|
||||||
|
client-id: messaging-client
|
||||||
|
client-secret: secret
|
||||||
|
authorization-grant-type: authorization_code
|
||||||
|
redirect-uri: "http://127.0.0.1:8080/login/oauth2/code/{registrationId}"
|
||||||
|
scope: openid,profile
|
||||||
|
client-name: messaging-client-oidc
|
||||||
|
messaging-client-authorization-code:
|
||||||
|
provider: spring
|
||||||
|
client-id: messaging-client
|
||||||
|
client-secret: secret
|
||||||
|
authorization-grant-type: authorization_code
|
||||||
|
redirect-uri: "http://127.0.0.1:8080/authorized"
|
||||||
|
scope: message.read,message.write
|
||||||
|
client-name: messaging-client-authorization-code
|
||||||
|
provider:
|
||||||
|
spring:
|
||||||
|
issuer-uri: http://localhost:9000
|
||||||
|
cloud:
|
||||||
|
gateway:
|
||||||
|
mvc:
|
||||||
|
routes:
|
||||||
|
- id: userinfo
|
||||||
|
uri: http://localhost:9000
|
||||||
|
predicates:
|
||||||
|
- Path=/userinfo
|
||||||
|
filters:
|
||||||
|
- TokenRelay=
|
||||||
|
- id: messages
|
||||||
|
uri: http://localhost:8090
|
||||||
|
predicates:
|
||||||
|
- Path=/messages
|
||||||
|
filters:
|
||||||
|
- RelayTokenIfExists=messaging-client-authorization-code
|
||||||
|
|
||||||
|
app:
|
||||||
|
base-uri: http://127.0.0.1:4200
|
||||||
17
samples/spa-client/.editorconfig
Normal file
17
samples/spa-client/.editorconfig
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Editor configuration, see https://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.ts]
|
||||||
|
quote_type = single
|
||||||
|
ij_typescript_use_double_quotes = false
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
||||||
42
samples/spa-client/.gitignore
vendored
Normal file
42
samples/spa-client/.gitignore
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||||
|
|
||||||
|
# Compiled output
|
||||||
|
/dist
|
||||||
|
/tmp
|
||||||
|
/out-tsc
|
||||||
|
/bazel-out
|
||||||
|
|
||||||
|
# Node
|
||||||
|
/node_modules
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
.idea/
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.history/*
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
/.angular/cache
|
||||||
|
.sass-cache/
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
|
||||||
|
# System files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
27
samples/spa-client/README.md
Normal file
27
samples/spa-client/README.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# SpaClient
|
||||||
|
|
||||||
|
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 18.2.11.
|
||||||
|
|
||||||
|
## Development server
|
||||||
|
|
||||||
|
Run `ng serve` for a dev server. Navigate to `http://127.0.0.1:4200/`. The application will automatically reload if you change any of the source files.
|
||||||
|
|
||||||
|
## Code scaffolding
|
||||||
|
|
||||||
|
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||||
|
|
||||||
|
## Running end-to-end tests
|
||||||
|
|
||||||
|
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
|
||||||
|
|
||||||
|
## Further help
|
||||||
|
|
||||||
|
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
||||||
103
samples/spa-client/angular.json
Normal file
103
samples/spa-client/angular.json
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"spa-client": {
|
||||||
|
"projectType": "application",
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"style": "scss"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular-devkit/build-angular:application",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/spa-client",
|
||||||
|
"index": "src/index.html",
|
||||||
|
"browser": "src/main.ts",
|
||||||
|
"polyfills": [
|
||||||
|
"zone.js"
|
||||||
|
],
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"inlineStyleLanguage": "scss",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "public"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.scss"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "500kB",
|
||||||
|
"maximumError": "1MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "2kB",
|
||||||
|
"maximumError": "4kB"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"optimization": false,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "production"
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"buildTarget": "spa-client:build:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"buildTarget": "spa-client:build:development",
|
||||||
|
"host": "127.0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development"
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"builder": "@angular-devkit/build-angular:extract-i18n"
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
|
"options": {
|
||||||
|
"polyfills": [
|
||||||
|
"zone.js",
|
||||||
|
"zone.js/testing"
|
||||||
|
],
|
||||||
|
"tsConfig": "tsconfig.spec.json",
|
||||||
|
"inlineStyleLanguage": "scss",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "public"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.scss"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13125
samples/spa-client/package-lock.json
generated
Normal file
13125
samples/spa-client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
samples/spa-client/package.json
Normal file
38
samples/spa-client/package.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "spa-client",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"ng": "ng",
|
||||||
|
"start": "ng serve",
|
||||||
|
"build": "ng build",
|
||||||
|
"watch": "ng build --watch --configuration development",
|
||||||
|
"test": "ng test"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/animations": "^18.2.0",
|
||||||
|
"@angular/common": "^18.2.0",
|
||||||
|
"@angular/compiler": "^18.2.0",
|
||||||
|
"@angular/core": "^18.2.0",
|
||||||
|
"@angular/forms": "^18.2.0",
|
||||||
|
"@angular/platform-browser": "^18.2.0",
|
||||||
|
"@angular/platform-browser-dynamic": "^18.2.0",
|
||||||
|
"@angular/router": "^18.2.0",
|
||||||
|
"rxjs": "~7.8.0",
|
||||||
|
"tslib": "^2.3.0",
|
||||||
|
"zone.js": "~0.14.10"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular-devkit/build-angular": "^18.2.11",
|
||||||
|
"@angular/cli": "^18.2.11",
|
||||||
|
"@angular/compiler-cli": "^18.2.0",
|
||||||
|
"@types/jasmine": "~5.1.0",
|
||||||
|
"jasmine-core": "~5.2.0",
|
||||||
|
"karma": "~6.4.0",
|
||||||
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
|
"karma-coverage": "~2.2.0",
|
||||||
|
"karma-jasmine": "~5.1.0",
|
||||||
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
|
"typescript": "~5.5.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
samples/spa-client/public/spring-security.svg
Normal file
1
samples/spa-client/public/spring-security.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 108.08 150.97"><defs><style>.cls-1{fill:#6bb344;}</style></defs><title>logo-security</title><path class="cls-1" d="M108.08,13,54,0,0,13V54.6H28.67a23.94,23.94,0,0,0,0,6H0V80.14C0,125,54,151,54,151s54-26,54-70.83V60.62H79.4a22.75,22.75,0,0,0,0-6h28.68ZM54,77.15A19.54,19.54,0,1,1,73.58,57.61,19.54,19.54,0,0,1,54,77.15Z"/><path class="cls-1" d="M54,48.34a5.06,5.06,0,0,0-2.32,9.56v1.31l1.49,1.49v1l1,1v1l-.88.88.94,1.55v1l-1,1.19,1.4,1.4,1.55-1.55V58A5.06,5.06,0,0,0,54,48.34Zm0,5.26a1.88,1.88,0,1,1,1.88-1.88A1.88,1.88,0,0,1,54,53.6Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 628 B |
58
samples/spa-client/src/app/app.component.html
Normal file
58
samples/spa-client/src/app/app.component.html
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<div>
|
||||||
|
<nav class="navbar navbar-expand-lg bg-light">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="#">
|
||||||
|
<img src="/spring-security.svg" width="40" height="32">
|
||||||
|
</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||||
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link active" aria-current="page" href="/">Home</a>
|
||||||
|
</li>
|
||||||
|
<li *ngIf="isAuthenticated" class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">Authorize</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="#" (click)="authorizeMessages()">Messages</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="d-flex">
|
||||||
|
@if (isAuthenticated) {
|
||||||
|
<div>
|
||||||
|
<span class="fs-6 px-3">{{ userName }}</span>
|
||||||
|
<button class="btn btn-outline-dark" (click)="logout()">Logout</button>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-outline-dark" (click)="login()">Login</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div *ngIf="messages.length > 0" class="row py-5 justify-content-start">
|
||||||
|
<div class="col">
|
||||||
|
<table class="table table-striped caption-top">
|
||||||
|
<caption>Messages</caption>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">#</th>
|
||||||
|
<th scope="col">Message</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let message of messages; let index = index">
|
||||||
|
<th scope="row">{{ index + 1 }}</th>
|
||||||
|
<td>{{ message }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
0
samples/spa-client/src/app/app.component.scss
Normal file
0
samples/spa-client/src/app/app.component.scss
Normal file
75
samples/spa-client/src/app/app.component.ts
Normal file
75
samples/spa-client/src/app/app.component.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import {Component, OnInit} from '@angular/core';
|
||||||
|
import {NgIf, NgForOf} from '@angular/common';
|
||||||
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
import {catchError, of} from 'rxjs';
|
||||||
|
import {environment} from "./environment";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
standalone: true,
|
||||||
|
imports: [NgIf, NgForOf],
|
||||||
|
templateUrl: './app.component.html',
|
||||||
|
styleUrl: './app.component.scss'
|
||||||
|
})
|
||||||
|
export class AppComponent implements OnInit {
|
||||||
|
isAuthenticated: boolean = false;
|
||||||
|
userName: string = '';
|
||||||
|
messages: string[] = [];
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.getUserInfo();
|
||||||
|
this.getMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
login(): void {
|
||||||
|
// The Backend is configured to trigger login when unauthenticated
|
||||||
|
window.location.href = environment.backendBaseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
logout(): void {
|
||||||
|
this.http.post('/logout', null)
|
||||||
|
.pipe(catchError((error) => {
|
||||||
|
console.error(error);
|
||||||
|
return of(null);
|
||||||
|
}))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.isAuthenticated = false;
|
||||||
|
this.userName = '';
|
||||||
|
this.messages = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserInfo(): void {
|
||||||
|
this.http.get<any>('/userinfo')
|
||||||
|
.pipe(catchError((error) => {
|
||||||
|
console.error(error);
|
||||||
|
return of(null);
|
||||||
|
}))
|
||||||
|
.subscribe((userInfo) => {
|
||||||
|
if (userInfo) {
|
||||||
|
this.isAuthenticated = true;
|
||||||
|
this.userName = userInfo.sub;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
authorizeMessages(): void {
|
||||||
|
// Trigger the Backend to perform the authorization_code grant flow.
|
||||||
|
// After authorization is complete, the Backend will redirect back to this app.
|
||||||
|
window.location.href = environment.backendBaseUrl + "/oauth2/authorization/messaging-client-authorization-code";
|
||||||
|
}
|
||||||
|
|
||||||
|
getMessages(): void {
|
||||||
|
this.http.get<string[]>('/messages')
|
||||||
|
.pipe(catchError((error) => {
|
||||||
|
console.error(error);
|
||||||
|
return of([]);
|
||||||
|
}))
|
||||||
|
.subscribe((messages) => {
|
||||||
|
this.messages = messages;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
14
samples/spa-client/src/app/app.config.ts
Normal file
14
samples/spa-client/src/app/app.config.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
import {provideHttpClient, withInterceptors} from '@angular/common/http';
|
||||||
|
|
||||||
|
import { routes } from './app.routes';
|
||||||
|
import { withCredentialsInterceptor } from './http.interceptors';
|
||||||
|
|
||||||
|
export const appConfig: ApplicationConfig = {
|
||||||
|
providers: [
|
||||||
|
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||||
|
provideRouter(routes),
|
||||||
|
provideHttpClient(withInterceptors([withCredentialsInterceptor]))
|
||||||
|
]
|
||||||
|
};
|
||||||
3
samples/spa-client/src/app/app.routes.ts
Normal file
3
samples/spa-client/src/app/app.routes.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
|
export const routes: Routes = [];
|
||||||
3
samples/spa-client/src/app/environment.ts
Normal file
3
samples/spa-client/src/app/environment.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const environment = {
|
||||||
|
backendBaseUrl: 'http://127.0.0.1:8080',
|
||||||
|
};
|
||||||
24
samples/spa-client/src/app/http.interceptors.ts
Normal file
24
samples/spa-client/src/app/http.interceptors.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import {HttpRequest, HttpHandlerFn, HttpEvent} from '@angular/common/http';
|
||||||
|
import {Observable} from 'rxjs';
|
||||||
|
import {environment} from "./environment";
|
||||||
|
|
||||||
|
/*
|
||||||
|
IMPORTANT:
|
||||||
|
|
||||||
|
By default, the HttpClient passes the CSRF token via the X-XSRF-TOKEN header using its built-in interceptor.
|
||||||
|
However, this DOES NOT WORK when absolute URLs are used in HttpClient calls.
|
||||||
|
Hence, the reason for this interceptor, as it prepends the Backend base URL to the relative URL.
|
||||||
|
Ensure you only use relative URLs in HttpClient calls for mutating requests (e.g. POST),
|
||||||
|
otherwise operations such as /logout will not work.
|
||||||
|
|
||||||
|
See the reference for further information:
|
||||||
|
https://angular.dev/best-practices/security#httpclient-xsrf-csrf-security
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function withCredentialsInterceptor(request: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
|
||||||
|
request = request.clone({
|
||||||
|
url: environment.backendBaseUrl + request.url,
|
||||||
|
withCredentials: true // This is required to ensure the Session Cookie is passed in every request to the Backend
|
||||||
|
});
|
||||||
|
return next(request);
|
||||||
|
}
|
||||||
15
samples/spa-client/src/index.html
Normal file
15
samples/spa-client/src/index.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>SPA with Backend and Spring Cloud Gateway</title>
|
||||||
|
<base href="/">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<app-root></app-root>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js" integrity="sha384-oBqDVmMz9ATKxIep9tiCxS/Z9fNfEXiDAYTujMAeBAsjFuCZSmKbSSUnQlmh/jp3" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js" integrity="sha384-cuYeSxntonz0PPNlHhBs68uyIAVpIIOZZ5JqeqvYYIcEL727kskC66kF92t6Xl2V" crossorigin="anonymous"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
6
samples/spa-client/src/main.ts
Normal file
6
samples/spa-client/src/main.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { bootstrapApplication } from '@angular/platform-browser';
|
||||||
|
import { appConfig } from './app/app.config';
|
||||||
|
import { AppComponent } from './app/app.component';
|
||||||
|
|
||||||
|
bootstrapApplication(AppComponent, appConfig)
|
||||||
|
.catch((err) => console.error(err));
|
||||||
1
samples/spa-client/src/styles.scss
Normal file
1
samples/spa-client/src/styles.scss
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/* You can add global styles to this file, and also import other style files */
|
||||||
15
samples/spa-client/tsconfig.app.json
Normal file
15
samples/spa-client/tsconfig.app.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||||
|
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/app",
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/main.ts"
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
33
samples/spa-client/tsconfig.json
Normal file
33
samples/spa-client/tsconfig.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||||
|
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||||
|
{
|
||||||
|
"compileOnSave": false,
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist/out-tsc",
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"declaration": false,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"importHelpers": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ES2022",
|
||||||
|
"lib": [
|
||||||
|
"ES2022",
|
||||||
|
"dom"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
"strictInjectionParameters": true,
|
||||||
|
"strictInputAccessModifiers": true,
|
||||||
|
"strictTemplates": true
|
||||||
|
}
|
||||||
|
}
|
||||||
15
samples/spa-client/tsconfig.spec.json
Normal file
15
samples/spa-client/tsconfig.spec.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||||
|
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/spec",
|
||||||
|
"types": [
|
||||||
|
"jasmine"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user