Merge branch 'dev' into docs/swagger-autogen
This commit is contained in:
		
						commit
						5986ca57bf
					
				
					 189 changed files with 6160 additions and 1581 deletions
				
			
		
							
								
								
									
										36
									
								
								frontend/Dockerfile
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								frontend/Dockerfile
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,36 @@ | |||
| FROM node:22 AS build-stage | ||||
| 
 | ||||
| # install simple http server for serving static content | ||||
| RUN npm install -g http-server | ||||
| 
 | ||||
| WORKDIR /app | ||||
| 
 | ||||
| # Install dependencies | ||||
| 
 | ||||
| COPY package*.json ./ | ||||
| COPY ./frontend/package.json ./frontend/ | ||||
| 
 | ||||
| RUN npm install --silent | ||||
| 
 | ||||
| # Build the frontend | ||||
| 
 | ||||
| # Root tsconfig.json | ||||
| COPY tsconfig.json ./ | ||||
| COPY assets ./assets/ | ||||
| 
 | ||||
| WORKDIR /app/frontend | ||||
| 
 | ||||
| COPY frontend ./ | ||||
| 
 | ||||
| RUN npx vite build | ||||
| 
 | ||||
| FROM nginx:stable AS production-stage | ||||
| 
 | ||||
| COPY config/nginx/nginx.conf /etc/nginx/nginx.conf | ||||
| 
 | ||||
| COPY --from=build-stage /app/assets /usr/share/nginx/html/assets | ||||
| COPY --from=build-stage /app/frontend/dist /usr/share/nginx/html | ||||
| 
 | ||||
| EXPOSE 8080 | ||||
| 
 | ||||
| CMD ["nginx", "-g", "daemon off;"] | ||||
|  | @ -14,6 +14,9 @@ const vueConfig = defineConfigWithVueTs( | |||
|     { | ||||
|         name: "app/files-to-lint", | ||||
|         files: ["**/*.{ts,mts,tsx,vue}"], | ||||
|         rules: { | ||||
|             "no-useless-assignment": "off", // Depend on `no-unused-vars` to catch this
 | ||||
|         }, | ||||
|     }, | ||||
| 
 | ||||
|     { | ||||
|  |  | |||
|  | @ -17,10 +17,11 @@ | |||
|     }, | ||||
|     "dependencies": { | ||||
|         "vue": "^3.5.13", | ||||
|         "vue-i18n": "^11.1.2", | ||||
|         "vue-router": "^4.5.0", | ||||
|         "vuetify": "^3.7.12", | ||||
|         "oidc-client-ts": "^3.1.0", | ||||
|         "axios": "^1.8.1" | ||||
|         "axios": "^1.8.2" | ||||
|     }, | ||||
|     "devDependencies": { | ||||
|         "@playwright/test": "^1.50.1", | ||||
|  |  | |||
|  | @ -2,8 +2,10 @@ | |||
|     import { ref } from "vue"; | ||||
|     import { useRoute } from "vue-router"; | ||||
|     import dwengoLogo from "../../../assets/img/dwengo-groen-zwart.svg"; | ||||
|     import { useI18n } from "vue-i18n"; | ||||
| 
 | ||||
|     const route = useRoute(); | ||||
|     const { t, locale } = useI18n(); | ||||
| 
 | ||||
|     // Instantiate variables to use in html to render right | ||||
|     // Links and content dependent on the role (student or teacher) | ||||
|  | @ -27,6 +29,8 @@ | |||
| 
 | ||||
|     // Logic to change the language of the website to the selected language | ||||
|     const changeLanguage = (langCode: string) => { | ||||
|         locale.value = langCode; | ||||
|         localStorage.setItem("user-lang", langCode); | ||||
|         console.log(langCode); | ||||
|     }; | ||||
| </script> | ||||
|  | @ -46,7 +50,7 @@ | |||
|                                 :src="dwengoLogo" | ||||
|                             /> | ||||
|                             <p class="caption"> | ||||
|                                 {{ role }} | ||||
|                                 {{ t(`${role}`) }} | ||||
|                             </p> | ||||
|                         </router-link> | ||||
|                     </li> | ||||
|  | @ -55,22 +59,22 @@ | |||
|                             :to="`/${role}/${userId}/assignment`" | ||||
|                             class="menu_item" | ||||
|                         > | ||||
|                             assignments | ||||
|                             {{ t("assignments") }} | ||||
|                         </router-link> | ||||
|                     </li> | ||||
|                     <li> | ||||
|                         <router-link | ||||
|                             :to="`/${role}/${userId}/class`" | ||||
|                             class="menu_item" | ||||
|                             >classes</router-link | ||||
|                             >{{ t("classes") }}</router-link | ||||
|                         > | ||||
|                     </li> | ||||
|                     <li> | ||||
|                         <router-link | ||||
|                             :to="`/${role}/${userId}/discussion`" | ||||
|                             class="menu_item" | ||||
|                             >discussions</router-link | ||||
|                         > | ||||
|                             >{{ t("discussions") }} | ||||
|                         </router-link> | ||||
|                     </li> | ||||
|                     <li> | ||||
|                         <v-menu open-on-hover> | ||||
|  | @ -104,7 +108,7 @@ | |||
|                 <li> | ||||
|                     <router-link :to="`/login`"> | ||||
|                         <v-tooltip | ||||
|                             text="log out" | ||||
|                             :text="t('logout')" | ||||
|                             location="bottom" | ||||
|                         > | ||||
|                             <template v-slot:activator="{ props }"> | ||||
|  |  | |||
|  | @ -1,5 +1,8 @@ | |||
| export const apiConfig = { | ||||
|     baseUrl: window.location.hostname == "localhost" ? "http://localhost:3000" : window.location.origin, | ||||
|     baseUrl: | ||||
|         window.location.hostname === "localhost" && !(window.location.port === "80" || window.location.port === "") | ||||
|             ? "http://localhost:3000/api" | ||||
|             : window.location.origin + "/api", | ||||
| }; | ||||
| 
 | ||||
| export const loginRoute = "/login"; | ||||
|  |  | |||
							
								
								
									
										22
									
								
								frontend/src/i18n/i18n.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								frontend/src/i18n/i18n.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,22 @@ | |||
| import { createI18n } from "vue-i18n"; | ||||
| 
 | ||||
| // Import translations
 | ||||
| import en from "@/i18n/locale/en.json"; | ||||
| import nl from "@/i18n/locale/nl.json"; | ||||
| import fr from "@/i18n/locale/fr.json"; | ||||
| import de from "@/i18n/locale/de.json"; | ||||
| 
 | ||||
| const savedLocale = localStorage.getItem("user-lang") || "en"; | ||||
| 
 | ||||
| const i18n = createI18n({ | ||||
|     locale: savedLocale, | ||||
|     fallbackLocale: "en", | ||||
|     messages: { | ||||
|         en: en, | ||||
|         nl: nl, | ||||
|         fr: fr, | ||||
|         de: de, | ||||
|     }, | ||||
| }); | ||||
| 
 | ||||
| export default i18n; | ||||
							
								
								
									
										3
									
								
								frontend/src/i18n/locale/de.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/src/i18n/locale/de.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| { | ||||
|     "welcome": "Willkommen" | ||||
| } | ||||
							
								
								
									
										9
									
								
								frontend/src/i18n/locale/en.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								frontend/src/i18n/locale/en.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | |||
| { | ||||
|     "welcome": "Welcome", | ||||
|     "student": "student", | ||||
|     "teacher": "teacher", | ||||
|     "assignments": "assignments", | ||||
|     "classes": "classes", | ||||
|     "discussions": "discussions", | ||||
|     "logout": "log out" | ||||
| } | ||||
							
								
								
									
										3
									
								
								frontend/src/i18n/locale/fr.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/src/i18n/locale/fr.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| { | ||||
|     "welcome": "Bienvenue" | ||||
| } | ||||
							
								
								
									
										9
									
								
								frontend/src/i18n/locale/nl.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								frontend/src/i18n/locale/nl.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | |||
| { | ||||
|     "welcome": "Welkom", | ||||
|     "student": "leerling", | ||||
|     "teacher": "leerkracht", | ||||
|     "assignments": "opdrachten", | ||||
|     "classes": "klassen", | ||||
|     "discussions": "discussies", | ||||
|     "logout": "log uit" | ||||
| } | ||||
|  | @ -5,6 +5,7 @@ import "vuetify/styles"; | |||
| import { createVuetify } from "vuetify"; | ||||
| import * as components from "vuetify/components"; | ||||
| import * as directives from "vuetify/directives"; | ||||
| import i18n from "./i18n/i18n.ts"; | ||||
| 
 | ||||
| // Components
 | ||||
| import App from "./App.vue"; | ||||
|  | @ -24,5 +25,5 @@ const vuetify = createVuetify({ | |||
|     directives, | ||||
| }); | ||||
| app.use(vuetify); | ||||
| 
 | ||||
| app.use(i18n); | ||||
| app.mount("#app"); | ||||
|  |  | |||
|  | @ -23,16 +23,12 @@ const router = createRouter({ | |||
|         { | ||||
|             path: "/", | ||||
|             name: "home", | ||||
|             component: () => { | ||||
|                 return import("../views/HomePage.vue"); | ||||
|             }, | ||||
|             component: () => import("../views/HomePage.vue"), | ||||
|         }, | ||||
|         { | ||||
|             path: "/login", | ||||
|             name: "LoginPage", | ||||
|             component: () => { | ||||
|                 return import("../views/LoginPage.vue"); | ||||
|             }, | ||||
|             component: () => import("../views/LoginPage.vue"), | ||||
|         }, | ||||
|         { | ||||
|             path: "/callback", | ||||
|  |  | |||
|  | @ -12,12 +12,13 @@ import apiClient from "@/services/api-client.ts"; | |||
| import router from "@/router"; | ||||
| import type { AxiosError } from "axios"; | ||||
| 
 | ||||
| const authConfig = await loadAuthConfig(); | ||||
| 
 | ||||
| const userManagers: UserManagersForRoles = { | ||||
|     student: new UserManager(authConfig.student), | ||||
|     teacher: new UserManager(authConfig.teacher), | ||||
| }; | ||||
| async function getUserManagers(): Promise<UserManagersForRoles> { | ||||
|     const authConfig = await loadAuthConfig(); | ||||
|     return { | ||||
|         student: new UserManager(authConfig.student), | ||||
|         teacher: new UserManager(authConfig.teacher), | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Load the information about who is currently logged in from the IDP. | ||||
|  | @ -27,7 +28,7 @@ async function loadUser(): Promise<User | null> { | |||
|     if (!activeRole) { | ||||
|         return null; | ||||
|     } | ||||
|     const user = await userManagers[activeRole].getUser(); | ||||
|     const user = await (await getUserManagers())[activeRole].getUser(); | ||||
|     authState.user = user; | ||||
|     authState.accessToken = user?.access_token || null; | ||||
|     authState.activeRole = activeRole || null; | ||||
|  | @ -43,9 +44,7 @@ const authState = reactive<AuthState>({ | |||
|     activeRole: authStorage.getActiveRole() || null, | ||||
| }); | ||||
| 
 | ||||
| const isLoggedIn = computed(() => { | ||||
|     return authState.user !== null; | ||||
| }); | ||||
| 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. | ||||
|  | @ -61,7 +60,7 @@ async function initiateLogin() { | |||
| async function loginAs(role: Role): Promise<void> { | ||||
|     // 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(); | ||||
|     await (await getUserManagers())[role].signinRedirect(); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  | @ -72,7 +71,7 @@ async function handleLoginCallback(): Promise<void> { | |||
|     if (!activeRole) { | ||||
|         throw new Error("Login callback received, but the user is not logging in!"); | ||||
|     } | ||||
|     authState.user = (await userManagers[activeRole].signinCallback()) || null; | ||||
|     authState.user = (await (await getUserManagers())[activeRole].signinCallback()) || null; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  | @ -86,7 +85,7 @@ async function renewToken() { | |||
|         return; | ||||
|     } | ||||
|     try { | ||||
|         return await userManagers[activeRole].signinSilent(); | ||||
|         return await (await getUserManagers())[activeRole].signinSilent(); | ||||
|     } catch (error) { | ||||
|         console.log("Can't renew the token:"); | ||||
|         console.log(error); | ||||
|  | @ -100,7 +99,7 @@ async function renewToken() { | |||
| async function logout(): Promise<void> { | ||||
|     const activeRole = authStorage.getActiveRole(); | ||||
|     if (activeRole) { | ||||
|         await userManagers[activeRole].signoutRedirect(); | ||||
|         await (await getUserManagers())[activeRole].signoutRedirect(); | ||||
|         authStorage.deleteActiveRole(); | ||||
|     } | ||||
| } | ||||
|  | @ -114,16 +113,12 @@ apiClient.interceptors.request.use( | |||
|         } | ||||
|         return reqConfig; | ||||
|     }, | ||||
|     (error) => { | ||||
|         return Promise.reject(error); | ||||
|     }, | ||||
|     (error) => Promise.reject(error), | ||||
| ); | ||||
| 
 | ||||
| // Registering interceptor to refresh the token when a request failed because it was expired.
 | ||||
| apiClient.interceptors.response.use( | ||||
|     (response) => { | ||||
|         return response; | ||||
|     }, | ||||
|     (response) => response, | ||||
|     async (error: AxiosError<{ message?: string }>) => { | ||||
|         if (error.response?.status === 401) { | ||||
|             if (error.response!.data.message === "token_expired") { | ||||
|  |  | |||
|  | @ -1,4 +1,5 @@ | |||
| <script setup lang="ts"> | ||||
|     import dwengoLogo from "../../../assets/img/dwengo-groen-zwart.svg"; | ||||
|     import auth from "@/services/auth/auth-service.ts"; | ||||
| 
 | ||||
|     function loginAsStudent() { | ||||
|  | @ -16,11 +17,53 @@ | |||
| 
 | ||||
| <template> | ||||
|     <main> | ||||
|         <!-- TODO Placeholder implementation to test the login - replace by a more beautiful page later --> | ||||
|         <div v-if="!auth.isLoggedIn.value"> | ||||
|             <p>You are currently not logged in.</p> | ||||
|             <v-btn @click="loginAsStudent">Login as student</v-btn> | ||||
|             <v-btn @click="loginAsTeacher">Login as teacher</v-btn> | ||||
|         <div | ||||
|             class="login_background" | ||||
|             v-if="!auth.isLoggedIn.value" | ||||
|         > | ||||
|             <ul> | ||||
|                 <img | ||||
|                     class="dwengo_logo" | ||||
|                     :src="dwengoLogo" | ||||
|                 /> | ||||
|                 <div class="container"> | ||||
|                     <ul> | ||||
|                         <li class="title">login</li> | ||||
|                         <li> | ||||
|                             <v-btn | ||||
|                                 density="comfortable" | ||||
|                                 size="large" | ||||
|                                 class="button" | ||||
|                                 @click="loginAsTeacher" | ||||
|                             > | ||||
|                                 teacher | ||||
|                                 <v-icon | ||||
|                                     end | ||||
|                                     size="x-large" | ||||
|                                 > | ||||
|                                     mdi-menu-right | ||||
|                                 </v-icon> | ||||
|                             </v-btn> | ||||
|                         </li> | ||||
|                         <li> | ||||
|                             <v-btn | ||||
|                                 density="comfortable" | ||||
|                                 size="large" | ||||
|                                 class="button" | ||||
|                                 @click="loginAsStudent" | ||||
|                             > | ||||
|                                 student | ||||
|                                 <v-icon | ||||
|                                     end | ||||
|                                     size="x-large" | ||||
|                                 > | ||||
|                                     mdi-menu-right | ||||
|                                 </v-icon> | ||||
|                             </v-btn> | ||||
|                         </li> | ||||
|                     </ul> | ||||
|                 </div> | ||||
|             </ul> | ||||
|         </div> | ||||
|         <div v-if="auth.isLoggedIn.value"> | ||||
|             <p> | ||||
|  | @ -31,4 +74,41 @@ | |||
|     </main> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped></style> | ||||
| <style scoped> | ||||
|     .login_background { | ||||
|         background-color: #f6faf2; | ||||
|         min-height: 100vh; | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         justify-content: center; | ||||
|     } | ||||
| 
 | ||||
|     img { | ||||
|         width: 200px; | ||||
|     } | ||||
| 
 | ||||
|     ul { | ||||
|         list-style: none; | ||||
|         text-align: center; | ||||
|     } | ||||
| 
 | ||||
|     li { | ||||
|         padding: 10px; | ||||
|     } | ||||
| 
 | ||||
|     .button { | ||||
|         background-color: #f6faf2; | ||||
|     } | ||||
| 
 | ||||
|     .container { | ||||
|         background-color: white; | ||||
|         width: 300px; | ||||
|         height: 400px; | ||||
|     } | ||||
| 
 | ||||
|     .title { | ||||
|         font-weight: bold; | ||||
|         font-size: xx-large; | ||||
|         text-transform: uppercase; | ||||
|     } | ||||
| </style> | ||||
|  |  | |||
		Reference in a new issue