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