diff --git a/frontend/src/services/auth/auth-api-client-interceptors.ts b/frontend/src/services/auth/auth-api-client-interceptors.ts deleted file mode 100644 index 938eaf92..00000000 --- a/frontend/src/services/auth/auth-api-client-interceptors.ts +++ /dev/null @@ -1,24 +0,0 @@ -import apiClient from "@/services/api-client.ts"; -import type {AuthState} from "@/services/auth/auth-types.ts"; - -export function configureApiClientAuthInterceptors(authState: AuthState, renewToken: () => Promise) { - apiClient.interceptors.request.use(async (reqConfig) => { - const token = authState?.user?.access_token; - if (token) { - reqConfig.headers.Authorization = `Bearer ${token}`; - } - return reqConfig; - }, (error) => Promise.reject(error)); - - apiClient.interceptors.response.use( - response => response, - async (error) => { - if (error.response?.status === 401) { - console.log("Access token expired, trying to refresh..."); - await renewToken(); - return apiClient(error.config); // Retry the request - } - return Promise.reject(error); - } - ); -} diff --git a/frontend/src/services/auth/auth-config-loader.ts b/frontend/src/services/auth/auth-config-loader.ts index e1792678..ff46ffe5 100644 --- a/frontend/src/services/auth/auth-config-loader.ts +++ b/frontend/src/services/auth/auth-config-loader.ts @@ -1,6 +1,9 @@ import apiClient from "@/services/api-client.ts"; import type {FrontendAuthConfig} from "@/services/auth/auth-types.ts"; +/** + * Fetch the authentication configuration from the backend. + */ export async function loadAuthConfig() { const authConfig = (await apiClient.get("auth/config")).data; return { diff --git a/frontend/src/services/auth/auth-service.ts b/frontend/src/services/auth/auth-service.ts index f6299c71..76d3b6dc 100644 --- a/frontend/src/services/auth/auth-service.ts +++ b/frontend/src/services/auth/auth-service.ts @@ -6,16 +6,22 @@ import {computed, reactive} from "vue"; import type {AuthState, Role, UserManagersForRoles} from "@/services/auth/auth-types.ts"; import {User, UserManager} from "oidc-client-ts"; import {loadAuthConfig} from "@/services/auth/auth-config-loader.ts"; -import {configureApiClientAuthInterceptors} from "@/services/auth/auth-api-client-interceptors.ts"; import authStorage from "./auth-storage.ts" +import {useRouter} from "vue-router"; +import {loginRoute} from "@/config.ts"; +import apiClient from "@/services/api-client.ts"; const authConfig = await loadAuthConfig(); +const router = useRouter(); const userManagers: UserManagersForRoles = { student: new UserManager(authConfig.student), teacher: new UserManager(authConfig.teacher), }; +/** + * Load the information about who is currently logged in from the IDP. + */ async function loadUser(): Promise { const activeRole = authStorage.getActiveRole(); if (!activeRole) { @@ -28,6 +34,9 @@ async function loadUser(): Promise { return user; } +/** + * Information about the current authentication state. + */ const authState = reactive({ user: null, accessToken: null, @@ -36,34 +45,56 @@ const authState = reactive({ const isLoggedIn = computed(() => authState.user !== null); +/** + * Redirect the user to the login page where he/she can choose whether to log in as a student or teacher. + */ +async function initiateLogin() { + await router.push(loginRoute); +} + +/** + * Redirect the user to the IDP for the given role so that he can log in there. + * Only call this function when the user is not logged in yet! + */ async function loginAs(role: Role): Promise { // Storing it in local storage so that it won't be lost when redirecting outside of the app. authStorage.setActiveRole(role); await userManagers[role].signinRedirect(); } +/** + * To be called when the user is redirected to the callback-endpoint by the IDP after a successful login. + */ async function handleLoginCallback(): Promise { const activeRole = authStorage.getActiveRole(); if (!activeRole) { - throw new Error("Can't renew the token: Not logged in!"); + throw new Error("Login callback received, but the user is not logging in!"); } authState.user = await userManagers[activeRole].signinCallback() || null; } +/** + * Refresh an expired authorization token. + */ async function renewToken() { const activeRole = authStorage.getActiveRole(); if (!activeRole) { - throw new Error("Can't renew the token: Not logged in!"); + console.log("Can't renew the token: Not logged in!"); + await initiateLogin(); + return; } try { return await userManagers[activeRole].signinSilent(); } catch (error) { console.log("Can't renew the token:"); console.log(error); - await loginAs(activeRole); + await initiateLogin(); } } +/** + * End the session of the current user. + */ async function logout(): Promise { const activeRole = authStorage.getActiveRole(); if (activeRole) { @@ -72,6 +103,26 @@ async function logout(): Promise { } } -configureApiClientAuthInterceptors(authState, renewToken); +// Registering interceptor to add the authorization header to each request when the user is logged in. +apiClient.interceptors.request.use(async (reqConfig) => { + const token = authState?.user?.access_token; + if (token) { + reqConfig.headers.Authorization = `Bearer ${token}`; + } + return reqConfig; +}, (error) => Promise.reject(error)); -export default {authState, isLoggedIn, loadUser, handleLoginCallback, loginAs, logout}; +// Registering interceptor to refresh the token when a request failed because it was expired. +apiClient.interceptors.response.use( + response => response, + async (error) => { + if (error.response?.status === 401) { + console.log("Access token expired, trying to refresh..."); + await renewToken(); + return apiClient(error.config); // Retry the request + } + return Promise.reject(error); + } +); + +export default {authState, isLoggedIn, initiateLogin, loadUser, handleLoginCallback, loginAs, logout}; diff --git a/frontend/src/services/auth/auth-storage.ts b/frontend/src/services/auth/auth-storage.ts index 2a31832a..f41b6d46 100644 --- a/frontend/src/services/auth/auth-storage.ts +++ b/frontend/src/services/auth/auth-storage.ts @@ -1,12 +1,25 @@ import type {Role} from "@/services/auth/auth-types.ts"; export default { + /** + * Get the role the user is currently logged in as from the local persistent storage. + */ getActiveRole(): Role | undefined { return localStorage.getItem("activeRole") as Role | undefined; }, + + /** + * Set the role the user is currently logged in as from the local persistent storage. + * This should happen when the user logs in with another account. + */ setActiveRole(role: Role) { localStorage.setItem("activeRole", role); }, + + /** + * Remove the saved current role from the local persistent storage. + * This should happen when the user is logged out. + */ deleteActiveRole() { localStorage.removeItem("activeRole"); }