Merge branch 'dev' into refactor/linting
This commit is contained in:
		
						commit
						588c556949
					
				
					 37 changed files with 686 additions and 796 deletions
				
			
		|  | @ -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(); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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) { | ||||||
|  |  | ||||||
|  | @ -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. | ||||||
|  |  | ||||||
|  | @ -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); | ||||||
|  |  | ||||||
|  | @ -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(); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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(); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										10
									
								
								backend/src/exceptions/bad-request-exception.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								backend/src/exceptions/bad-request-exception.ts
									
										
									
									
									
										Normal 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); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -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); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										12
									
								
								backend/src/exceptions/conflict-exception.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								backend/src/exceptions/conflict-exception.ts
									
										
									
									
									
										Normal 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); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,7 @@ | ||||||
|  | import { ConflictException } from './conflict-exception.js'; | ||||||
|  | 
 | ||||||
|  | export class EntityAlreadyExistsException extends ConflictException { | ||||||
|  |     constructor(message: string) { | ||||||
|  |         super(message); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								backend/src/exceptions/exception-with-http-state.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								backend/src/exceptions/exception-with-http-state.ts
									
										
									
									
									
										Normal 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); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								backend/src/exceptions/forbidden-exception.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								backend/src/exceptions/forbidden-exception.ts
									
										
									
									
									
										Normal 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); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -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); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -1,8 +0,0 @@ | ||||||
| export class HttpException extends Error { |  | ||||||
|     constructor( |  | ||||||
|         public status: number, |  | ||||||
|         message: string |  | ||||||
|     ) { |  | ||||||
|         super(message); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										12
									
								
								backend/src/exceptions/not-found-exception.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								backend/src/exceptions/not-found-exception.ts
									
										
									
									
									
										Normal 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); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -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); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										10
									
								
								backend/src/exceptions/unauthorized-exception.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								backend/src/exceptions/unauthorized-exception.ts
									
										
									
									
									
										Normal 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); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -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); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -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, | ||||||
|  |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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, | ||||||
|  |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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; | ||||||
|  |  | ||||||
							
								
								
									
										15
									
								
								backend/src/middleware/error-handling/error-handler.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								backend/src/middleware/error-handling/error-handler.ts
									
										
									
									
									
										Normal 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); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -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
 | ||||||
|  |  | ||||||
|  | @ -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> { | ||||||
|  |  | ||||||
|  | @ -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> { | ||||||
|  |  | ||||||
|  | @ -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(); | ||||||
|  |  | ||||||
|  | @ -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(); | ||||||
|  |  | ||||||
|  | @ -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" | ||||||
|  |  | ||||||
|  | @ -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> | ||||||
|  |     <v-app> | ||||||
|  |         <menu-bar v-if="showMenuBar"></menu-bar> | ||||||
|  |         <v-main> | ||||||
|             <router-view /> |             <router-view /> | ||||||
|  |         </v-main> | ||||||
|  |     </v-app> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped></style> | <style scoped></style> | ||||||
|  |  | ||||||
|  | @ -42,17 +42,14 @@ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|     <main> |  | ||||||
|         <v-app class="menu_collapsed"> |  | ||||||
|     <v-app-bar |     <v-app-bar | ||||||
|  |         class="app-bar" | ||||||
|         app |         app | ||||||
|                 style="background-color: #f6faf2" |  | ||||||
|     > |     > | ||||||
|                 <template v-slot:prepend> |         <v-app-bar-nav-icon | ||||||
|                     <v-app-bar-nav-icon @click="drawer = !drawer" /> |             class="menu_collapsed" | ||||||
|                 </template> |             @click="drawer = !drawer" | ||||||
| 
 |         /> | ||||||
|                 <v-app-bar-title> |  | ||||||
|         <router-link |         <router-link | ||||||
|             to="/user" |             to="/user" | ||||||
|             class="dwengo_home" |             class="dwengo_home" | ||||||
|  | @ -60,141 +57,36 @@ | ||||||
|             <div> |             <div> | ||||||
|                 <img |                 <img | ||||||
|                     class="dwengo_logo" |                     class="dwengo_logo" | ||||||
|                                 :src="dwengoLogo" |                     alt="Dwengo logo" | ||||||
|                                 style="width: 100px" |  | ||||||
|                             /> |  | ||||||
|                             <p |  | ||||||
|                                 class="caption" |  | ||||||
|                                 style="font-size: smaller" |  | ||||||
|                             > |  | ||||||
|                                 {{ t(`${role}`) }} |  | ||||||
|                             </p> |  | ||||||
|                         </div> |  | ||||||
|                     </router-link> |  | ||||||
|                 </v-app-bar-title> |  | ||||||
| 
 |  | ||||||
|                 <v-spacer></v-spacer> |  | ||||||
| 
 |  | ||||||
|                 <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> |  | ||||||
| 
 |  | ||||||
|                 <v-btn |  | ||||||
|                     @click="performLogout" |  | ||||||
|                     text |  | ||||||
|                 > |  | ||||||
|                     <v-tooltip |  | ||||||
|                         :text="t('logout')" |  | ||||||
|                         location="bottom" |  | ||||||
|                     > |  | ||||||
|                         <template v-slot:activator="{ props }"> |  | ||||||
|                             <v-icon |  | ||||||
|                                 v-bind="props" |  | ||||||
|                                 icon="mdi-logout" |  | ||||||
|                                 size="x-large" |  | ||||||
|                                 color="#0e6942" |  | ||||||
|                             /> |  | ||||||
|                         </template> |  | ||||||
|                     </v-tooltip> |  | ||||||
|                 </v-btn> |  | ||||||
|             </v-app-bar> |  | ||||||
| 
 |  | ||||||
|             <v-navigation-drawer |  | ||||||
|                 v-model="drawer" |  | ||||||
|                 app |  | ||||||
|             > |  | ||||||
|                 <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 |  | ||||||
|                         to="/user/class" |  | ||||||
|                         link |  | ||||||
|                     > |  | ||||||
|                         <v-list-item-content> |  | ||||||
|                             <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" |                     :src="dwengoLogo" | ||||||
|                 /> |                 /> | ||||||
|                 <p class="caption"> |                 <p class="caption"> | ||||||
|                     {{ t(`${role}`) }} |                     {{ t(`${role}`) }} | ||||||
|                 </p> |                 </p> | ||||||
|  |             </div> | ||||||
|         </router-link> |         </router-link> | ||||||
|                     </li> |         <v-toolbar-items class="menu"> | ||||||
|                     <li> |             <v-btn | ||||||
|                         <router-link |  | ||||||
|                             :to="`/user/assignment`" |  | ||||||
|                 class="menu_item" |                 class="menu_item" | ||||||
|  |                 variant="text" | ||||||
|  |                 to="/user/assignment" | ||||||
|             > |             > | ||||||
|                 {{ t("assignments") }} |                 {{ t("assignments") }} | ||||||
|                         </router-link> |             </v-btn> | ||||||
|                     </li> |             <v-btn | ||||||
|                     <li> |                 class="menu_item" | ||||||
|                         <router-link |                 variant="text" | ||||||
|                 to="/user/class" |                 to="/user/class" | ||||||
|                             class="menu_item" |  | ||||||
|                             >{{ t("classes") }}</router-link |  | ||||||
|             > |             > | ||||||
|                     </li> |                 {{ t("classes") }} | ||||||
|                     <li> |             </v-btn> | ||||||
|                         <router-link |             <v-btn | ||||||
|                             to="/user/discussion" |  | ||||||
|                 class="menu_item" |                 class="menu_item" | ||||||
|                             >{{ t("discussions") }} |                 variant="text" | ||||||
|                         </router-link> |                 to="/user/discussion" | ||||||
|                     </li> |             > | ||||||
|                     <li> |                 {{ t("discussions") }} | ||||||
|  |             </v-btn> | ||||||
|             <v-menu open-on-hover> |             <v-menu open-on-hover> | ||||||
|                 <template v-slot:activator="{ props }"> |                 <template v-slot:activator="{ props }"> | ||||||
|                     <v-btn |                     <v-btn | ||||||
|  | @ -219,35 +111,14 @@ | ||||||
|                     </v-list-item> |                     </v-list-item> | ||||||
|                 </v-list> |                 </v-list> | ||||||
|             </v-menu> |             </v-menu> | ||||||
|                     </li> |         </v-toolbar-items> | ||||||
|                 </ul> |         <v-spacer></v-spacer> | ||||||
|             </div> |  | ||||||
|             <div class="right"> |  | ||||||
|                 <li> |  | ||||||
|                     <!-- <v-btn |  | ||||||
|                         @click="performLogout" |  | ||||||
|                         to="/login" |  | ||||||
|                         style="background-color: transparent; box-shadow: none !important" |  | ||||||
|                     > |  | ||||||
|                         <v-tooltip |  | ||||||
|                             :text="t('logout')" |  | ||||||
|                             location="bottom" |  | ||||||
|                         > |  | ||||||
|                             <template v-slot:activator="{ props }"> |  | ||||||
|                                 <v-icon |  | ||||||
|                                     v-bind="props" |  | ||||||
|                                     icon="mdi-logout" |  | ||||||
|                                     size="x-large" |  | ||||||
|                                     color="#0e6942" |  | ||||||
|                                 ></v-icon> |  | ||||||
|                             </template> |  | ||||||
|                         </v-tooltip> |  | ||||||
|                     </v-btn> --> |  | ||||||
|         <v-dialog max-width="500"> |         <v-dialog max-width="500"> | ||||||
|             <template v-slot:activator="{ props: activatorProps }"> |             <template v-slot:activator="{ props: activatorProps }"> | ||||||
|                 <v-btn |                 <v-btn | ||||||
|                     v-bind="activatorProps" |                     v-bind="activatorProps" | ||||||
|                                 style="background-color: transparent; box-shadow: none !important" |                     :rounded="true" | ||||||
|  |                     variant="text" | ||||||
|                 > |                 > | ||||||
|                     <v-tooltip |                     <v-tooltip | ||||||
|                         :text="t('logout')" |                         :text="t('logout')" | ||||||
|  | @ -284,31 +155,56 @@ | ||||||
|                 </v-card> |                 </v-card> | ||||||
|             </template> |             </template> | ||||||
|         </v-dialog> |         </v-dialog> | ||||||
|                 </li> |  | ||||||
|                 <li> |  | ||||||
|         <v-avatar |         <v-avatar | ||||||
|             size="large" |             size="large" | ||||||
|             color="#0e6942" |             color="#0e6942" | ||||||
|                         style="font-size: large; font-weight: bold" |             class="user-button" | ||||||
|             >{{ initials }}</v-avatar |             >{{ initials }}</v-avatar | ||||||
|         > |         > | ||||||
|                 </li> |     </v-app-bar> | ||||||
|             </div> |     <v-navigation-drawer | ||||||
|         </nav> |         v-model="drawer" | ||||||
|         <router-view /> |         temporary | ||||||
|     </main> |         app | ||||||
|  |     > | ||||||
|  |         <v-list> | ||||||
|  |             <v-list-item | ||||||
|  |                 to="/user/assignment" | ||||||
|  |                 link | ||||||
|  |             > | ||||||
|  |                 <v-list-item-title class="menu_item">{{ t("assignments") }}</v-list-item-title> | ||||||
|  |             </v-list-item> | ||||||
|  | 
 | ||||||
|  |             <v-list-item | ||||||
|  |                 to="/user/class" | ||||||
|  |                 link | ||||||
|  |             > | ||||||
|  |                 <v-list-item-title class="menu_item">{{ t("classes") }}</v-list-item-title> | ||||||
|  |             </v-list-item> | ||||||
|  | 
 | ||||||
|  |             <v-list-item | ||||||
|  |                 to="/user/discussion" | ||||||
|  |                 link | ||||||
|  |             > | ||||||
|  |                 <v-list-item-title class="menu_item">{{ t("discussions") }}</v-list-item-title> | ||||||
|  |             </v-list-item> | ||||||
|  |         </v-list> | ||||||
|  |     </v-navigation-drawer> | ||||||
| </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) { | ||||||
|  |  | ||||||
|  | @ -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), | ||||||
|  |  | ||||||
|  | @ -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: [ | ||||||
|                 { |                 { | ||||||
|  |  | ||||||
|  | @ -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, | ||||||
|  |  | ||||||
|  | @ -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; | ||||||
|  |  | ||||||
|  | @ -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); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -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
									
									
									
								
							
							
						
						
									
										810
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
		Reference in a new issue