Add Checkstyle and Prettier to BFF #65
@ -11,7 +11,7 @@ import org.springframework.boot.web.servlet.support.SpringBootServletInitializer
|
||||
@ConfigurationPropertiesScan
|
||||
public class Studentportalen extends SpringBootServletInitializer {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Studentportalen.class, args);
|
||||
}
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Studentportalen.class, args);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,5 +3,4 @@ package se.su.dsv.studentportalen.bff.config;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties("se.su.dsv.backend-api")
|
||||
public record BackendApiConfiguration(String daisyUrl) {
|
||||
}
|
||||
public record BackendApiConfiguration(String daisyUrl) {}
|
||||
|
||||
@ -4,9 +4,11 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties("se.su.dsv.frontend")
|
||||
public record FrontendConfiguration(String url) {
|
||||
public FrontendConfiguration {
|
||||
if (url == null || url.isBlank()) {
|
||||
throw new IllegalArgumentException("se.su.dsv.frontend.url must not be null or blank");
|
||||
}
|
||||
public FrontendConfiguration {
|
||||
if (url == null || url.isBlank()) {
|
||||
throw new IllegalArgumentException(
|
||||
"se.su.dsv.frontend.url must not be null or blank"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package se.su.dsv.studentportalen.bff.config;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
@ -7,45 +8,55 @@ import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import se.su.dsv.studentportalen.bff.login.BFFAuthenticationEntryPoint;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(
|
||||
HttpSecurity http,
|
||||
FrontendConfiguration frontendConfiguration)
|
||||
throws Exception
|
||||
{
|
||||
http.exceptionHandling(exception -> exception
|
||||
.authenticationEntryPoint(new BFFAuthenticationEntryPoint()));
|
||||
http.oauth2Login(login -> login
|
||||
.defaultSuccessUrl(frontendConfiguration.url(), true));
|
||||
http.authorizeHttpRequests(authorize -> authorize
|
||||
.requestMatchers("/swagger", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
|
||||
.anyRequest().authenticated());
|
||||
http.cors(cors -> cors
|
||||
.configurationSource(_ -> frontendOnlyCors(frontendConfiguration)));
|
||||
http.csrf(csrf -> csrf.spa());
|
||||
return http.build();
|
||||
}
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(
|
||||
HttpSecurity http,
|
||||
FrontendConfiguration frontendConfiguration
|
||||
) throws Exception {
|
||||
http.exceptionHandling(exception ->
|
||||
exception.authenticationEntryPoint(new BFFAuthenticationEntryPoint())
|
||||
);
|
||||
http.oauth2Login(login ->
|
||||
login.defaultSuccessUrl(frontendConfiguration.url(), true)
|
||||
);
|
||||
http.authorizeHttpRequests(authorize ->
|
||||
authorize
|
||||
.requestMatchers("/swagger", "/swagger-ui/**", "/v3/api-docs/**")
|
||||
.permitAll()
|
||||
.anyRequest()
|
||||
.authenticated()
|
||||
);
|
||||
http.cors(cors ->
|
||||
cors.configurationSource(_ -> frontendOnlyCors(frontendConfiguration))
|
||||
);
|
||||
http.csrf(csrf -> csrf.spa());
|
||||
return http.build();
|
||||
}
|
||||
|
||||
private static CorsConfiguration frontendOnlyCors(FrontendConfiguration frontendConfiguration) {
|
||||
var corsConfiguration = new CorsConfiguration();
|
||||
corsConfiguration.setAllowedOrigins(List.of(frontendConfiguration.url()));
|
||||
corsConfiguration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
|
||||
private static CorsConfiguration frontendOnlyCors(
|
||||
FrontendConfiguration frontendConfiguration
|
||||
) {
|
||||
var corsConfiguration = new CorsConfiguration();
|
||||
corsConfiguration.setAllowedOrigins(List.of(frontendConfiguration.url()));
|
||||
corsConfiguration.setAllowedMethods(
|
||||
List.of("GET", "POST", "PUT", "DELETE")
|
||||
);
|
||||
|
||||
// Allow the frontend to see the X-Authorization-Url header
|
||||
corsConfiguration.setExposedHeaders(List.of("X-Authorization-Url"));
|
||||
// Allow the frontend to see the X-Authorization-Url header
|
||||
corsConfiguration.setExposedHeaders(List.of("X-Authorization-Url"));
|
||||
|
||||
// To allow the session cookie to be included
|
||||
corsConfiguration.setAllowCredentials(true);
|
||||
// To allow the session cookie to be included
|
||||
corsConfiguration.setAllowCredentials(true);
|
||||
|
||||
// Content-Type is allowed by default but with a restriction on the value
|
||||
// The restriction does not allow "application/json" so we add it as an allowed header
|
||||
// X-XSRF-TOKEN is needed for CSRF protection
|
||||
corsConfiguration.setAllowedHeaders(List.of("Content-Type", "X-XSRF-TOKEN"));
|
||||
return corsConfiguration;
|
||||
}
|
||||
// Content-Type is allowed by default but with a restriction on the value
|
||||
// The restriction does not allow "application/json" so we add it as an allowed header
|
||||
// X-XSRF-TOKEN is needed for CSRF protection
|
||||
corsConfiguration.setAllowedHeaders(
|
||||
List.of("Content-Type", "X-XSRF-TOKEN")
|
||||
);
|
||||
return corsConfiguration;
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,16 +16,16 @@ import se.su.dsv.studentportalen.bff.service.ProfileService;
|
||||
@RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public class ProfileController {
|
||||
|
||||
private final ProfileService service;
|
||||
private final ProfileService service;
|
||||
|
||||
public ProfileController(ProfileService service) {
|
||||
this.service = service;
|
||||
}
|
||||
public ProfileController(ProfileService service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
@GetMapping("/profile")
|
||||
public ProfileResponse getProfile(
|
||||
@AuthenticationPrincipal(errorOnInvalidType = true) OAuth2User currentUser)
|
||||
{
|
||||
return service.getProfile(currentUser);
|
||||
}
|
||||
@GetMapping("/profile")
|
||||
public ProfileResponse getProfile(
|
||||
@AuthenticationPrincipal(errorOnInvalidType = true) OAuth2User currentUser
|
||||
) {
|
||||
return service.getProfile(currentUser);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,85 +1,88 @@
|
||||
package se.su.dsv.studentportalen.bff.controller;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.StructuredTaskScope;
|
||||
import java.util.concurrent.StructuredTaskScope.Subtask;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import se.su.dsv.studentportalen.bff.config.BackendApiConfiguration;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.StructuredTaskScope;
|
||||
import java.util.concurrent.StructuredTaskScope.Subtask;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/test")
|
||||
@Profile("development")
|
||||
public class TestController {
|
||||
|
||||
private static final int NAME_DELAY_SECONDS = 2;
|
||||
private static final int EMAIL_DELAY_SECONDS = 3;
|
||||
private static final int NAME_DELAY_SECONDS = 2;
|
||||
private static final int EMAIL_DELAY_SECONDS = 3;
|
||||
|
||||
private final RestClient restClient;
|
||||
private final BackendApiConfiguration backendApiConfiguration;
|
||||
private final RestClient restClient;
|
||||
private final BackendApiConfiguration backendApiConfiguration;
|
||||
|
||||
public TestController(final BackendApiConfiguration backendApiConfiguration) {
|
||||
this.backendApiConfiguration = backendApiConfiguration;
|
||||
this.restClient = RestClient.builder()
|
||||
.baseUrl("http://localhost:8080")
|
||||
.build();
|
||||
public TestController(final BackendApiConfiguration backendApiConfiguration) {
|
||||
this.backendApiConfiguration = backendApiConfiguration;
|
||||
this.restClient = RestClient.builder()
|
||||
.baseUrl("http://localhost:8080")
|
||||
.build();
|
||||
}
|
||||
|
||||
@RequestMapping
|
||||
public String helloWorld() throws InterruptedException, ExecutionException {
|
||||
// Pick the scope based on the desired behaviour
|
||||
try (var scope = StructuredTaskScope.open()) {
|
||||
Subtask<String> nameTask = scope.fork(() -> {
|
||||
String name = restClient
|
||||
.get()
|
||||
.uri("/test/name")
|
||||
.retrieve()
|
||||
.body(String.class);
|
||||
return name;
|
||||
});
|
||||
|
||||
Subtask<String> emailTask = scope.fork(() -> {
|
||||
String email = restClient
|
||||
.get()
|
||||
.uri("/test/email")
|
||||
.retrieve()
|
||||
.body(String.class);
|
||||
return email;
|
||||
});
|
||||
|
||||
Subtask<DaisyProfile> daisyProfile = scope.fork(() -> {
|
||||
DaisyProfile profile = restClient
|
||||
.get()
|
||||
.uri(backendApiConfiguration.daisyUrl(), builder ->
|
||||
builder.path("/profile").build()
|
||||
)
|
||||
.retrieve()
|
||||
.body(DaisyProfile.class);
|
||||
return profile;
|
||||
});
|
||||
|
||||
// Wait for all tasks to complete (either success or failure)
|
||||
scope.join();
|
||||
|
||||
return "Hello, I am %s and my email is %s. My Daisy profile is: %s".formatted(
|
||||
nameTask.get(),
|
||||
emailTask.get(),
|
||||
daisyProfile.get()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@RequestMapping
|
||||
public String helloWorld() throws InterruptedException, ExecutionException {
|
||||
// Pick the scope based on the desired behaviour
|
||||
try (var scope = StructuredTaskScope.open()) {
|
||||
Subtask<String> nameTask = scope.fork(() -> {
|
||||
String name = restClient.get()
|
||||
.uri("/test/name")
|
||||
.retrieve()
|
||||
.body(String.class);
|
||||
return name;
|
||||
});
|
||||
@RequestMapping("/name")
|
||||
public String name() throws InterruptedException {
|
||||
Thread.sleep(Duration.ofSeconds(NAME_DELAY_SECONDS));
|
||||
return "Greg";
|
||||
}
|
||||
|
||||
Subtask<String> emailTask = scope.fork(() -> {
|
||||
String email = restClient.get()
|
||||
.uri("/test/email")
|
||||
.retrieve()
|
||||
.body(String.class);
|
||||
return email;
|
||||
});
|
||||
@RequestMapping("/email")
|
||||
public String email() throws InterruptedException {
|
||||
Thread.sleep(Duration.ofSeconds(EMAIL_DELAY_SECONDS));
|
||||
return "greg@localhost";
|
||||
}
|
||||
|
||||
Subtask<DaisyProfile> daisyProfile = scope.fork(() -> {
|
||||
DaisyProfile profile = restClient.get()
|
||||
.uri(backendApiConfiguration.daisyUrl(), builder -> builder
|
||||
.path("/profile")
|
||||
.build())
|
||||
.retrieve()
|
||||
.body(DaisyProfile.class);
|
||||
return profile;
|
||||
});
|
||||
|
||||
// Wait for all tasks to complete (either success or failure)
|
||||
scope.join();
|
||||
|
||||
return "Hello, I am %s and my email is %s. My Daisy profile is: %s".formatted(
|
||||
nameTask.get(),
|
||||
emailTask.get(),
|
||||
daisyProfile.get());
|
||||
}
|
||||
}
|
||||
|
||||
@RequestMapping("/name")
|
||||
public String name() throws InterruptedException {
|
||||
Thread.sleep(Duration.ofSeconds(NAME_DELAY_SECONDS));
|
||||
return "Greg";
|
||||
}
|
||||
|
||||
@RequestMapping("/email")
|
||||
public String email() throws InterruptedException {
|
||||
Thread.sleep(Duration.ofSeconds(EMAIL_DELAY_SECONDS));
|
||||
return "greg@localhost";
|
||||
}
|
||||
|
||||
record DaisyProfile(String name, String mail) {}
|
||||
record DaisyProfile(String name, String mail) {}
|
||||
}
|
||||
|
||||
@ -1,26 +1,25 @@
|
||||
package se.su.dsv.studentportalen.bff.dto.response;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* User profile information.
|
||||
*/
|
||||
public record ProfileResponse(
|
||||
@JsonProperty(value = "name", required = true)
|
||||
String name,
|
||||
@JsonProperty(value = "name", required = true) String name,
|
||||
|
||||
@JsonProperty(value = "language", required = true)
|
||||
Language language)
|
||||
{
|
||||
public enum Language {
|
||||
@JsonProperty("sv") SWEDISH,
|
||||
@JsonProperty("en") ENGLISH
|
||||
}
|
||||
@JsonProperty(value = "language", required = true) Language language
|
||||
) {
|
||||
public enum Language {
|
||||
@JsonProperty("sv")
|
||||
SWEDISH,
|
||||
@JsonProperty("en")
|
||||
ENGLISH,
|
||||
}
|
||||
|
||||
public ProfileResponse {
|
||||
Objects.requireNonNull(name, "name must be specified");
|
||||
Objects.requireNonNull(language, "language must be specified");
|
||||
}
|
||||
public ProfileResponse {
|
||||
Objects.requireNonNull(name, "name must be specified");
|
||||
Objects.requireNonNull(language, "language must be specified");
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,17 +7,18 @@ import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||
|
||||
public class BFFAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||
@Override
|
||||
public void commence(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
AuthenticationException authException)
|
||||
{
|
||||
String loginUri = ServletUriComponentsBuilder.fromRequest(request)
|
||||
.replacePath("/oauth2/authorization/studentportalen")
|
||||
.build()
|
||||
.toUriString();
|
||||
response.addHeader("X-Authorization-Url", loginUri);
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commence(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
AuthenticationException authException
|
||||
) {
|
||||
String loginUri = ServletUriComponentsBuilder.fromRequest(request)
|
||||
.replacePath("/oauth2/authorization/studentportalen")
|
||||
.build()
|
||||
.toUriString();
|
||||
response.addHeader("X-Authorization-Url", loginUri);
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,8 +10,8 @@ import se.su.dsv.studentportalen.bff.dto.response.ProfileResponse;
|
||||
@Service
|
||||
public class ProfileService {
|
||||
|
||||
public ProfileResponse getProfile(OAuth2User currentUser) {
|
||||
String name = currentUser.getAttribute("name");
|
||||
return new ProfileResponse(name, ProfileResponse.Language.ENGLISH);
|
||||
}
|
||||
public ProfileResponse getProfile(OAuth2User currentUser) {
|
||||
String name = currentUser.getAttribute("name");
|
||||
return new ProfileResponse(name, ProfileResponse.Language.ENGLISH);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user