diff --git a/bff/pom.xml b/bff/pom.xml
index d5ac45a..9786d17 100644
--- a/bff/pom.xml
+++ b/bff/pom.xml
@@ -7,7 +7,7 @@
org.springframework.boot
spring-boot-starter-parent
- 3.4.4
+ 4.0.1
diff --git a/bff/src/main/java/se/su/dsv/studentportalen/bff/Studentportalen.java b/bff/src/main/java/se/su/dsv/studentportalen/bff/Studentportalen.java
index 6f92487..19d9281 100644
--- a/bff/src/main/java/se/su/dsv/studentportalen/bff/Studentportalen.java
+++ b/bff/src/main/java/se/su/dsv/studentportalen/bff/Studentportalen.java
@@ -5,64 +5,13 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
-import org.springframework.context.annotation.Bean;
-import org.springframework.security.config.annotation.web.builders.HttpSecurity;
-import org.springframework.security.web.SecurityFilterChain;
-import org.springframework.security.web.util.matcher.RequestMatcher;
-import org.springframework.security.web.util.matcher.RequestMatchers;
-import org.springframework.web.cors.CorsConfiguration;
-import se.su.dsv.studentportalen.bff.login.BFFAuthenticationEntryPoint;
-
-import java.util.List;
-
-import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;
@SpringBootApplication
@EnableConfigurationProperties
@ConfigurationPropertiesScan
public class Studentportalen extends SpringBootServletInitializer {
- private static final RequestMatcher DOCUMENTATION_MATCHER = RequestMatchers.anyOf(
- antMatcher("/swagger"),
- antMatcher("/swagger-ui/**"),
- antMatcher("/v3/api-docs/**"));
-
public static void main(String[] args) {
SpringApplication.run(Studentportalen.class, args);
}
-
- @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(DOCUMENTATION_MATCHER).permitAll()
- .anyRequest().authenticated());
- http.cors(cors -> cors
- .configurationSource(_ -> frontendOnlyCors(frontendConfiguration)));
- 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"));
-
- // 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);
-
- // 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
- corsConfiguration.setAllowedHeaders(List.of("Content-Type"));
- return corsConfiguration;
- }
}
diff --git a/bff/src/main/java/se/su/dsv/studentportalen/bff/BackendApiConfiguration.java b/bff/src/main/java/se/su/dsv/studentportalen/bff/config/BackendApiConfiguration.java
similarity index 80%
rename from bff/src/main/java/se/su/dsv/studentportalen/bff/BackendApiConfiguration.java
rename to bff/src/main/java/se/su/dsv/studentportalen/bff/config/BackendApiConfiguration.java
index 47eba93..a11ff1f 100644
--- a/bff/src/main/java/se/su/dsv/studentportalen/bff/BackendApiConfiguration.java
+++ b/bff/src/main/java/se/su/dsv/studentportalen/bff/config/BackendApiConfiguration.java
@@ -1,4 +1,4 @@
-package se.su.dsv.studentportalen.bff;
+package se.su.dsv.studentportalen.bff.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
diff --git a/bff/src/main/java/se/su/dsv/studentportalen/bff/FrontendConfiguration.java b/bff/src/main/java/se/su/dsv/studentportalen/bff/config/FrontendConfiguration.java
similarity index 88%
rename from bff/src/main/java/se/su/dsv/studentportalen/bff/FrontendConfiguration.java
rename to bff/src/main/java/se/su/dsv/studentportalen/bff/config/FrontendConfiguration.java
index b6aea15..ad670db 100644
--- a/bff/src/main/java/se/su/dsv/studentportalen/bff/FrontendConfiguration.java
+++ b/bff/src/main/java/se/su/dsv/studentportalen/bff/config/FrontendConfiguration.java
@@ -1,4 +1,4 @@
-package se.su.dsv.studentportalen.bff;
+package se.su.dsv.studentportalen.bff.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
diff --git a/bff/src/main/java/se/su/dsv/studentportalen/bff/config/SecurityConfiguration.java b/bff/src/main/java/se/su/dsv/studentportalen/bff/config/SecurityConfiguration.java
new file mode 100644
index 0000000..d188365
--- /dev/null
+++ b/bff/src/main/java/se/su/dsv/studentportalen/bff/config/SecurityConfiguration.java
@@ -0,0 +1,51 @@
+package se.su.dsv.studentportalen.bff.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+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();
+ }
+
+ 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"));
+
+ // 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;
+ }
+}
diff --git a/bff/src/main/java/se/su/dsv/studentportalen/bff/frontend/profile/ProfileController.java b/bff/src/main/java/se/su/dsv/studentportalen/bff/controller/ProfileController.java
similarity index 57%
rename from bff/src/main/java/se/su/dsv/studentportalen/bff/frontend/profile/ProfileController.java
rename to bff/src/main/java/se/su/dsv/studentportalen/bff/controller/ProfileController.java
index 4483c71..eb97bd2 100644
--- a/bff/src/main/java/se/su/dsv/studentportalen/bff/frontend/profile/ProfileController.java
+++ b/bff/src/main/java/se/su/dsv/studentportalen/bff/controller/ProfileController.java
@@ -1,4 +1,4 @@
-package se.su.dsv.studentportalen.bff.frontend.profile;
+package se.su.dsv.studentportalen.bff.controller;
import org.springframework.http.MediaType;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
@@ -6,14 +6,26 @@ import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
+import se.su.dsv.studentportalen.bff.dto.response.ProfileResponse;
+import se.su.dsv.studentportalen.bff.service.ProfileService;
+/**
+ * REST controller for user profile operations.
+ */
@RestController
@RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public class ProfileController {
+
+ private final ProfileService service;
+
+ public ProfileController(ProfileService service) {
+ this.service = service;
+ }
+
@GetMapping("/profile")
- public Profile getProfile(
+ public ProfileResponse getProfile(
@AuthenticationPrincipal(errorOnInvalidType = true) OAuth2User currentUser)
{
- return new Profile(currentUser.getAttribute("name"), Profile.Language.ENGLISH);
+ return service.getProfile(currentUser);
}
}
diff --git a/bff/src/main/java/se/su/dsv/studentportalen/bff/TestController.java b/bff/src/main/java/se/su/dsv/studentportalen/bff/controller/TestController.java
similarity index 88%
rename from bff/src/main/java/se/su/dsv/studentportalen/bff/TestController.java
rename to bff/src/main/java/se/su/dsv/studentportalen/bff/controller/TestController.java
index 950a214..b977a13 100644
--- a/bff/src/main/java/se/su/dsv/studentportalen/bff/TestController.java
+++ b/bff/src/main/java/se/su/dsv/studentportalen/bff/controller/TestController.java
@@ -1,9 +1,10 @@
-package se.su.dsv.studentportalen.bff;
+package se.su.dsv.studentportalen.bff.controller;
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;
@@ -15,6 +16,9 @@ import java.util.concurrent.StructuredTaskScope.Subtask;
@Profile("development")
public class TestController {
+ private static final int NAME_DELAY_SECONDS = 2;
+ private static final int EMAIL_DELAY_SECONDS = 3;
+
private final RestClient restClient;
private final BackendApiConfiguration backendApiConfiguration;
@@ -67,13 +71,13 @@ public class TestController {
@RequestMapping("/name")
public String name() throws InterruptedException {
- Thread.sleep(Duration.ofSeconds(2));
+ Thread.sleep(Duration.ofSeconds(NAME_DELAY_SECONDS));
return "Greg";
}
@RequestMapping("/email")
public String email() throws InterruptedException {
- Thread.sleep(Duration.ofSeconds(3));
+ Thread.sleep(Duration.ofSeconds(EMAIL_DELAY_SECONDS));
return "greg@localhost";
}
diff --git a/bff/src/main/java/se/su/dsv/studentportalen/bff/frontend/package-info.java b/bff/src/main/java/se/su/dsv/studentportalen/bff/controller/package-info.java
similarity index 53%
rename from bff/src/main/java/se/su/dsv/studentportalen/bff/frontend/package-info.java
rename to bff/src/main/java/se/su/dsv/studentportalen/bff/controller/package-info.java
index 476dc55..4315538 100644
--- a/bff/src/main/java/se/su/dsv/studentportalen/bff/frontend/package-info.java
+++ b/bff/src/main/java/se/su/dsv/studentportalen/bff/controller/package-info.java
@@ -1,7 +1,6 @@
-/// Everything related to the API used by the frontend
@NonNullApi
@NonNullFields
-package se.su.dsv.studentportalen.bff.frontend;
+package se.su.dsv.studentportalen.bff.controller;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;
diff --git a/bff/src/main/java/se/su/dsv/studentportalen/bff/frontend/profile/Profile.java b/bff/src/main/java/se/su/dsv/studentportalen/bff/dto/response/ProfileResponse.java
similarity index 53%
rename from bff/src/main/java/se/su/dsv/studentportalen/bff/frontend/profile/Profile.java
rename to bff/src/main/java/se/su/dsv/studentportalen/bff/dto/response/ProfileResponse.java
index f0be027..a5c655c 100644
--- a/bff/src/main/java/se/su/dsv/studentportalen/bff/frontend/profile/Profile.java
+++ b/bff/src/main/java/se/su/dsv/studentportalen/bff/dto/response/ProfileResponse.java
@@ -1,19 +1,25 @@
-package se.su.dsv.studentportalen.bff.frontend.profile;
+package se.su.dsv.studentportalen.bff.dto.response;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Objects;
-public record Profile(
- @JsonProperty(value = "name", required = true) String name,
- @JsonProperty(value = "language", required = true) Language language)
+/**
+ * User profile information.
+ */
+public record ProfileResponse(
+ @JsonProperty(value = "name", required = true)
+ String name,
+
+ @JsonProperty(value = "language", required = true)
+ Language language)
{
public enum Language {
@JsonProperty("sv") SWEDISH,
@JsonProperty("en") ENGLISH
}
- public Profile {
+ public ProfileResponse {
Objects.requireNonNull(name, "name must be specified");
Objects.requireNonNull(language, "language must be specified");
}
diff --git a/bff/src/main/java/se/su/dsv/studentportalen/bff/dto/response/package-info.java b/bff/src/main/java/se/su/dsv/studentportalen/bff/dto/response/package-info.java
new file mode 100644
index 0000000..238166f
--- /dev/null
+++ b/bff/src/main/java/se/su/dsv/studentportalen/bff/dto/response/package-info.java
@@ -0,0 +1,6 @@
+@NonNullApi
+@NonNullFields
+package se.su.dsv.studentportalen.bff.dto.response;
+
+import org.springframework.lang.NonNullApi;
+import org.springframework.lang.NonNullFields;
diff --git a/bff/src/main/java/se/su/dsv/studentportalen/bff/login/BFFAuthenticationEntryPoint.java b/bff/src/main/java/se/su/dsv/studentportalen/bff/login/BFFAuthenticationEntryPoint.java
index 32290fe..9992289 100644
--- a/bff/src/main/java/se/su/dsv/studentportalen/bff/login/BFFAuthenticationEntryPoint.java
+++ b/bff/src/main/java/se/su/dsv/studentportalen/bff/login/BFFAuthenticationEntryPoint.java
@@ -8,7 +8,11 @@ import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
public class BFFAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
- public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
+ public void commence(
+ HttpServletRequest request,
+ HttpServletResponse response,
+ AuthenticationException authException)
+ {
String loginUri = ServletUriComponentsBuilder.fromRequest(request)
.replacePath("/oauth2/authorization/studentportalen")
.build()
diff --git a/bff/src/main/java/se/su/dsv/studentportalen/bff/login/package-info.java b/bff/src/main/java/se/su/dsv/studentportalen/bff/login/package-info.java
index ad2e5cc..986d63e 100644
--- a/bff/src/main/java/se/su/dsv/studentportalen/bff/login/package-info.java
+++ b/bff/src/main/java/se/su/dsv/studentportalen/bff/login/package-info.java
@@ -1,5 +1,6 @@
/// This package contains the classes and logic for handling the login process
-/// described in section 6.1 of [OAuth 2.0 for Browser-Based Applications](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps#name-backend-for-frontend-bff).
+/// described in section 6.1 of OAuth 2.0 for Browser-Based Applications.
+/// See: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps
///
/// The frontend running in the browser will attempt to make a request to the
/// backend-for-frontend (BFF). If the client is not already authenticated (by
diff --git a/bff/src/main/java/se/su/dsv/studentportalen/bff/service/ProfileService.java b/bff/src/main/java/se/su/dsv/studentportalen/bff/service/ProfileService.java
new file mode 100644
index 0000000..a21963c
--- /dev/null
+++ b/bff/src/main/java/se/su/dsv/studentportalen/bff/service/ProfileService.java
@@ -0,0 +1,17 @@
+package se.su.dsv.studentportalen.bff.service;
+
+import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.stereotype.Service;
+import se.su.dsv.studentportalen.bff.dto.response.ProfileResponse;
+
+/**
+ * Service for user profile operations.
+ */
+@Service
+public class ProfileService {
+
+ public ProfileResponse getProfile(OAuth2User currentUser) {
+ String name = currentUser.getAttribute("name");
+ return new ProfileResponse(name, ProfileResponse.Language.ENGLISH);
+ }
+}
diff --git a/bff/src/main/java/se/su/dsv/studentportalen/bff/service/package-info.java b/bff/src/main/java/se/su/dsv/studentportalen/bff/service/package-info.java
new file mode 100644
index 0000000..8740265
--- /dev/null
+++ b/bff/src/main/java/se/su/dsv/studentportalen/bff/service/package-info.java
@@ -0,0 +1,6 @@
+@NonNullApi
+@NonNullFields
+package se.su.dsv.studentportalen.bff.service;
+
+import org.springframework.lang.NonNullApi;
+import org.springframework.lang.NonNullFields;
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 77e508c..195cbb8 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,5 +1,7 @@
+import { useEffect } from "react";
import "./App.css";
import suLogoLandscape from "./assets/SU_logo_optimized.svg";
+import { setLanguagePreference } from "./hooks/backend.ts";
import { ProfileContext } from "./hooks/profile.ts";
import Studentportalen from "./Studentportalen.tsx";
import { useViewTransitioningFetch } from "./hooks/fetch";
@@ -7,6 +9,10 @@ import { useViewTransitioningFetch } from "./hooks/fetch";
function App() {
const { data: profile, error } = useViewTransitioningFetch("/profile");
+ useEffect(() => {
+ setLanguagePreference(profile?.language);
+ }, [profile?.language]);
+
if (!profile) {
return splashScreen("Loading...");
}
diff --git a/frontend/src/hooks/backend.ts b/frontend/src/hooks/backend.ts
index e5f4bcc..22242e0 100644
--- a/frontend/src/hooks/backend.ts
+++ b/frontend/src/hooks/backend.ts
@@ -1,6 +1,12 @@
import createClient, { Middleware } from "openapi-fetch";
import type { paths } from "../lib/api";
+let languagePreference: string | undefined;
+
+export function setLanguagePreference(language: string | undefined) {
+ languagePreference = language;
+}
+
const client = createClient({
baseUrl: import.meta.env.VITE_BACKEND_URL,
});
@@ -11,6 +17,33 @@ const includeCredentials: Middleware = {
},
};
+function getCookie(name: string): string | undefined {
+ const match = document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`));
+ return match ? match[2] : undefined;
+}
+
+const includeCsrfToken: Middleware = {
+ onRequest({ request }) {
+ const method = request.method.toUpperCase();
+ if (method === "POST" || method === "PUT" || method === "DELETE") {
+ const csrfToken = getCookie("XSRF-TOKEN");
+ if (csrfToken) {
+ request.headers.set("X-XSRF-TOKEN", csrfToken);
+ }
+ }
+ return request;
+ },
+};
+
+const includeAcceptLanguage: Middleware = {
+ onRequest({ request }) {
+ const language =
+ languagePreference ?? navigator.language.split("-")[0] ?? "en";
+ request.headers.set("Accept-Language", language);
+ return request;
+ },
+};
+
const initiateAuthorizationOnUnauthorized: Middleware = {
onResponse({ response }) {
if (response.status === 401) {
@@ -24,6 +57,8 @@ const initiateAuthorizationOnUnauthorized: Middleware = {
};
client.use(includeCredentials);
+client.use(includeCsrfToken);
+client.use(includeAcceptLanguage);
client.use(initiateAuthorizationOnUnauthorized);
export function useBackend() {