Merge branch 'dev' into refactor/linting

This commit is contained in:
Tibo De Peuter 2025-03-30 22:44:13 +02:00
commit 588c556949
Signed by: tdpeuter
GPG key ID: 38297DE43F75FFE2
37 changed files with 686 additions and 796 deletions

View file

@ -9,6 +9,7 @@ import { envVars, getNumericEnvVar } from './util/envVars.js';
import apiRouter from './routes/router.js'; import apiRouter from './routes/router.js';
import swaggerMiddleware from './swagger.js'; import swaggerMiddleware from './swagger.js';
import swaggerUi from 'swagger-ui-express'; import swaggerUi from 'swagger-ui-express';
import { errorHandler } from './middleware/error-handling/error-handler.js';
const logger: Logger = getLogger(); const logger: Logger = getLogger();
@ -26,6 +27,8 @@ app.use('/api', apiRouter);
// Swagger // Swagger
app.use('/api-docs', swaggerUi.serve, swaggerMiddleware); app.use('/api-docs', swaggerUi.serve, swaggerMiddleware);
app.use(errorHandler);
async function startServer(): Promise<void> { async function startServer(): Promise<void> {
await initORM(); await initORM();

View file

@ -1,12 +1,16 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { FALLBACK_LANG } from '../config.js'; import { FALLBACK_LANG } from '../config.js';
import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../interfaces/learning-content.js'; import {
FilteredLearningObject,
LearningObjectIdentifier,
LearningPathIdentifier,
} from '../interfaces/learning-content.js';
import learningObjectService from '../services/learning-objects/learning-object-service.js'; import learningObjectService from '../services/learning-objects/learning-object-service.js';
import { envVars, getEnvVar } from '../util/envVars.js'; import { envVars, getEnvVar } from '../util/envVars.js';
import { Language } from '../entities/content/language.js'; import { Language } from '../entities/content/language.js';
import { BadRequestException } from '../exceptions/badRequestException.js';
import attachmentService from '../services/learning-objects/attachment-service.js'; import attachmentService from '../services/learning-objects/attachment-service.js';
import { NotFoundError } from '@mikro-orm/core'; import { NotFoundError } from '@mikro-orm/core';
import { BadRequestException } from '../exceptions/bad-request-exception.js';
function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier { function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier {
if (!req.params.hruid) { if (!req.params.hruid) {

View file

@ -2,14 +2,14 @@ import { Request, Response } from 'express';
import { themes } from '../data/themes.js'; import { themes } from '../data/themes.js';
import { FALLBACK_LANG } from '../config.js'; import { FALLBACK_LANG } from '../config.js';
import learningPathService from '../services/learning-paths/learning-path-service.js'; import learningPathService from '../services/learning-paths/learning-path-service.js';
import { BadRequestException } from '../exceptions/badRequestException.js';
import { Language } from '../entities/content/language.js'; import { Language } from '../entities/content/language.js';
import { import {
PersonalizationTarget, PersonalizationTarget,
personalizedForGroup, personalizedForGroup,
personalizedForStudent, personalizedForStudent,
} from '../services/learning-paths/learning-path-personalization-util.js'; } from '../services/learning-paths/learning-path-personalization-util.js';
import { NotFoundException } from '../exceptions/notFoundException.js'; import { BadRequestException } from '../exceptions/bad-request-exception.js';
import { NotFoundException } from '../exceptions/not-found-exception.js';
/** /**
* Fetch learning paths based on query parameters. * Fetch learning paths based on query parameters.

View file

@ -1,10 +1,12 @@
import { EntityRepository, FilterQuery } from '@mikro-orm/core'; import { EntityRepository, FilterQuery } from '@mikro-orm/core';
import { EntityAlreadyExistsException } from '../exceptions/entity-already-exists-exception.js';
export abstract class DwengoEntityRepository<T extends object> extends EntityRepository<T> { export abstract class DwengoEntityRepository<T extends object> extends EntityRepository<T> {
public async save(entity: T): Promise<void> { public async save(entity: T, options?: { preventOverwrite?: boolean }): Promise<void> {
const em = this.getEntityManager(); if (options?.preventOverwrite && (await this.findOne(entity))) {
em.persist(entity); throw new EntityAlreadyExistsException(`A ${this.getEntityName()} with this identifier already exists.`);
await em.flush(); }
await this.getEntityManager().persistAndFlush(entity);
} }
public async deleteWhere(query: FilterQuery<T>): Promise<void> { public async deleteWhere(query: FilterQuery<T>): Promise<void> {
const toDelete = await this.findOne(query); const toDelete = await this.findOne(query);

View file

@ -13,12 +13,4 @@ export class Student extends User {
@ManyToMany(() => Group) @ManyToMany(() => Group)
groups!: Collection<Group>; groups!: Collection<Group>;
constructor(
public username: string,
public firstName: string,
public lastName: string
) {
super();
}
} }

View file

@ -7,12 +7,4 @@ import { TeacherRepository } from '../../data/users/teacher-repository.js';
export class Teacher extends User { export class Teacher extends User {
@ManyToMany(() => Class) @ManyToMany(() => Class)
classes!: Collection<Class>; classes!: Collection<Class>;
constructor(
public username: string,
public firstName: string,
public lastName: string
) {
super();
}
} }

View file

@ -0,0 +1,10 @@
import { ExceptionWithHttpState } from './exception-with-http-state.js';
/**
* Exception for HTTP 400 Bad Request
*/
export class BadRequestException extends ExceptionWithHttpState {
constructor(error: string) {
super(400, error);
}
}

View file

@ -1,11 +0,0 @@
import { HttpException } from './httpException.js';
/**
* Exception for HTTP 400 Bad Request
*/
export class BadRequestException extends HttpException {
constructor(message = 'Bad Request') {
super(400, message);
}
}

View file

@ -0,0 +1,12 @@
import { ExceptionWithHttpState } from './exception-with-http-state.js';
/**
* Exception for HTTP 409 Conflict
*/
export class ConflictException extends ExceptionWithHttpState {
public status = 409;
constructor(error: string) {
super(409, error);
}
}

View file

@ -0,0 +1,7 @@
import { ConflictException } from './conflict-exception.js';
export class EntityAlreadyExistsException extends ConflictException {
constructor(message: string) {
super(message);
}
}

View file

@ -0,0 +1,11 @@
/**
* Exceptions which are associated with a HTTP error code.
*/
export abstract class ExceptionWithHttpState extends Error {
constructor(
public status: number,
public error: string
) {
super(error);
}
}

View file

@ -0,0 +1,12 @@
import { ExceptionWithHttpState } from './exception-with-http-state.js';
/**
* Exception for HTTP 403 Forbidden
*/
export class ForbiddenException extends ExceptionWithHttpState {
status = 403;
constructor(message = 'Forbidden') {
super(403, message);
}
}

View file

@ -1,10 +0,0 @@
import { HttpException } from './httpException.js';
/**
* Exception for HTTP 403 Forbidden
*/
export class ForbiddenException extends HttpException {
constructor(message = 'Forbidden') {
super(403, message);
}
}

View file

@ -1,8 +0,0 @@
export class HttpException extends Error {
constructor(
public status: number,
message: string
) {
super(message);
}
}

View file

@ -0,0 +1,12 @@
import { ExceptionWithHttpState } from './exception-with-http-state.js';
/**
* Exception for HTTP 404 Not Found
*/
export class NotFoundException extends ExceptionWithHttpState {
public status = 404;
constructor(error: string) {
super(404, error);
}
}

View file

@ -1,10 +0,0 @@
import { HttpException } from './httpException.js';
/**
* Exception for HTTP 404 Not Found
*/
export class NotFoundException extends HttpException {
constructor(message = 'Not Found') {
super(404, message);
}
}

View file

@ -0,0 +1,10 @@
import { ExceptionWithHttpState } from './exception-with-http-state.js';
/**
* Exception for HTTP 401 Unauthorized
*/
export class UnauthorizedException extends ExceptionWithHttpState {
constructor(message = 'Unauthorized') {
super(401, message);
}
}

View file

@ -1,10 +0,0 @@
import { HttpException } from './httpException.js';
/**
* Exception for HTTP 401 Unauthorized
*/
export class UnauthorizedException extends HttpException {
constructor(message = 'Unauthorized') {
super(401, message);
}
}

View file

@ -1,4 +1,5 @@
import { Student } from '../entities/users/student.entity.js'; import { Student } from '../entities/users/student.entity.js';
import { getStudentRepository } from '../data/repositories.js';
export interface StudentDTO { export interface StudentDTO {
id: string; id: string;
@ -23,7 +24,9 @@ export function mapToStudentDTO(student: Student): StudentDTO {
} }
export function mapToStudent(studentData: StudentDTO): Student { export function mapToStudent(studentData: StudentDTO): Student {
const student = new Student(studentData.username, studentData.firstName, studentData.lastName); return getStudentRepository().create({
username: studentData.username,
return student; firstName: studentData.firstName,
lastName: studentData.lastName,
});
} }

View file

@ -1,4 +1,5 @@
import { Teacher } from '../entities/users/teacher.entity.js'; import { Teacher } from '../entities/users/teacher.entity.js';
import { getTeacherRepository } from '../data/repositories.js';
export interface TeacherDTO { export interface TeacherDTO {
id: string; id: string;
@ -22,8 +23,10 @@ export function mapToTeacherDTO(teacher: Teacher): TeacherDTO {
}; };
} }
export function mapToTeacher(teacherDTO: TeacherDTO): Teacher { export function mapToTeacher(teacherData: TeacherDTO): Teacher {
const teacher = new Teacher(teacherDTO.username, teacherDTO.firstName, teacherDTO.lastName); return getTeacherRepository().create({
username: teacherData.username,
return teacher; firstName: teacherData.firstName,
lastName: teacherData.lastName,
});
} }

View file

@ -6,8 +6,8 @@ import jwksClient from 'jwks-rsa';
import * as express from 'express'; import * as express from 'express';
import { AuthenticatedRequest } from './authenticated-request.js'; import { AuthenticatedRequest } from './authenticated-request.js';
import { AuthenticationInfo } from './authentication-info.js'; import { AuthenticationInfo } from './authentication-info.js';
import { UnauthorizedException } from '../../exceptions/unauthorizedException.js'; import { UnauthorizedException } from '../../exceptions/unauthorized-exception.js';
import { ForbiddenException } from '../../exceptions/forbiddenException.js'; import { ForbiddenException } from '../../exceptions/forbidden-exception.js';
const JWKS_CACHE = true; const JWKS_CACHE = true;
const JWKS_RATE_LIMIT = true; const JWKS_RATE_LIMIT = true;

View file

@ -0,0 +1,15 @@
import { NextFunction, Request, Response } from 'express';
import { getLogger, Logger } from '../../logging/initalize.js';
import { ExceptionWithHttpState } from '../../exceptions/exception-with-http-state.js';
const logger: Logger = getLogger();
export function errorHandler(err: unknown, _req: Request, res: Response, _: NextFunction): void {
if (err instanceof ExceptionWithHttpState) {
logger.warn(`An error occurred while handling a request: ${err} (-> HTTP ${err.status})`);
res.status(err.status).json(err);
} else {
logger.error(`Unexpected error occurred while handing a request: ${JSON.stringify(err)}`);
res.status(500).json(err);
}
}

View file

@ -49,6 +49,7 @@ function config(testingMode = false): Options {
dbName: getEnvVar(envVars.DbName), dbName: getEnvVar(envVars.DbName),
subscribers: [new SqliteAutoincrementSubscriber()], subscribers: [new SqliteAutoincrementSubscriber()],
entities: entities, entities: entities,
persistOnCreate: false, // Do not implicitly save entities when they are created via `create`.
// EntitiesTs: entitiesTs, // EntitiesTs: entitiesTs,
// Workaround: vitest: `TypeError: Unknown file extension ".ts"` (ERR_UNKNOWN_FILE_EXTENSION) // Workaround: vitest: `TypeError: Unknown file extension ".ts"` (ERR_UNKNOWN_FILE_EXTENSION)
@ -65,6 +66,7 @@ function config(testingMode = false): Options {
user: getEnvVar(envVars.DbUsername), user: getEnvVar(envVars.DbUsername),
password: getEnvVar(envVars.DbPassword), password: getEnvVar(envVars.DbPassword),
entities: entities, entities: entities,
persistOnCreate: false, // Do not implicitly save entities when they are created via `create`.
// EntitiesTs: entitiesTs, // EntitiesTs: entitiesTs,
// Logging // Logging

View file

@ -1,4 +1,9 @@
import { getClassRepository, getGroupRepository, getStudentRepository, getSubmissionRepository } from '../data/repositories.js'; import {
getClassRepository,
getGroupRepository,
getStudentRepository,
getSubmissionRepository,
} from '../data/repositories.js';
import { AssignmentDTO } from '../interfaces/assignment.js'; import { AssignmentDTO } from '../interfaces/assignment.js';
import { ClassDTO, mapToClassDTO } from '../interfaces/class.js'; import { ClassDTO, mapToClassDTO } from '../interfaces/class.js';
import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js';
@ -27,15 +32,9 @@ export async function getStudent(username: string): Promise<StudentDTO | null> {
export async function createStudent(userData: StudentDTO): Promise<StudentDTO | null> { export async function createStudent(userData: StudentDTO): Promise<StudentDTO | null> {
const studentRepository = getStudentRepository(); const studentRepository = getStudentRepository();
try { const newStudent = mapToStudent(userData);
const newStudent = studentRepository.create(mapToStudent(userData)); await studentRepository.save(newStudent, { preventOverwrite: true });
await studentRepository.save(newStudent); return mapToStudentDTO(newStudent);
return mapToStudentDTO(newStudent);
} catch (e) {
getLogger().error(e);
return null;
}
} }
export async function deleteStudent(username: string): Promise<StudentDTO | null> { export async function deleteStudent(username: string): Promise<StudentDTO | null> {

View file

@ -26,15 +26,10 @@ export async function getTeacher(username: string): Promise<TeacherDTO | null> {
export async function createTeacher(userData: TeacherDTO): Promise<TeacherDTO | null> { export async function createTeacher(userData: TeacherDTO): Promise<TeacherDTO | null> {
const teacherRepository = getTeacherRepository(); const teacherRepository = getTeacherRepository();
try { const newTeacher = mapToTeacher(userData);
const newTeacher = teacherRepository.create(mapToTeacher(userData)); await teacherRepository.save(newTeacher, { preventOverwrite: true });
await teacherRepository.save(newTeacher);
return mapToTeacherDTO(newTeacher); return mapToTeacherDTO(newTeacher);
} catch (e) {
getLogger().error(e);
return null;
}
} }
export async function deleteTeacher(username: string): Promise<TeacherDTO | null> { export async function deleteTeacher(username: string): Promise<TeacherDTO | null> {

View file

@ -1,5 +1,4 @@
import { setupTestApp } from '../../setup-tests.js'; import { setupTestApp } from '../../setup-tests.js';
import { Student } from '../../../src/entities/users/student.entity.js';
import { describe, it, expect, beforeAll } from 'vitest'; import { describe, it, expect, beforeAll } from 'vitest';
import { StudentRepository } from '../../../src/data/users/student-repository.js'; import { StudentRepository } from '../../../src/data/users/student-repository.js';
import { getStudentRepository } from '../../../src/data/repositories.js'; import { getStudentRepository } from '../../../src/data/repositories.js';
@ -30,7 +29,7 @@ describe('StudentRepository', () => {
}); });
it('should return the queried student after he was added', async () => { it('should return the queried student after he was added', async () => {
await studentRepository.insert(new Student(username, firstName, lastName)); await studentRepository.insert(studentRepository.create({ username, firstName, lastName }));
const retrievedStudent = await studentRepository.findByUsername(username); const retrievedStudent = await studentRepository.findByUsername(username);
expect(retrievedStudent).toBeTruthy(); expect(retrievedStudent).toBeTruthy();

View file

@ -2,7 +2,6 @@ import { describe, it, expect, beforeAll } from 'vitest';
import { TeacherRepository } from '../../../src/data/users/teacher-repository'; import { TeacherRepository } from '../../../src/data/users/teacher-repository';
import { setupTestApp } from '../../setup-tests'; import { setupTestApp } from '../../setup-tests';
import { getTeacherRepository } from '../../../src/data/repositories'; import { getTeacherRepository } from '../../../src/data/repositories';
import { Teacher } from '../../../src/entities/users/teacher.entity';
const username = 'testteacher'; const username = 'testteacher';
const firstName = 'John'; const firstName = 'John';
@ -30,7 +29,7 @@ describe('TeacherRepository', () => {
}); });
it('should return the queried teacher after he was added', async () => { it('should return the queried teacher after he was added', async () => {
await teacherRepository.insert(new Teacher(username, firstName, lastName)); await teacherRepository.insert(teacherRepository.create({ username, firstName, lastName }));
const retrievedTeacher = await teacherRepository.findByUsername(username); const retrievedTeacher = await teacherRepository.findByUsername(username);
expect(retrievedTeacher).toBeTruthy(); expect(retrievedTeacher).toBeTruthy();

View file

@ -42,7 +42,7 @@
"jsdom": "^26.0.0", "jsdom": "^26.0.0",
"npm-run-all2": "^7.0.2", "npm-run-all2": "^7.0.2",
"typescript": "~5.7.3", "typescript": "~5.7.3",
"vite": "^6.1.0", "vite": "^6.1.2",
"vite-plugin-vue-devtools": "^7.7.2", "vite-plugin-vue-devtools": "^7.7.2",
"vitest": "^3.0.5", "vitest": "^3.0.5",
"vue-tsc": "^2.2.2" "vue-tsc": "^2.2.2"

View file

@ -1,10 +1,26 @@
<script setup lang="ts"> <script setup lang="ts">
import auth from "@/services/auth/auth-service.ts"; import auth from "@/services/auth/auth-service.ts";
import MenuBar from "@/components/MenuBar.vue";
import { useRoute } from "vue-router";
import { computed } from "vue";
const route = useRoute();
await auth.loadUser(); await auth.loadUser();
interface RouteMeta {
requiresAuth?: boolean;
}
const showMenuBar = computed(() => (route.meta as RouteMeta).requiresAuth && auth.authState.user);
</script> </script>
<template> <template>
<router-view /> <v-app>
<menu-bar v-if="showMenuBar"></menu-bar>
<v-main>
<router-view />
</v-main>
</v-app>
</template> </template>
<style scoped></style> <style scoped></style>

View file

@ -42,67 +42,83 @@
</script> </script>
<template> <template>
<main> <v-app-bar
<v-app class="menu_collapsed"> class="app-bar"
<v-app-bar app
app >
style="background-color: #f6faf2" <v-app-bar-nav-icon
class="menu_collapsed"
@click="drawer = !drawer"
/>
<router-link
to="/user"
class="dwengo_home"
>
<div>
<img
class="dwengo_logo"
alt="Dwengo logo"
:src="dwengoLogo"
/>
<p class="caption">
{{ t(`${role}`) }}
</p>
</div>
</router-link>
<v-toolbar-items class="menu">
<v-btn
class="menu_item"
variant="text"
to="/user/assignment"
> >
<template v-slot:prepend> {{ t("assignments") }}
<v-app-bar-nav-icon @click="drawer = !drawer" /> </v-btn>
</template> <v-btn
class="menu_item"
<v-app-bar-title> variant="text"
<router-link to="/user/class"
to="/user" >
class="dwengo_home" {{ t("classes") }}
</v-btn>
<v-btn
class="menu_item"
variant="text"
to="/user/discussion"
>
{{ t("discussions") }}
</v-btn>
<v-menu open-on-hover>
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
icon
variant="text"
> >
<div> <v-icon
<img icon="mdi-translate"
class="dwengo_logo" size="small"
:src="dwengoLogo" color="#0e6942"
style="width: 100px" ></v-icon>
/> </v-btn>
<p </template>
class="caption" <v-list>
style="font-size: smaller" <v-list-item
> v-for="(language, index) in languages"
{{ t(`${role}`) }} :key="index"
</p> @click="changeLanguage(language.code)"
</div> >
</router-link> <v-list-item-title>{{ language.name }}</v-list-item-title>
</v-app-bar-title> </v-list-item>
</v-list>
<v-spacer></v-spacer> </v-menu>
</v-toolbar-items>
<v-menu open-on-hover> <v-spacer></v-spacer>
<template v-slot:activator="{ props }"> <v-dialog max-width="500">
<v-btn <template v-slot:activator="{ props: activatorProps }">
v-bind="props"
icon
variant="text"
>
<v-icon
icon="mdi-translate"
size="small"
color="#0e6942"
></v-icon>
</v-btn>
</template>
<v-list>
<v-list-item
v-for="(language, index) in languages"
:key="index"
@click="changeLanguage(language.code)"
>
<v-list-item-title>{{ language.name }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-btn <v-btn
@click="performLogout" v-bind="activatorProps"
text :rounded="true"
variant="text"
> >
<v-tooltip <v-tooltip
:text="t('logout')" :text="t('logout')"
@ -114,201 +130,81 @@
icon="mdi-logout" icon="mdi-logout"
size="x-large" size="x-large"
color="#0e6942" color="#0e6942"
/> >
</v-icon>
</template> </template>
</v-tooltip> </v-tooltip>
</v-btn> </v-btn>
</v-app-bar> </template>
<v-navigation-drawer <template v-slot:default="{ isActive }">
v-model="drawer" <v-card :title="t('logoutVerification')">
app <v-card-actions>
> <v-spacer></v-spacer>
<v-list>
<v-list-item
to="/user/assignment"
link
>
<v-list-item-content>
<v-list-item-title class="menu_item">{{ t("assignments") }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item <v-btn
to="/user/class" :text="t('cancel')"
link @click="isActive.value = false"
> ></v-btn>
<v-list-item-content> <v-btn
<v-list-item-title class="menu_item">{{ t("classes") }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item
to="/user/discussion"
link
>
<v-list-item-content>
<v-list-item-title class="menu_item">{{ t("discussions") }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</v-navigation-drawer>
</v-app>
<nav class="menu">
<div class="left">
<ul>
<li>
<router-link
to="/user"
class="dwengo_home"
>
<img
class="dwengo_logo"
:src="dwengoLogo"
/>
<p class="caption">
{{ t(`${role}`) }}
</p>
</router-link>
</li>
<li>
<router-link
:to="`/user/assignment`"
class="menu_item"
>
{{ t("assignments") }}
</router-link>
</li>
<li>
<router-link
to="/user/class"
class="menu_item"
>{{ t("classes") }}</router-link
>
</li>
<li>
<router-link
to="/user/discussion"
class="menu_item"
>{{ t("discussions") }}
</router-link>
</li>
<li>
<v-menu open-on-hover>
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
icon
variant="text"
>
<v-icon
icon="mdi-translate"
size="small"
color="#0e6942"
></v-icon>
</v-btn>
</template>
<v-list>
<v-list-item
v-for="(language, index) in languages"
:key="index"
@click="changeLanguage(language.code)"
>
<v-list-item-title>{{ language.name }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</li>
</ul>
</div>
<div class="right">
<li>
<!-- <v-btn
@click="performLogout"
to="/login"
style="background-color: transparent; box-shadow: none !important"
>
<v-tooltip
:text="t('logout')" :text="t('logout')"
location="bottom" @click="performLogout"
> to="/login"
<template v-slot:activator="{ props }"> ></v-btn>
<v-icon </v-card-actions>
v-bind="props" </v-card>
icon="mdi-logout" </template>
size="x-large" </v-dialog>
color="#0e6942" <v-avatar
></v-icon> size="large"
</template> color="#0e6942"
</v-tooltip> class="user-button"
</v-btn> --> >{{ initials }}</v-avatar
<v-dialog max-width="500"> >
<template v-slot:activator="{ props: activatorProps }"> </v-app-bar>
<v-btn <v-navigation-drawer
v-bind="activatorProps" v-model="drawer"
style="background-color: transparent; box-shadow: none !important" temporary
> app
<v-tooltip >
:text="t('logout')" <v-list>
location="bottom" <v-list-item
> to="/user/assignment"
<template v-slot:activator="{ props }"> link
<v-icon >
v-bind="props" <v-list-item-title class="menu_item">{{ t("assignments") }}</v-list-item-title>
icon="mdi-logout" </v-list-item>
size="x-large"
color="#0e6942"
>
</v-icon>
</template>
</v-tooltip>
</v-btn>
</template>
<template v-slot:default="{ isActive }"> <v-list-item
<v-card :title="t('logoutVerification')"> to="/user/class"
<v-card-actions> link
<v-spacer></v-spacer> >
<v-list-item-title class="menu_item">{{ t("classes") }}</v-list-item-title>
</v-list-item>
<v-btn <v-list-item
:text="t('cancel')" to="/user/discussion"
@click="isActive.value = false" link
></v-btn> >
<v-btn <v-list-item-title class="menu_item">{{ t("discussions") }}</v-list-item-title>
:text="t('logout')" </v-list-item>
@click="performLogout" </v-list>
to="/login" </v-navigation-drawer>
></v-btn>
</v-card-actions>
</v-card>
</template>
</v-dialog>
</li>
<li>
<v-avatar
size="large"
color="#0e6942"
style="font-size: large; font-weight: bold"
>{{ initials }}</v-avatar
>
</li>
</div>
</nav>
<router-view />
</main>
</template> </template>
<style scoped> <style scoped>
.app-bar {
background-color: #f6faf2;
}
.menu { .menu {
background-color: #f6faf2; background-color: #f6faf2;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
.user-button {
.right { margin-right: 10px;
align-items: center; font-size: large;
padding: 10px; font-weight: bold;
} }
.right li { .right li {
@ -346,16 +242,19 @@
color: #0e6942; color: #0e6942;
text-decoration: none; text-decoration: none;
font-size: large; font-size: large;
} text-transform: none;
nav a.router-link-active {
font-weight: bold;
} }
@media (max-width: 700px) { @media (max-width: 700px) {
.menu { .menu {
display: none; display: none;
} }
.caption {
font-size: smaller;
}
.dwengo_logo {
width: 100px;
}
} }
@media (min-width: 701px) { @media (min-width: 701px) {

View file

@ -5,7 +5,7 @@ import { type MaybeRefOrGetter, toValue } from "vue";
const themeController = getThemeController(); const themeController = getThemeController();
export function useThemeQuery(language: MaybeRefOrGetter<string>): UseQueryReturnType<never, Error> { export function useThemeQuery(language: MaybeRefOrGetter<string>): UseQueryReturnType<never, Error> {
return useQuery({ useQuery({
queryKey: ["themes", language], queryKey: ["themes", language],
queryFn: async () => { queryFn: async () => {
const lang = toValue(language); const lang = toValue(language);
@ -16,7 +16,7 @@ export function useThemeQuery(language: MaybeRefOrGetter<string>): UseQueryRetur
} }
export function useThemeHruidsQuery(themeKey: string | null): UseQueryReturnType<never, Error> { export function useThemeHruidsQuery(themeKey: string | null): UseQueryReturnType<never, Error> {
return useQuery({ useQuery({
queryKey: ["theme-hruids", themeKey], queryKey: ["theme-hruids", themeKey],
queryFn: async () => themeController.getHruidsByKey(themeKey!), queryFn: async () => themeController.getHruidsByKey(themeKey!),
enabled: Boolean(themeKey), enabled: Boolean(themeKey),

View file

@ -1,5 +1,4 @@
import { createRouter, createWebHistory } from "vue-router"; import { createRouter, createWebHistory } from "vue-router";
import MenuBar from "@/components/MenuBar.vue";
import SingleAssignment from "@/views/assignments/SingleAssignment.vue"; import SingleAssignment from "@/views/assignments/SingleAssignment.vue";
import SingleClass from "@/views/classes/SingleClass.vue"; import SingleClass from "@/views/classes/SingleClass.vue";
import SingleDiscussion from "@/views/discussions/SingleDiscussion.vue"; import SingleDiscussion from "@/views/discussions/SingleDiscussion.vue";
@ -38,7 +37,6 @@ const router = createRouter({
{ {
path: "/user", path: "/user",
component: MenuBar,
meta: { requiresAuth: true }, meta: { requiresAuth: true },
children: [ children: [
{ {

View file

@ -2,11 +2,14 @@ import apiClient from "@/services/api-client.ts";
import type { FrontendAuthConfig } from "@/services/auth/auth.d.ts"; import type { FrontendAuthConfig } from "@/services/auth/auth.d.ts";
import type { UserManagerSettings } from "oidc-client-ts"; import type { UserManagerSettings } from "oidc-client-ts";
export const AUTH_CONFIG_ENDPOINT = "auth/config";
/** /**
* Fetch the authentication configuration from the backend. * Fetch the authentication configuration from the backend.
*/ */
export async function loadAuthConfig(): Promise<Record<string, UserManagerSettings>> { export async function loadAuthConfig(): Promise<Record<string, UserManagerSettings>> {
const authConfig = (await apiClient.get<FrontendAuthConfig>("auth/config")).data; const authConfigResponse = await apiClient.get<FrontendAuthConfig>(AUTH_CONFIG_ENDPOINT);
const authConfig = authConfigResponse.data;
return { return {
student: { student: {
authority: authConfig.student.authority, authority: authConfig.student.authority,

View file

@ -5,7 +5,7 @@
import { computed, reactive } from "vue"; import { computed, reactive } from "vue";
import type { AuthState, Role, UserManagersForRoles } from "@/services/auth/auth.d.ts"; import type { AuthState, Role, UserManagersForRoles } from "@/services/auth/auth.d.ts";
import { User, UserManager } from "oidc-client-ts"; import { User, UserManager } from "oidc-client-ts";
import { loadAuthConfig } from "@/services/auth/auth-config-loader.ts"; import { AUTH_CONFIG_ENDPOINT, loadAuthConfig } from "@/services/auth/auth-config-loader.ts";
import authStorage from "./auth-storage.ts"; import authStorage from "./auth-storage.ts";
import { loginRoute } from "@/config.ts"; import { loginRoute } from "@/config.ts";
import apiClient from "@/services/api-client.ts"; import apiClient from "@/services/api-client.ts";
@ -108,7 +108,7 @@ async function logout(): Promise<void> {
apiClient.interceptors.request.use( apiClient.interceptors.request.use(
async (reqConfig) => { async (reqConfig) => {
const token = authState?.user?.access_token; const token = authState?.user?.access_token;
if (token) { if (token && reqConfig.url !== AUTH_CONFIG_ENDPOINT) {
reqConfig.headers.Authorization = `Bearer ${token}`; reqConfig.headers.Authorization = `Bearer ${token}`;
} }
return reqConfig; return reqConfig;

View file

@ -8,7 +8,7 @@
onMounted(async () => { onMounted(async () => {
try { try {
await auth.handleLoginCallback(); await auth.handleLoginCallback();
await router.replace("/"); // Redirect to home (or dashboard) await router.replace("/user"); // Redirect to theme page
} catch (_error) { } catch (_error) {
// FIXME console.error("OIDC callback error:", error); // FIXME console.error("OIDC callback error:", error);
} }

View file

@ -25,9 +25,10 @@
<div class="container_left"> <div class="container_left">
<img <img
:src="dwengoLogo" :src="dwengoLogo"
alt="Dwengo logo"
style="align-self: center" style="align-self: center"
/> />
<h> {{ t("homeTitle") }}</h> <h1>{{ t("homeTitle") }}</h1>
<p class="info"> <p class="info">
{{ t("homeIntroduction1") }} {{ t("homeIntroduction1") }}
</p> </p>
@ -55,7 +56,7 @@
width="125" width="125"
src="/assets/home/innovative.png" src="/assets/home/innovative.png"
></v-img> ></v-img>
<h class="big">{{ t("innovative") }}</h> <h2 class="big">{{ t("innovative") }}</h2>
</div> </div>
<div class="img_small"> <div class="img_small">
<v-img <v-img
@ -63,7 +64,7 @@
width="125" width="125"
src="/assets/home/research_based.png" src="/assets/home/research_based.png"
></v-img> ></v-img>
<h class="big">{{ t("researchBased") }}</h> <h2 class="big">{{ t("researchBased") }}</h2>
</div> </div>
<div class="img_small"> <div class="img_small">
<v-img <v-img
@ -71,7 +72,7 @@
width="125" width="125"
src="/assets/home/inclusive.png" src="/assets/home/inclusive.png"
></v-img> ></v-img>
<h class="big">{{ t("sociallyRelevant") }}</h> <h2 class="big">{{ t("sociallyRelevant") }}</h2>
</div> </div>
<div class="img_small"> <div class="img_small">
<v-img <v-img
@ -79,7 +80,7 @@
width="125" width="125"
src="/assets/home/socially_relevant.png" src="/assets/home/socially_relevant.png"
></v-img> ></v-img>
<h class="big">{{ t("inclusive") }}</h> <h2 class="big">{{ t("inclusive") }}</h2>
</div> </div>
</div> </div>
<div class="container_right"> <div class="container_right">
@ -158,7 +159,7 @@
margin-bottom: 10px; margin-bottom: 10px;
} }
h { h2 {
font-size: large; font-size: large;
font-weight: bold; font-weight: bold;
align-self: center; align-self: center;

810
package-lock.json generated

File diff suppressed because it is too large Load diff