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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue