Merge remote-tracking branch 'origin/dev' into feat/pagina-om-leerpaden-te-bekijken-#41
# Conflicts: # backend/src/controllers/learning-objects.ts # frontend/src/controllers/base-controller.ts
This commit is contained in:
		
						commit
						99dc346dc1
					
				
					 155 changed files with 3463 additions and 2931 deletions
				
			
		|  | @ -5,13 +5,15 @@ | |||
|     import { computed } from "vue"; | ||||
| 
 | ||||
|     const route = useRoute(); | ||||
|     auth.loadUser(); | ||||
| 
 | ||||
|     interface RouteMeta { | ||||
|         requiresAuth?: boolean; | ||||
|     } | ||||
| 
 | ||||
|     const showMenuBar = computed(() => (route.meta as RouteMeta).requiresAuth && auth.authState.user); | ||||
| 
 | ||||
|     auth.loadUser().catch((_error) => { | ||||
|         // TODO Could not load user! | ||||
|     }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|  |  | |||
|  | @ -26,20 +26,19 @@ | |||
|     ]); | ||||
| 
 | ||||
|     // Logic to change the language of the website to the selected language | ||||
|     const changeLanguage = (langCode: string) => { | ||||
|     function changeLanguage(langCode: string): void { | ||||
|         locale.value = langCode; | ||||
|         localStorage.setItem("user-lang", langCode); | ||||
|     }; | ||||
|     } | ||||
| 
 | ||||
|     // Contains functionality to let the collapsed menu appear and disappear | ||||
|     // When the screen size varies | ||||
|     // Contains functionality to let the collapsed menu appear and disappear when the screen size varies | ||||
|     const drawer = ref(false); | ||||
| 
 | ||||
|     // When the user wants to logout, a popup is shown to verify this | ||||
|     // If verified, the user should be logged out | ||||
|     const performLogout = () => { | ||||
|         auth.logout(); | ||||
|     }; | ||||
|     async function performLogout(): Promise<void> { | ||||
|         await auth.logout(); | ||||
|     } | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|  |  | |||
|  | @ -2,12 +2,12 @@ import { ThemeController } from "@/controllers/themes.ts"; | |||
| import {LearningObjectController} from "@/controllers/learning-objects.ts"; | ||||
| import {LearningPathController} from "@/controllers/learning-paths.ts"; | ||||
| 
 | ||||
| export function controllerGetter<T>(Factory: new () => T): () => T { | ||||
| export function controllerGetter<T>(factory: new () => T): () => T { | ||||
|     let instance: T | undefined; | ||||
| 
 | ||||
|     return (): T => { | ||||
|         if (!instance) { | ||||
|             instance = new Factory(); | ||||
|             instance = new factory(); | ||||
|         } | ||||
|         return instance; | ||||
|     }; | ||||
|  |  | |||
|  | @ -5,12 +5,12 @@ export class ThemeController extends BaseController { | |||
|         super("theme"); | ||||
|     } | ||||
| 
 | ||||
|     getAll(language: string | null = null) { | ||||
|     async getAll(language: string | null = null): Promise<unknown> { | ||||
|         const query = language ? { language } : undefined; | ||||
|         return this.get<any[]>("/", query); | ||||
|         return this.get("/", query); | ||||
|     } | ||||
| 
 | ||||
|     getHruidsByKey(themeKey: string) { | ||||
|     async getHruidsByKey(themeKey: string): Promise<string[]> { | ||||
|         return this.get<string[]>(`/${encodeURIComponent(themeKey)}`); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,22 +1,24 @@ | |||
| import { useQuery } from "@tanstack/vue-query"; | ||||
| import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; | ||||
| import { getThemeController } from "@/controllers/controllers"; | ||||
| import { type MaybeRefOrGetter, toValue } from "vue"; | ||||
| 
 | ||||
| const themeController = getThemeController(); | ||||
| 
 | ||||
| export const useThemeQuery = (language: MaybeRefOrGetter<string>) => | ||||
|     useQuery({ | ||||
| export function useThemeQuery(language: MaybeRefOrGetter<string>): UseQueryReturnType<unknown, Error> { | ||||
|     return useQuery({ | ||||
|         queryKey: ["themes", language], | ||||
|         queryFn: () => { | ||||
|         queryFn: async () => { | ||||
|             const lang = toValue(language); | ||||
|             return themeController.getAll(lang); | ||||
|         }, | ||||
|         enabled: () => Boolean(toValue(language)), | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| export const useThemeHruidsQuery = (themeKey: string | null) => | ||||
|     useQuery({ | ||||
| export function useThemeHruidsQuery(themeKey: string | null): UseQueryReturnType<unknown, Error> { | ||||
|     return useQuery({ | ||||
|         queryKey: ["theme-hruids", themeKey], | ||||
|         queryFn: () => themeController.getHruidsByKey(themeKey!), | ||||
|         queryFn: async () => themeController.getHruidsByKey(themeKey!), | ||||
|         enabled: Boolean(themeKey), | ||||
|     }); | ||||
| } | ||||
|  |  | |||
|  | @ -22,13 +22,13 @@ const router = createRouter({ | |||
|         { | ||||
|             path: "/", | ||||
|             name: "home", | ||||
|             component: () => import("../views/HomePage.vue"), | ||||
|             component: async (): Promise<unknown> => import("../views/HomePage.vue"), | ||||
|             meta: { requiresAuth: false }, | ||||
|         }, | ||||
|         { | ||||
|             path: "/login", | ||||
|             name: "LoginPage", | ||||
|             component: () => import("../views/LoginPage.vue"), | ||||
|             component: async (): Promise<unknown> => import("../views/LoginPage.vue"), | ||||
|             meta: { requiresAuth: false }, | ||||
|         }, | ||||
|         { | ||||
|  | @ -142,7 +142,7 @@ const router = createRouter({ | |||
|     ], | ||||
| }); | ||||
| 
 | ||||
| router.beforeEach(async (to, from, next) => { | ||||
| router.beforeEach(async (to, _from, next) => { | ||||
|     // Verify if user is logged in before accessing certain routes
 | ||||
|     if (to.meta.requiresAuth) { | ||||
|         if (!authState.isLoggedIn.value) { | ||||
|  |  | |||
|  | @ -1,12 +1,13 @@ | |||
| import apiClient from "@/services/api-client/api-client.ts"; | ||||
| import type { FrontendAuthConfig } from "@/services/auth/auth.d.ts"; | ||||
| import type { UserManagerSettings } from "oidc-client-ts"; | ||||
| 
 | ||||
| export const AUTH_CONFIG_ENDPOINT = "auth/config"; | ||||
| 
 | ||||
| /** | ||||
|  * Fetch the authentication configuration from the backend. | ||||
|  */ | ||||
| export async function loadAuthConfig() { | ||||
| export async function loadAuthConfig(): Promise<Record<string, UserManagerSettings>> { | ||||
|     const authConfigResponse = await apiClient.get<FrontendAuthConfig>(AUTH_CONFIG_ENDPOINT); | ||||
|     const authConfig = authConfigResponse.data; | ||||
|     return { | ||||
|  |  | |||
|  | @ -49,7 +49,7 @@ 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() { | ||||
| async function initiateLogin(): Promise<void> { | ||||
|     await router.push(loginRoute); | ||||
| } | ||||
| 
 | ||||
|  | @ -77,20 +77,20 @@ async function handleLoginCallback(): Promise<void> { | |||
| /** | ||||
|  * Refresh an expired authorization token. | ||||
|  */ | ||||
| async function renewToken() { | ||||
| async function renewToken(): Promise<User | null> { | ||||
|     const activeRole = authStorage.getActiveRole(); | ||||
|     if (!activeRole) { | ||||
|         console.log("Can't renew the token: Not logged in!"); | ||||
|         // FIXME console.log("Can't renew the token: Not logged in!");
 | ||||
|         await initiateLogin(); | ||||
|         return; | ||||
|         return null; | ||||
|     } | ||||
|     try { | ||||
|         return await (await getUserManagers())[activeRole].signinSilent(); | ||||
|     } catch (error) { | ||||
|         console.log("Can't renew the token:"); | ||||
|         console.log(error); | ||||
|     } catch (_error) { | ||||
|         // FIXME console.log("Can't renew the token: " + error);
 | ||||
|         await initiateLogin(); | ||||
|     } | ||||
|     return null; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  | @ -113,7 +113,7 @@ apiClient.interceptors.request.use( | |||
|         } | ||||
|         return reqConfig; | ||||
|     }, | ||||
|     (error) => Promise.reject(error), | ||||
|     async (error) => Promise.reject(error), | ||||
| ); | ||||
| 
 | ||||
| // Registering interceptor to refresh the token when a request failed because it was expired.
 | ||||
|  | @ -121,8 +121,8 @@ apiClient.interceptors.response.use( | |||
|     (response) => response, | ||||
|     async (error: AxiosError<{ message?: string }>) => { | ||||
|         if (error.response?.status === 401) { | ||||
|             if (error.response!.data.message === "token_expired") { | ||||
|                 console.log("Access token expired, trying to refresh..."); | ||||
|             if (error.response.data.message === "token_expired") { | ||||
|                 // FIXME console.log("Access token expired, trying to refresh...");
 | ||||
|                 await renewToken(); | ||||
|                 return apiClient(error.config!); // Retry the request
 | ||||
|             } // Apparently, the user got a 401 because he was not logged in yet at all. Redirect him to login.
 | ||||
|  |  | |||
|  | @ -12,7 +12,7 @@ export default { | |||
|      * 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) { | ||||
|     setActiveRole(role: Role): void { | ||||
|         localStorage.setItem("activeRole", role); | ||||
|     }, | ||||
| 
 | ||||
|  | @ -20,7 +20,7 @@ export default { | |||
|      * Remove the saved current role from the local persistent storage. | ||||
|      * This should happen when the user is logged out. | ||||
|      */ | ||||
|     deleteActiveRole() { | ||||
|     deleteActiveRole(): void { | ||||
|         localStorage.removeItem("activeRole"); | ||||
|     }, | ||||
| }; | ||||
|  |  | |||
							
								
								
									
										17
									
								
								frontend/src/services/auth/auth.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										17
									
								
								frontend/src/services/auth/auth.d.ts
									
										
									
									
										vendored
									
									
								
							|  | @ -1,22 +1,25 @@ | |||
| import { type User, UserManager } from "oidc-client-ts"; | ||||
| 
 | ||||
| export type AuthState = { | ||||
| export interface AuthState { | ||||
|     user: User | null; | ||||
|     accessToken: string | null; | ||||
|     activeRole: Role | null; | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| export type FrontendAuthConfig = { | ||||
| export interface FrontendAuthConfig { | ||||
|     student: FrontendIdpConfig; | ||||
|     teacher: FrontendIdpConfig; | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| export type FrontendIdpConfig = { | ||||
| export interface FrontendIdpConfig { | ||||
|     authority: string; | ||||
|     clientId: string; | ||||
|     scope: string; | ||||
|     responseType: string; | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| export type Role = "student" | "teacher"; | ||||
| export type UserManagersForRoles = { student: UserManager; teacher: UserManager }; | ||||
| export interface UserManagersForRoles { | ||||
|     student: UserManager; | ||||
|     teacher: UserManager; | ||||
| } | ||||
|  |  | |||
|  | @ -9,8 +9,8 @@ | |||
|         try { | ||||
|             await auth.handleLoginCallback(); | ||||
|             await router.replace("/user"); // Redirect to theme page | ||||
|         } catch (error) { | ||||
|             console.error("OIDC callback error:", error); | ||||
|         } catch (_error) { | ||||
|             // FIXME console.error("OIDC callback error:", error); | ||||
|         } | ||||
|     }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -15,10 +15,10 @@ | |||
|     ]); | ||||
| 
 | ||||
|     // Logic to change the language of the website to the selected language | ||||
|     const changeLanguage = (langCode: string) => { | ||||
|     function changeLanguage(langCode: string): void { | ||||
|         locale.value = langCode; | ||||
|         localStorage.setItem("user-lang", langCode); | ||||
|     }; | ||||
|     } | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|  |  | |||
|  | @ -2,16 +2,16 @@ | |||
|     import dwengoLogo from "../../../assets/img/dwengo-groen-zwart.svg"; | ||||
|     import auth from "@/services/auth/auth-service.ts"; | ||||
| 
 | ||||
|     function loginAsStudent() { | ||||
|         auth.loginAs("student"); | ||||
|     async function loginAsStudent(): Promise<void> { | ||||
|         await auth.loginAs("student"); | ||||
|     } | ||||
| 
 | ||||
|     function loginAsTeacher() { | ||||
|         auth.loginAs("teacher"); | ||||
|     async function loginAsTeacher(): Promise<void> { | ||||
|         await auth.loginAs("teacher"); | ||||
|     } | ||||
| 
 | ||||
|     function performLogout() { | ||||
|         auth.logout(); | ||||
|     async function performLogout(): Promise<void> { | ||||
|         await auth.logout(); | ||||
|     } | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger