Add Checkstyle and Prettier to BFF #65

Merged
stne3960 merged 15 commits from chore/checkstyle into main 2026-01-13 13:17:19 +01:00
9 changed files with 164 additions and 149 deletions
Showing only changes of commit 0e3c33a1d0 - Show all commits

View File

@ -11,7 +11,7 @@ import org.springframework.boot.web.servlet.support.SpringBootServletInitializer
@ConfigurationPropertiesScan @ConfigurationPropertiesScan
public class Studentportalen extends SpringBootServletInitializer { public class Studentportalen extends SpringBootServletInitializer {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(Studentportalen.class, args); SpringApplication.run(Studentportalen.class, args);
} }
} }

View File

@ -3,5 +3,4 @@ package se.su.dsv.studentportalen.bff.config;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("se.su.dsv.backend-api") @ConfigurationProperties("se.su.dsv.backend-api")
public record BackendApiConfiguration(String daisyUrl) { public record BackendApiConfiguration(String daisyUrl) {}
}

View File

@ -4,9 +4,11 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("se.su.dsv.frontend") @ConfigurationProperties("se.su.dsv.frontend")
public record FrontendConfiguration(String url) { public record FrontendConfiguration(String url) {
public FrontendConfiguration { public FrontendConfiguration {
if (url == null || url.isBlank()) { if (url == null || url.isBlank()) {
throw new IllegalArgumentException("se.su.dsv.frontend.url must not be null or blank"); throw new IllegalArgumentException(
} "se.su.dsv.frontend.url must not be null or blank"
);
} }
}
} }

View File

@ -1,5 +1,6 @@
package se.su.dsv.studentportalen.bff.config; package se.su.dsv.studentportalen.bff.config;
import java.util.List;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; 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 org.springframework.web.cors.CorsConfiguration;
import se.su.dsv.studentportalen.bff.login.BFFAuthenticationEntryPoint; import se.su.dsv.studentportalen.bff.login.BFFAuthenticationEntryPoint;
import java.util.List;
@Configuration @Configuration
public class SecurityConfiguration { public class SecurityConfiguration {
@Bean @Bean
public SecurityFilterChain securityFilterChain( public SecurityFilterChain securityFilterChain(
HttpSecurity http, HttpSecurity http,
FrontendConfiguration frontendConfiguration) FrontendConfiguration frontendConfiguration
throws Exception ) throws Exception {
{ http.exceptionHandling(exception ->
http.exceptionHandling(exception -> exception exception.authenticationEntryPoint(new BFFAuthenticationEntryPoint())
.authenticationEntryPoint(new BFFAuthenticationEntryPoint())); );
http.oauth2Login(login -> login http.oauth2Login(login ->
.defaultSuccessUrl(frontendConfiguration.url(), true)); login.defaultSuccessUrl(frontendConfiguration.url(), true)
http.authorizeHttpRequests(authorize -> authorize );
.requestMatchers("/swagger", "/swagger-ui/**", "/v3/api-docs/**").permitAll() http.authorizeHttpRequests(authorize ->
.anyRequest().authenticated()); authorize
http.cors(cors -> cors .requestMatchers("/swagger", "/swagger-ui/**", "/v3/api-docs/**")
.configurationSource(_ -> frontendOnlyCors(frontendConfiguration))); .permitAll()
http.csrf(csrf -> csrf.spa()); .anyRequest()
return http.build(); .authenticated()
} );
http.cors(cors ->
cors.configurationSource(_ -> frontendOnlyCors(frontendConfiguration))
);
http.csrf(csrf -> csrf.spa());
return http.build();
}
private static CorsConfiguration frontendOnlyCors(FrontendConfiguration frontendConfiguration) { private static CorsConfiguration frontendOnlyCors(
var corsConfiguration = new CorsConfiguration(); FrontendConfiguration frontendConfiguration
corsConfiguration.setAllowedOrigins(List.of(frontendConfiguration.url())); ) {
corsConfiguration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE")); 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 // Allow the frontend to see the X-Authorization-Url header
corsConfiguration.setExposedHeaders(List.of("X-Authorization-Url")); corsConfiguration.setExposedHeaders(List.of("X-Authorization-Url"));
// To allow the session cookie to be included // To allow the session cookie to be included
corsConfiguration.setAllowCredentials(true); corsConfiguration.setAllowCredentials(true);
// Content-Type is allowed by default but with a restriction on the value // 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 // The restriction does not allow "application/json" so we add it as an allowed header
// X-XSRF-TOKEN is needed for CSRF protection // X-XSRF-TOKEN is needed for CSRF protection
corsConfiguration.setAllowedHeaders(List.of("Content-Type", "X-XSRF-TOKEN")); corsConfiguration.setAllowedHeaders(
return corsConfiguration; List.of("Content-Type", "X-XSRF-TOKEN")
} );
return corsConfiguration;
}
} }

View File

@ -16,16 +16,16 @@ import se.su.dsv.studentportalen.bff.service.ProfileService;
@RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE) @RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public class ProfileController { public class ProfileController {
private final ProfileService service; private final ProfileService service;
public ProfileController(ProfileService service) { public ProfileController(ProfileService service) {
this.service = service; this.service = service;
} }
@GetMapping("/profile") @GetMapping("/profile")
public ProfileResponse getProfile( public ProfileResponse getProfile(
@AuthenticationPrincipal(errorOnInvalidType = true) OAuth2User currentUser) @AuthenticationPrincipal(errorOnInvalidType = true) OAuth2User currentUser
{ ) {
return service.getProfile(currentUser); return service.getProfile(currentUser);
} }
} }

View File

@ -1,85 +1,88 @@
package se.su.dsv.studentportalen.bff.controller; 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.context.annotation.Profile;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClient;
import se.su.dsv.studentportalen.bff.config.BackendApiConfiguration; 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 @RestController
@RequestMapping("/test") @RequestMapping("/test")
@Profile("development") @Profile("development")
public class TestController { public class TestController {
private static final int NAME_DELAY_SECONDS = 2; private static final int NAME_DELAY_SECONDS = 2;
private static final int EMAIL_DELAY_SECONDS = 3; private static final int EMAIL_DELAY_SECONDS = 3;
private final RestClient restClient; private final RestClient restClient;
private final BackendApiConfiguration backendApiConfiguration; private final BackendApiConfiguration backendApiConfiguration;
public TestController(final BackendApiConfiguration backendApiConfiguration) { public TestController(final BackendApiConfiguration backendApiConfiguration) {
this.backendApiConfiguration = backendApiConfiguration; this.backendApiConfiguration = backendApiConfiguration;
this.restClient = RestClient.builder() this.restClient = RestClient.builder()
.baseUrl("http://localhost:8080") .baseUrl("http://localhost:8080")
.build(); .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 @RequestMapping("/name")
public String helloWorld() throws InterruptedException, ExecutionException { public String name() throws InterruptedException {
// Pick the scope based on the desired behaviour Thread.sleep(Duration.ofSeconds(NAME_DELAY_SECONDS));
try (var scope = StructuredTaskScope.open()) { return "Greg";
Subtask<String> nameTask = scope.fork(() -> { }
String name = restClient.get()
.uri("/test/name")
.retrieve()
.body(String.class);
return name;
});
Subtask<String> emailTask = scope.fork(() -> { @RequestMapping("/email")
String email = restClient.get() public String email() throws InterruptedException {
.uri("/test/email") Thread.sleep(Duration.ofSeconds(EMAIL_DELAY_SECONDS));
.retrieve() return "greg@localhost";
.body(String.class); }
return email;
});
Subtask<DaisyProfile> daisyProfile = scope.fork(() -> { record DaisyProfile(String name, String mail) {}
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) {}
} }

View File

@ -1,26 +1,25 @@
package se.su.dsv.studentportalen.bff.dto.response; package se.su.dsv.studentportalen.bff.dto.response;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Objects; import java.util.Objects;
/** /**
* User profile information. * User profile information.
*/ */
public record ProfileResponse( public record ProfileResponse(
@JsonProperty(value = "name", required = true) @JsonProperty(value = "name", required = true) String name,
String name,
@JsonProperty(value = "language", required = true) @JsonProperty(value = "language", required = true) Language language
Language language) ) {
{ public enum Language {
public enum Language { @JsonProperty("sv")
@JsonProperty("sv") SWEDISH, SWEDISH,
@JsonProperty("en") ENGLISH @JsonProperty("en")
} ENGLISH,
}
public ProfileResponse { public ProfileResponse {
Objects.requireNonNull(name, "name must be specified"); Objects.requireNonNull(name, "name must be specified");
Objects.requireNonNull(language, "language must be specified"); Objects.requireNonNull(language, "language must be specified");
} }
} }

View File

@ -7,17 +7,18 @@ import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
public class BFFAuthenticationEntryPoint implements AuthenticationEntryPoint { public class BFFAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence( @Override
HttpServletRequest request, public void commence(
HttpServletResponse response, HttpServletRequest request,
AuthenticationException authException) HttpServletResponse response,
{ AuthenticationException authException
String loginUri = ServletUriComponentsBuilder.fromRequest(request) ) {
.replacePath("/oauth2/authorization/studentportalen") String loginUri = ServletUriComponentsBuilder.fromRequest(request)
.build() .replacePath("/oauth2/authorization/studentportalen")
.toUriString(); .build()
response.addHeader("X-Authorization-Url", loginUri); .toUriString();
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.addHeader("X-Authorization-Url", loginUri);
} response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
} }

View File

@ -10,8 +10,8 @@ import se.su.dsv.studentportalen.bff.dto.response.ProfileResponse;
@Service @Service
public class ProfileService { public class ProfileService {
public ProfileResponse getProfile(OAuth2User currentUser) { public ProfileResponse getProfile(OAuth2User currentUser) {
String name = currentUser.getAttribute("name"); String name = currentUser.getAttribute("name");
return new ProfileResponse(name, ProfileResponse.Language.ENGLISH); return new ProfileResponse(name, ProfileResponse.Language.ENGLISH);
} }
} }